FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/iterm2enc.c
Date: 2026-09-15 09:36:26
Exec Total Coverage
Lines: 0 51 0.0%
Functions: 0 6 0.0%
Branches: 0 20 0.0%

Line Branch Exec Source
1 /*
2 * This file is part of FFmpeg.
3 *
4 * Copyright (c) 2026 Zhao Zhili <quinkblack@foxmail.com>
5 *
6 * FFmpeg is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * FFmpeg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with FFmpeg; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 #include <string.h>
22
23 #include "libavutil/base64.h"
24 #include "libavutil/macros.h"
25 #include "libavutil/mem.h"
26 #include "libavutil/opt.h"
27 #include "avformat.h"
28 #include "mux.h"
29
30 /* iTerm2 inline image protocol: https://iterm2.com/documentation-images.html */
31
32 #define ESC "\033"
33
34 #define SYNC_BEGIN ESC "[?2026h"
35 #define SYNC_END ESC "[?2026l"
36 #define CURSOR_SAVE ESC "7"
37 #define CURSOR_RESTORE ESC "8"
38 #define CURSOR_HOME ESC "[H"
39 #define CURSOR_BOTTOM ESC "[999H"
40
41 #define OSC_START ESC "]1337;"
42 #define BEL "\a"
43 #define ST ESC "\\"
44
45 /* tmux requires DCS passthrough with ST termination and ESC doubling */
46 #define TMUX_DCS ESC "Ptmux;"
47
48 /* iTerm2 and tmux silently drop a single OSC sequence >= 1 MiB, so split the
49 * image into chunks below that limit. Old tmux capped a sequence at 256 bytes,
50 * but the tiny chunks that would require flood the tmux parser and freeze the
51 * terminal, so we do not support such versions. */
52 #define FILEPART_CHUNK ((1 << 20) - 4096)
53
54 #define WRITE_LITERAL(pb, str) avio_write(pb, (const unsigned char *)(str), \
55 sizeof(str) - 1)
56
57 typedef struct ITerm2Context {
58 const AVClass *class;
59 char *display_width;
60 char *display_height;
61 int keep_aspect;
62 int tmux;
63 char *b64;
64 unsigned b64_size;
65 } ITerm2Context;
66
67 static void osc_open(ITerm2Context *c, AVIOContext *pb)
68 {
69 if (c->tmux)
70 WRITE_LITERAL(pb, TMUX_DCS ESC);
71 WRITE_LITERAL(pb, OSC_START);
72 }
73
74 static void osc_close(ITerm2Context *c, AVIOContext *pb)
75 {
76 WRITE_LITERAL(pb, BEL);
77 if (c->tmux)
78 WRITE_LITERAL(pb, ST);
79 }
80
81 static void write_image(ITerm2Context *c, AVIOContext *pb, int size)
82 {
83 size_t b64_len = strlen(c->b64);
84
85 osc_open(c, pb);
86 WRITE_LITERAL(pb, "MultipartFile=");
87 avio_printf(pb, "inline=1;size=%d", size);
88 if (c->display_width && c->display_width[0])
89 avio_printf(pb, ";width=%s", c->display_width);
90 if (c->display_height && c->display_height[0])
91 avio_printf(pb, ";height=%s", c->display_height);
92 if (!c->keep_aspect)
93 WRITE_LITERAL(pb, ";preserveAspectRatio=0");
94 osc_close(c, pb);
95
96 for (size_t off = 0; off < b64_len; off += FILEPART_CHUNK) {
97 size_t n = FFMIN(FILEPART_CHUNK, b64_len - off);
98
99 osc_open(c, pb);
100 WRITE_LITERAL(pb, "FilePart=");
101 avio_write(pb, c->b64 + off, n);
102 osc_close(c, pb);
103 }
104
105 osc_open(c, pb);
106 WRITE_LITERAL(pb, "FileEnd");
107 osc_close(c, pb);
108 }
109
110 static int iterm2_write_packet(AVFormatContext *s, AVPacket *pkt)
111 {
112 ITerm2Context *c = s->priv_data;
113
114 av_fast_malloc(&c->b64, &c->b64_size, AV_BASE64_SIZE(pkt->size));
115 if (!c->b64)
116 return AVERROR(ENOMEM);
117 if (!av_base64_encode(c->b64, c->b64_size, pkt->data, pkt->size))
118 return AVERROR(EINVAL);
119
120 /* Synchronized output swaps the frame in atomically. */
121 WRITE_LITERAL(s->pb, SYNC_BEGIN CURSOR_SAVE CURSOR_HOME);
122
123 write_image(c, s->pb, pkt->size);
124
125 WRITE_LITERAL(s->pb, CURSOR_RESTORE SYNC_END);
126
127 avio_flush(s->pb);
128
129 return 0;
130 }
131
132 static int iterm2_write_trailer(AVFormatContext *s)
133 {
134 WRITE_LITERAL(s->pb, CURSOR_BOTTOM "\n");
135
136 return 0;
137 }
138
139 static av_cold void iterm2_deinit(AVFormatContext *s)
140 {
141 ITerm2Context *c = s->priv_data;
142 av_freep(&c->b64);
143 }
144
145 #define OFFSET(x) offsetof(ITerm2Context, x)
146 #define ENC AV_OPT_FLAG_ENCODING_PARAM
147
148 static const AVOption options[] = {
149 { "display_width", "on-screen width (auto, N cells, Npx, N%%)",
150 OFFSET(display_width), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, ENC },
151 { "display_height", "on-screen height (auto, N cells, Npx, N%%)",
152 OFFSET(display_height), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, ENC },
153 { "keep_aspect", "preserve aspect ratio when scaling",
154 OFFSET(keep_aspect), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, ENC },
155 { "tmux", "wrap image in tmux DCS passthrough, requires tmux set -g allow-passthrough on",
156 OFFSET(tmux), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, ENC },
157 { NULL },
158 };
159
160 static const AVClass iterm2_class = {
161 .class_name = "iTerm2 muxer",
162 .item_name = av_default_item_name,
163 .option = options,
164 .version = LIBAVUTIL_VERSION_INT,
165 .category = AV_CLASS_CATEGORY_MUXER,
166 };
167
168 const FFOutputFormat ff_iterm2_muxer = {
169 .p.name = "iterm2",
170 .p.long_name = NULL_IF_CONFIG_SMALL("iTerm2 inline image protocol"),
171 .priv_data_size = sizeof(ITerm2Context),
172 .p.audio_codec = AV_CODEC_ID_NONE,
173 .p.video_codec = AV_CODEC_ID_MJPEG,
174 .write_packet = iterm2_write_packet,
175 .write_trailer = iterm2_write_trailer,
176 .deinit = iterm2_deinit,
177 .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH,
178 .p.flags = AVFMT_NOTIMESTAMPS | AVFMT_NODIMENSIONS,
179 .p.priv_class = &iterm2_class,
180 };
181