This is an automated email from the git hooks/post-receive script.
Git pushed a commit to branch master
in repository ffmpeg.
The following commit(s) were added to refs/heads/master by this push:
new 0a44e7ddc1 avformat: add iTerm2 inline image protocol muxer
0a44e7ddc1 is described below
commit 0a44e7ddc18d6ec58b23cad7b56f2ca0217f8768
Author: Zhao Zhili <[email protected]>
AuthorDate: Fri Jun 19 21:34:06 2026 +0800
Commit: Zhao Zhili <[email protected]>
CommitDate: Tue Jun 30 05:08:44 2026 +0000
avformat: add iTerm2 inline image protocol muxer
Add a muxer that wraps encoded image in the iTerm2 inline image protocol
(OSC 1337) so ffmpeg can play video directly in an iTerm2 terminal. The
output is a self-contained byte stream: it can be played live or saved
to a file and replayed with cat.
---
Changelog | 1 +
doc/muxers.texi | 52 ++++++++++++++
libavformat/Makefile | 1 +
libavformat/allformats.c | 1 +
libavformat/iterm2enc.c | 180 +++++++++++++++++++++++++++++++++++++++++++++++
libavformat/version.h | 2 +-
6 files changed, 236 insertions(+), 1 deletion(-)
diff --git a/Changelog b/Changelog
index 3268ab2cca..b7ff2ac602 100644
--- a/Changelog
+++ b/Changelog
@@ -22,6 +22,7 @@ version 9.0:
- Add AMF hardware memory mapping support.
- ONNX Runtime DNN backend with GPU execution provider support
- Remove deprecated NVENC options and support for pre-11.1 SDK versions
+- iTerm2 inline image protocol muxer
version 8.1:
diff --git a/doc/muxers.texi b/doc/muxers.texi
index 9a579ebebf..92d707ad9f 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -2722,6 +2722,58 @@ computer-generated compositions.
This muxer accepts a single audio stream containing PCM data.
+@section iterm2
+iTerm2 inline image protocol muxer.
+
+This muxer writes video frames as OSC 1337 inline images for display in
+terminals that support the iTerm2 image protocol. Use @option{-re} to limit
+the output rate to the source framerate; without it, frames are emitted as
+fast as they are encoded, which is usually not desired for live display.
+
+Frames are sent with the multipart form of the protocol, which splits each
+image across several short control sequences. This avoids the per-sequence
+size limit that otherwise discards large frames, and requires iTerm2 3.5 or
+newer.
+
+The output is a self-contained byte stream and can be redirected to a file.
+Replaying the file with @command{cat} displays the images in the terminal.
+
+@subsection Options
+@table @option
+@item display_width @var{size}
+Set the displayed image width. @var{size} can be @samp{auto}, @var{N} terminal
+cells, @var{N}px pixels, or @var{N}% of the terminal width. When unset, the
+terminal derives the width from the image.
+
+@item display_height @var{size}
+Set the displayed image height. @var{size} uses the same syntax as
+@option{display_width}. When unset, the terminal derives the height from the
+image.
+
+@item keep_aspect @var{bool}
+Preserve the input aspect ratio when scaling. Default is enabled.
+
+@item tmux @var{bool}
+Wrap image data in tmux DCS passthrough. This requires a tmux version whose
+passthrough sequence size limit is large enough for image data, with
+passthrough enabled via @command{tmux set -g allow-passthrough on}. Default is
+disabled.
+@end table
+
+@subsection Examples
+
+Display a video in an iTerm2 terminal:
+@example
+ffmpeg -re -i input.mp4 -f iterm2 -
+@end example
+
+Scale the displayed image to 40 terminal cells tall. Inside tmux, enable
+passthrough first with @command{tmux set -g allow-passthrough on}, then add
+@option{tmux}:
+@example
+ffmpeg -re -i input.mp4 -f iterm2 -display_height 40 -tmux 1 -
+@end example
+
@section ivf
On2 IVF muxer.
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 752436cf5f..b244eaabf9 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -338,6 +338,7 @@ OBJS-$(CONFIG_IPU_DEMUXER) += ipudec.o
rawdec.o
OBJS-$(CONFIG_IRCAM_DEMUXER) += ircamdec.o ircam.o pcm.o
OBJS-$(CONFIG_IRCAM_MUXER) += ircamenc.o ircam.o rawenc.o
OBJS-$(CONFIG_ISS_DEMUXER) += iss.o
+OBJS-$(CONFIG_ITERM2_MUXER) += iterm2enc.o
OBJS-$(CONFIG_IV8_DEMUXER) += iv8.o
OBJS-$(CONFIG_IVF_DEMUXER) += ivfdec.o
OBJS-$(CONFIG_IVF_MUXER) += ivfenc.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index af7eea5e5c..05624e66eb 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -242,6 +242,7 @@ extern const FFInputFormat ff_ircam_demuxer;
extern const FFOutputFormat ff_ircam_muxer;
extern const FFOutputFormat ff_ismv_muxer;
extern const FFInputFormat ff_iss_demuxer;
+extern const FFOutputFormat ff_iterm2_muxer;
extern const FFInputFormat ff_iv8_demuxer;
extern const FFInputFormat ff_ivf_demuxer;
extern const FFOutputFormat ff_ivf_muxer;
diff --git a/libavformat/iterm2enc.c b/libavformat/iterm2enc.c
new file mode 100644
index 0000000000..5503b08266
--- /dev/null
+++ b/libavformat/iterm2enc.c
@@ -0,0 +1,180 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * Copyright (c) 2026 Zhao Zhili <[email protected]>
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <string.h>
+
+#include "libavutil/base64.h"
+#include "libavutil/macros.h"
+#include "libavutil/mem.h"
+#include "libavutil/opt.h"
+#include "avformat.h"
+#include "mux.h"
+
+/* iTerm2 inline image protocol: https://iterm2.com/documentation-images.html
*/
+
+#define ESC "\033"
+
+#define SYNC_BEGIN ESC "[?2026h"
+#define SYNC_END ESC "[?2026l"
+#define CURSOR_SAVE ESC "7"
+#define CURSOR_RESTORE ESC "8"
+#define CURSOR_HOME ESC "[H"
+#define CURSOR_BOTTOM ESC "[999H"
+
+#define OSC_START ESC "]1337;"
+#define BEL "\a"
+#define ST ESC "\\"
+
+/* tmux requires DCS passthrough with ST termination and ESC doubling */
+#define TMUX_DCS ESC "Ptmux;"
+
+/* iTerm2 and tmux silently drop a single OSC sequence >= 1 MiB, so split the
+ * image into chunks below that limit. Old tmux capped a sequence at 256 bytes,
+ * but the tiny chunks that would require flood the tmux parser and freeze the
+ * terminal, so we do not support such versions. */
+#define FILEPART_CHUNK ((1 << 20) - 4096)
+
+#define WRITE_LITERAL(pb, str) avio_write(pb, (const unsigned char *)(str), \
+ sizeof(str) - 1)
+
+typedef struct ITerm2Context {
+ const AVClass *class;
+ char *display_width;
+ char *display_height;
+ int keep_aspect;
+ int tmux;
+ char *b64;
+ unsigned b64_size;
+} ITerm2Context;
+
+static void osc_open(ITerm2Context *c, AVIOContext *pb)
+{
+ if (c->tmux)
+ WRITE_LITERAL(pb, TMUX_DCS ESC);
+ WRITE_LITERAL(pb, OSC_START);
+}
+
+static void osc_close(ITerm2Context *c, AVIOContext *pb)
+{
+ WRITE_LITERAL(pb, BEL);
+ if (c->tmux)
+ WRITE_LITERAL(pb, ST);
+}
+
+static void write_image(ITerm2Context *c, AVIOContext *pb, int size)
+{
+ size_t b64_len = strlen(c->b64);
+
+ osc_open(c, pb);
+ WRITE_LITERAL(pb, "MultipartFile=");
+ avio_printf(pb, "inline=1;size=%d", size);
+ if (c->display_width && c->display_width[0])
+ avio_printf(pb, ";width=%s", c->display_width);
+ if (c->display_height && c->display_height[0])
+ avio_printf(pb, ";height=%s", c->display_height);
+ if (!c->keep_aspect)
+ WRITE_LITERAL(pb, ";preserveAspectRatio=0");
+ osc_close(c, pb);
+
+ for (size_t off = 0; off < b64_len; off += FILEPART_CHUNK) {
+ size_t n = FFMIN(FILEPART_CHUNK, b64_len - off);
+
+ osc_open(c, pb);
+ WRITE_LITERAL(pb, "FilePart=");
+ avio_write(pb, c->b64 + off, n);
+ osc_close(c, pb);
+ }
+
+ osc_open(c, pb);
+ WRITE_LITERAL(pb, "FileEnd");
+ osc_close(c, pb);
+}
+
+static int iterm2_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ ITerm2Context *c = s->priv_data;
+
+ av_fast_malloc(&c->b64, &c->b64_size, AV_BASE64_SIZE(pkt->size));
+ if (!c->b64)
+ return AVERROR(ENOMEM);
+ if (!av_base64_encode(c->b64, c->b64_size, pkt->data, pkt->size))
+ return AVERROR(EINVAL);
+
+ /* Synchronized output swaps the frame in atomically. */
+ WRITE_LITERAL(s->pb, SYNC_BEGIN CURSOR_SAVE CURSOR_HOME);
+
+ write_image(c, s->pb, pkt->size);
+
+ WRITE_LITERAL(s->pb, CURSOR_RESTORE SYNC_END);
+
+ avio_flush(s->pb);
+
+ return 0;
+}
+
+static int iterm2_write_trailer(AVFormatContext *s)
+{
+ WRITE_LITERAL(s->pb, CURSOR_BOTTOM "\n");
+
+ return 0;
+}
+
+static av_cold void iterm2_deinit(AVFormatContext *s)
+{
+ ITerm2Context *c = s->priv_data;
+ av_freep(&c->b64);
+}
+
+#define OFFSET(x) offsetof(ITerm2Context, x)
+#define ENC AV_OPT_FLAG_ENCODING_PARAM
+
+static const AVOption options[] = {
+ { "display_width", "on-screen width (auto, N cells, Npx, N%%)",
+ OFFSET(display_width), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, ENC
},
+ { "display_height", "on-screen height (auto, N cells, Npx, N%%)",
+ OFFSET(display_height), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, ENC
},
+ { "keep_aspect", "preserve aspect ratio when scaling",
+ OFFSET(keep_aspect), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, ENC },
+ { "tmux", "wrap image in tmux DCS passthrough, requires tmux set -g
allow-passthrough on",
+ OFFSET(tmux), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, ENC },
+ { NULL },
+};
+
+static const AVClass iterm2_class = {
+ .class_name = "iTerm2 muxer",
+ .item_name = av_default_item_name,
+ .option = options,
+ .version = LIBAVUTIL_VERSION_INT,
+ .category = AV_CLASS_CATEGORY_MUXER,
+};
+
+const FFOutputFormat ff_iterm2_muxer = {
+ .p.name = "iterm2",
+ .p.long_name = NULL_IF_CONFIG_SMALL("iTerm2 inline image protocol"),
+ .priv_data_size = sizeof(ITerm2Context),
+ .p.audio_codec = AV_CODEC_ID_NONE,
+ .p.video_codec = AV_CODEC_ID_MJPEG,
+ .write_packet = iterm2_write_packet,
+ .write_trailer = iterm2_write_trailer,
+ .deinit = iterm2_deinit,
+ .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH,
+ .p.flags = AVFMT_NOTIMESTAMPS | AVFMT_NODIMENSIONS,
+ .p.priv_class = &iterm2_class,
+};
diff --git a/libavformat/version.h b/libavformat/version.h
index 904e7f06aa..7ff1483912 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,7 +31,7 @@
#include "version_major.h"
-#define LIBAVFORMAT_VERSION_MINOR 2
+#define LIBAVFORMAT_VERSION_MINOR 3
#define LIBAVFORMAT_VERSION_MICRO 100
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]