Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
 help / color / mirror / Atom feed
From: kimapr <code@ffmpeg.org>
To: ffmpeg-devel@ffmpeg.org
Subject: [FFmpeg-devel] [PATCH] WIP: avformat: add libbytebeat demuxer (PR #20207)
Date: Sun, 10 Aug 2025 16:51:56 +0300 (EEST)
Message-ID: <20250810135156.2389268D232@ffbox0-bg.ffmpeg.org> (raw)

PR #20207 opened by kimapr
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/20207
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/20207.patch

A demuxer for bytebeat, which is basically music that is written by creating a small (or not) snippet of (usually C or Javascript) code that generates raw audio data.

based on [libbytebeat](https://git.kimapr.net/kimapr/libbytebeat/), which is also my work.  libbytebeat aims to be compatible with [SthephanShi's bytebeat-composer](https://dollchan.net/bytebeat), which a fair amount of bytebeat songs seem to be written for.  There isn't a widely accepted standard file format for sharing bytebeat music, usually people just throw around URLs to the web player(s) with the song encoded in said URL.  libbytebeat's file format is just the song's code with a metadata block at the beginning, [concept borrowed from web userscripts](https://wiki.greasespot.net/Metadata_Block).  i'm not sure what file extension is appropriate, so just used .js because it's javascript, other suggestions welcome

besides audio this demuxer can also output visual data in the form of subtitles on top of a repeating black video frame. (TODO: detect font glyph width to set video width more precisely, maybe options to change font)

Some sample files: https://mindcore.kimapr.net/lappy/uploads/caa86d8e5ee699ee32e0a52aa9e6141e/bytebeats-collection.tar.gz

## Caveats

- The demuxer runs potentially malicious Javascript code on the user's computer.  It's sandboxed with no way to communicate with the outside world other than passing audio/errors to libbytebeat so it shouldn't be a security issue, but it can still do some mischief like eat all of available CPU and RAM (as shown in `run_this_to_destroy_your_computer.js` in the linked file).  there might be a way to limit this though.
- Libbytebeat uses JavaScriptCore (specifically, JavaScriptCoreGTK) to run Javascript code.  JavaScriptCore is only distributed together with WebKit, which has lots of dependencies surely including FFmpeg directly or indirectly.  As this would result in a circular dependency (FFmpeg -> libbytebeat -> WebKitGTK -> ... -> FFmpeg) packaging FFmpeg with this demuxer enabled might be a challenge.
- I'm not very familiar with FFmpeg internals (or even it's public API), so the demuxer might be doing lots of things wrong.  I did manage to get it to play well in mpv though.
- i named one variable `has_t` because i forgot you can't do that. i'll fix that later


IDK if this demuxer fits for inclusion into FFmpeg, it's a pretty cursed idea. 


From b21dd8a1683c899ab499c2e4a5d0a91d0a9bfd48 Mon Sep 17 00:00:00 2001
From: Kimapr <root@kimapr.net>
Date: Sun, 10 Aug 2025 13:33:19 +0500
Subject: [PATCH] avformat: add libbytebeat demuxer

---
 configure                 |   4 +
 libavformat/Makefile      |   1 +
 libavformat/allformats.c  |   1 +
 libavformat/libbytebeat.c | 427 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 433 insertions(+)
 create mode 100644 libavformat/libbytebeat.c

diff --git a/configure b/configure
index 727c3daea8..f9f9058058 100755
--- a/configure
+++ b/configure
@@ -217,6 +217,7 @@ External library support:
   --enable-libaribcaption  enable ARIB text and caption decoding via libaribcaption [no]
   --enable-libass          enable libass subtitles rendering,
                            needed for subtitles and ass filter [no]
+  --enable-libbytebeat     enable generative bytebeat audio via libbytebeat [no]
   --enable-libbluray       enable BluRay reading using libbluray [no]
   --enable-libbs2b         enable bs2b DSP library [no]
   --enable-libcaca         enable textual display using libcaca [no]
@@ -1930,6 +1931,7 @@ EXTERNAL_LIBRARY_LIST="
     libass
     libbluray
     libbs2b
+    libbytebeat
     libcaca
     libcelt
     libcodec2
@@ -3597,6 +3599,7 @@ libaom_av1_encoder_deps="libaom"
 libaom_av1_encoder_select="extract_extradata_bsf dovi_rpuenc"
 libaribb24_decoder_deps="libaribb24"
 libaribcaption_decoder_deps="libaribcaption"
+libbytebeat_demuxer_deps="libbytebeat"
 libcelt_decoder_deps="libcelt"
 libcodec2_decoder_deps="libcodec2"
 libcodec2_encoder_deps="libcodec2"
@@ -7028,6 +7031,7 @@ enabled libiec61883       && require libiec61883 libiec61883/iec61883.h iec61883
 enabled libass            && require_pkg_config libass "libass >= 0.11.0" ass/ass.h ass_library_init
 enabled libbluray         && require_pkg_config libbluray libbluray libbluray/bluray.h bd_open
 enabled libbs2b           && require_pkg_config libbs2b libbs2b bs2b.h bs2b_open
+enabled libbytebeat       && require_pkg_config libbytebeat libbytebeat bytebeat.h bytebeat_open
 enabled libcelt           && require libcelt celt/celt.h celt_decode -lcelt0 &&
                              { check_lib libcelt celt/celt.h celt_decoder_create_custom -lcelt0 ||
                                die "ERROR: libcelt must be installed and version must be >= 0.11.0."; }
diff --git a/libavformat/Makefile b/libavformat/Makefile
index ab5551a735..aed063b587 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -665,6 +665,7 @@ OBJS-$(CONFIG_YUV4MPEGPIPE_MUXER)        += yuv4mpegenc.o
 # external library muxers/demuxers
 OBJS-$(CONFIG_AVISYNTH_DEMUXER)          += avisynth.o
 OBJS-$(CONFIG_CHROMAPRINT_MUXER)         += chromaprint.o
+OBJS-$(CONFIG_LIBBYTEBEAT_DEMUXER)       += libbytebeat.o
 OBJS-$(CONFIG_LIBGME_DEMUXER)            += libgme.o
 OBJS-$(CONFIG_LIBMODPLUG_DEMUXER)        += libmodplug.o
 OBJS-$(CONFIG_LIBOPENMPT_DEMUXER)        += libopenmpt.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index e39eab8e85..27e83c2d44 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -577,6 +577,7 @@ extern const FFInputFormat  ff_image_xwd_pipe_demuxer;
 extern const FFInputFormat  ff_avisynth_demuxer;
 extern const FFOutputFormat ff_chromaprint_muxer;
 extern const FFInputFormat  ff_dvdvideo_demuxer;
+extern const FFInputFormat  ff_libbytebeat_demuxer;
 extern const FFInputFormat  ff_libgme_demuxer;
 extern const FFInputFormat  ff_libmodplug_demuxer;
 extern const FFInputFormat  ff_libopenmpt_demuxer;
diff --git a/libavformat/libbytebeat.c b/libavformat/libbytebeat.c
new file mode 100644
index 0000000000..985790a5dd
--- /dev/null
+++ b/libavformat/libbytebeat.c
@@ -0,0 +1,427 @@
+/*
+ * bytebeat demuxer (libbytebeat)
+ * Copyright (c) 2025 kimapr
+ *
+ * This file is part of FFmpeg.
+ *
+ * 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 <bytebeat.h>
+#include <stdlib.h>
+
+#include "libavutil/avstring.h"
+#include "libavutil/bprint.h"
+#include "libavutil/channel_layout.h"
+#include "libavutil/common.h"
+#include "libavutil/opt.h"
+#include "libavcodec/ass.h"
+#include "avformat.h"
+#include "demux.h"
+#include "internal.h"
+
+
+typedef struct BBContext {
+    const AVClass *class;
+    bytebeat_State *state;
+    bytebeat_Opts *opts;
+
+    double duration;
+    int sample_rate;
+    int channel_count;
+    int tw, th;
+    int has_t;
+    int vw, vh;
+    bytebeat_Pkt *pkt;
+    int subi;
+    double next_video_pos;
+    long long lowest_sst_pts;
+    AVPacket *video_pkt;
+} BBContext;
+
+static const AVOption options[] = {
+    { NULL }
+};
+
+static void bb_logger(const char *message, long long t, void *userdata)
+{
+    if (t == -1) {
+        av_log(userdata, AV_LOG_ERROR, "%s\n", message);
+    }
+}
+
+static int read_close_bytebeat(AVFormatContext *s);
+
+static int read_header_bytebeat(AVFormatContext *s)
+{
+    AVStream *ast, *sst, *vst;
+    BBContext *bb = s->priv_data;
+    int64_t size;
+    char *buf;
+    int known_duration = 0;
+    bytebeat_MetaValue mtv;
+    bb->opts = NULL;
+    bb->state = NULL;
+    bb->pkt = NULL;
+    bb->video_pkt = NULL;
+    bb->lowest_sst_pts = -1;
+    bb->subi = 0;
+
+    size = avio_size(s->pb);
+    if (size <= 0)
+        return AVERROR_INVALIDDATA;
+    buf = av_malloc(size);
+    if (!buf)
+        return AVERROR(ENOMEM);
+    size = avio_read(s->pb, buf, size);
+    if (size < 0) {
+        av_log(s, AV_LOG_ERROR, "Reading input buffer failed.\n");
+        av_freep(&buf);
+        return size;
+    }
+
+    bb->opts = bytebeat_default_opts();
+    if (!bb->opts) {
+        av_freep(&buf);
+        read_close_bytebeat(s);
+        return AVERROR(ENOMEM);
+    }
+    bb->opts->code = buf;
+    bb->opts->code_len = size;
+    bb->opts->logger = bb_logger;
+
+    bb->state = bytebeat_open(bb->opts);
+    if (!bb->state) {
+        read_close_bytebeat(s);
+        return AVERROR_INVALIDDATA;
+    }
+
+    mtv.type = BYTEBEAT_META_F64;
+    bytebeat_meta_get(bb->state, "freq", &mtv);
+    bb->sample_rate = mtv.v_f64;
+
+    mtv.type = BYTEBEAT_META_I64;
+    bytebeat_meta_get(bb->state, "chanc", &mtv);
+    bb->channel_count = mtv.v_i64;
+
+    mtv.type = BYTEBEAT_META_I64;
+    bytebeat_meta_get(bb->state, "len", &mtv);
+    bb->duration = ((double)mtv.v_i64)/bb->sample_rate;
+
+    for (const char *keys[] = {
+        "album", "album_artist",
+        "artist", "comment",
+        "composer", "copyright",
+        "creation_time", "date",
+        "disc", "encoder",
+        "encoded_by", "filename",
+        "genre", "language",
+        "performer", "publisher",
+        "service_name", "service_provider",
+        "title", "track", NULL
+    }, **key = keys; *key != NULL; key++) {
+        mtv.type = BYTEBEAT_META_STR;
+        if (bytebeat_meta_get(bb->state, *key, &mtv))
+            continue;
+        av_dict_set(&s->metadata, *key, mtv.v_str, 0);
+        bytebeat_meta_free(&mtv);
+    }
+
+    ast = avformat_new_stream(s, NULL);
+    if (!ast) {
+        read_close_bytebeat(s);
+        return AVERROR(ENOMEM);
+    }
+    avpriv_set_pts_info(ast, 64, 1, AV_TIME_BASE);
+    if (bb->duration >= 0 && bb->duration < ((double)INT64_MAX + 1) / AV_TIME_BASE) {
+        known_duration = 1;
+        ast->duration = llrint(bb->duration*AV_TIME_BASE);
+    }
+
+    ast->codecpar->codec_type  = AVMEDIA_TYPE_AUDIO;
+    ast->codecpar->codec_id    = AV_NE(AV_CODEC_ID_PCM_F32BE, AV_CODEC_ID_PCM_F32LE);
+    ast->codecpar->sample_rate = bb->sample_rate;
+    av_channel_layout_default(&ast->codecpar->ch_layout, bb->channel_count);
+
+    if (bb->has_t = !bytebeat_get_term_size(bb->state, &bb->tw, &bb->th)) {
+        vst = avformat_new_stream(s, NULL);
+        sst = avformat_new_stream(s, NULL);
+        if ((!vst) || (!sst)) {
+            read_close_bytebeat(s);
+            return AVERROR(ENOMEM);
+        }
+        avpriv_set_pts_info(vst, 64, 1, AV_TIME_BASE);
+        avpriv_set_pts_info(sst, 64, 1, 100);
+        if (known_duration) {
+            vst->duration = ast->duration;
+        }
+        vst->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+        vst->codecpar->codec_id = AV_CODEC_ID_RAWVIDEO;
+        if (bb->vw < 0 || bb->vh < 0 || (double)bb->vw*bb->vh >= (double)SIZE_MAX + 1) {
+            vst->codecpar->height = bb->vw = 0;
+            vst->codecpar->width = bb->vh = 0;
+        } else {
+            vst->codecpar->height = bb->vh = 32 * bb->th;
+            vst->codecpar->width = bb->vw = 16 * bb->tw;
+        }
+        vst->codecpar->format = AV_PIX_FMT_GRAY8;
+        vst->codecpar->framerate = (AVRational){ 0, 1 };
+        bb->next_video_pos = 0;
+
+        sst->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
+        sst->codecpar->codec_id = AV_CODEC_ID_ASS;
+        sst->disposition = AV_DISPOSITION_FORCED | AV_DISPOSITION_DEFAULT;
+
+        sst->codecpar->extradata = av_asprintf(
+            "[Script Info]\r\n"
+            "ScriptType: v4.00+\r\n"
+            "ScaledBorderAndShadow: yes\r\n"
+            "YCbCr Matrix: None\r\n"
+            "PlayResX: %i\r\n"
+            "PlayResY: %i\r\n"
+            "WrapStyle: 2\r\n"
+            "\r\n"
+            "[V4+ Styles]\r\n"
+            "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, "
+            "Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, "
+            "Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n"
+
+            "Style: Default,monospace,32,&HFFFFFFF,,,,-1,0,0,0,100,100,0,0,1,0.4,0,7,0,0,0,1\r\n"
+            "\r\n"
+            "[Events]\r\n"
+            "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n",
+            bb->vw, bb->vh);
+        if (!sst->codecpar->extradata) {
+            read_close_bytebeat(s);
+            return AVERROR(ENOMEM);
+        }
+        sst->codecpar->extradata_size = strlen(sst->codecpar->extradata);
+    }
+
+    return 0;
+}
+
+static int read_packet_bytebeat(AVFormatContext *s, AVPacket *pkt)
+{
+    BBContext *bb = s->priv_data;
+    bytebeat_Pos pos;
+    size_t len;
+    int ret;
+
+    if (!bb->state)
+        return AVERROR_INVALIDDATA;
+
+    pos.type = BYTEBEAT_POSTYPE_SECS;
+    bytebeat_get_pos(bb->state, &pos);
+
+    if (bb->has_t && pos.seconds >= bb->next_video_pos) {
+        if (!bb->video_pkt) {
+            bb->video_pkt = av_packet_alloc();
+            if (!bb->video_pkt)
+                return AVERROR(ENOMEM);
+            if ((ret = av_new_packet(bb->video_pkt, (size_t)bb->vw*bb->vh))) {
+                av_packet_free(&bb->video_pkt);
+                return ret;
+            }
+            for (char *pos = bb->video_pkt->data; pos < (char*)bb->video_pkt->data + (size_t)bb->vw*bb->vh; pos++)
+                *pos = 0;
+        }
+
+        if ((ret = av_packet_ref(pkt, bb->video_pkt)))
+            return ret;
+        if (pos.seconds == 0)
+            pkt->duration = 1;
+        else
+            pkt->duration = 1./25. * AV_TIME_BASE;
+        bb->next_video_pos += (double)pkt->duration / AV_TIME_BASE;
+        pkt->stream_index = 1;
+        if (bb->next_video_pos >= 0 && bb->next_video_pos < ((double)INT64_MAX + 1) / AV_TIME_BASE)
+            pkt->pts = llrint(bb->next_video_pos * AV_TIME_BASE);
+        return 0;
+    }
+
+    if (!bb->pkt) {
+        bb->pkt = bytebeat_read(bb->state);
+        bb->subi = 0;
+    }
+
+    if (!bb->pkt)
+        return AVERROR_EOF;
+
+    if (bb->pkt) {
+        size_t sublen;
+        int sret = 0, subbed = false;
+        if (bb->has_t) {
+            bytebeat_TideoFrame *tpkt = bytebeat_pkt_get_tideo(bb->pkt, &sublen);
+            while (sublen - bb->subi > 0) {
+                char *data;
+                subbed = true;
+                AVBPrint sbuf;
+                av_bprint_init(&sbuf, 0, -1);
+                size_t i = bb->subi++;
+                if (bb->lowest_sst_pts == -1)
+                    bb->lowest_sst_pts = tpkt[i].position;
+                long long beginp = tpkt[i].position - bb->lowest_sst_pts;
+                av_bprintf(&sbuf, "%lld,0,Default,,0,0,0,,", beginp);
+                for (char *c = tpkt[i].text; *c != 0; c++) {
+                    if (*c == '{' || *c == '}' || *c == '\\')
+                        av_bprintf(&sbuf, "\\%c", *c);
+                    else if (*c == '\n')
+                        av_bprintf(&sbuf, "\\N");
+                    else
+                        av_bprint_chars(&sbuf, *c, 1);
+                }
+                if ((sret = !av_bprint_is_complete(&sbuf))) {
+                    av_bprint_clear(&sbuf);
+                    break;
+                }
+                if ((sret = av_bprint_finalize(&sbuf, &data))) {
+                    break;
+                }
+                if ((sret = av_packet_from_data(pkt, data, sbuf.len))) {
+                    av_freep(&data);
+                    break;
+                }
+                pkt->size = sbuf.len;
+                pkt->pts = tpkt[i].position;
+                pkt->duration = tpkt[i].duration;
+                pkt->stream_index = 2;
+                break;
+            }
+            if (subbed)
+                return sret;
+        }
+
+        len = bb->channel_count * bb->pkt->sample_count * sizeof(float);
+        if ((ret = av_new_packet(pkt, len))) {
+            return ret;
+        }
+
+        memcpy(pkt->data, bb->pkt->data, len);
+
+        pkt->size = len;
+        double ppos = (double)bb->pkt->sample_position / bb->sample_rate;
+        if (ppos >= 0 && ppos < ((double)INT64_MAX + 1) / AV_TIME_BASE)
+            pkt->pts = llrint(ppos * AV_TIME_BASE);
+
+        bytebeat_pkt_free(bb->pkt);
+        bb->pkt = NULL;
+
+        pkt->stream_index = 0;
+    }
+
+    return 0;
+}
+
+static int read_close_bytebeat(AVFormatContext *s)
+{
+    BBContext *bb = s->priv_data;
+    if (bb->state) {
+        bytebeat_close(bb->state);
+        bb->state = NULL;
+    }
+    if (bb->opts) {
+        av_freep(&bb->opts->code);
+        free(bb->opts);
+        bb->opts = NULL;
+    }
+    if (bb->pkt) {
+        bytebeat_pkt_free(bb->pkt);
+        bb->pkt = NULL;
+    }
+    if (bb->video_pkt) {
+        av_packet_free(&bb->video_pkt);
+    }
+    return 0;
+}
+
+static int read_seek_bytebeat(AVFormatContext *s, int stream_idx, int64_t ts, int flags)
+{
+    BBContext *bb = s->priv_data;
+    bytebeat_Pos pos;
+    double old_pos_s;
+    pos.type = BYTEBEAT_POSTYPE_SECS;
+
+    if (!bb->state)
+        return AVERROR_INVALIDDATA;
+
+    bytebeat_get_pos(bb->state, &pos);
+    old_pos_s = pos.seconds;
+
+    pos.seconds = (double)ts/AV_TIME_BASE;
+
+    if (pos.seconds < 0)
+        pos.seconds = 0;
+    bb->next_video_pos = pos.seconds;
+
+    if (bb->pkt) {
+        bytebeat_pkt_free(bb->pkt);
+        bb->pkt = NULL;
+    }
+
+    if (bytebeat_set_pos(bb->state, &pos)) {
+        // seeking is NOT possible.
+        // for backwards seeking, just reopen the song as a last resort
+        // forwards, simply fail and let the player figure it out
+        if (pos.seconds < old_pos_s) {
+            av_log(bb, AV_LOG_WARNING, "seeking backwards is not possible, rebooting song (%f -> %f)\n", old_pos_s, pos.seconds);
+            bytebeat_close(bb->state);
+            bb->state = bytebeat_open(bb->opts);
+            if (!bb->state)
+                return AVERROR_INVALIDDATA;
+            return 0;
+        }
+        av_log(bb, AV_LOG_WARNING, "seeking forwards is not possible (%f -> %f)\n", old_pos_s, pos.seconds);
+        return AVERROR_UNKNOWN;
+    }
+    return 0;
+}
+
+static int read_probe_bytebeat(const AVProbeData *p)
+{
+    enum bytebeat_ProbeResult result;
+
+    if (!p->buf)
+        return 0;
+
+    result = bytebeat_probe(p->buf, p->buf_size);
+
+    if (result == BYTEBEAT_PROBE_GOOD)
+        return AVPROBE_SCORE_MIME + 1;
+    if (result == BYTEBEAT_PROBE_UNSURE)
+        return AVPROBE_SCORE_RETRY;
+    return 0;
+}
+
+static const AVClass class_bytebeat = {
+    .class_name = "libbytebeat",
+    .item_name  = av_default_item_name,
+    .option     = options,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+const FFInputFormat ff_libbytebeat_demuxer = {
+    .p.name         = "libbytebeat",
+    .p.long_name    = NULL_IF_CONFIG_SMALL("Generative audio (libbytebeat)"),
+    .p.priv_class   = &class_bytebeat,
+    .p.extensions   = "js",
+    .priv_data_size = sizeof(BBContext),
+    .flags_internal = FF_INFMT_FLAG_INIT_CLEANUP,
+    .read_probe     = read_probe_bytebeat,
+    .read_header    = read_header_bytebeat,
+    .read_packet    = read_packet_bytebeat,
+    .read_close     = read_close_bytebeat,
+    .read_seek      = read_seek_bytebeat,
+};
-- 
2.49.1

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

                 reply	other threads:[~2025-08-10 13:52 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250810135156.2389268D232@ffbox0-bg.ffmpeg.org \
    --to=code@ffmpeg.org \
    --cc=ffmpeg-devel@ffmpeg.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

This inbox may be cloned and mirrored by anyone:

	git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \
		ffmpegdev@gitmailbox.com
	public-inbox-index ffmpegdev

Example config snippet for mirrors.


AGPL code for this site: git clone https://public-inbox.org/public-inbox.git