This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

commit 4c0d563f858f3996d9d1aaab20823f65c14e027d
Author:     Priyanshu Thapliyal <[email protected]>
AuthorDate: Wed Apr 8 19:26:42 2026 +0530
Commit:     michaelni <[email protected]>
CommitDate: Thu Apr 9 03:01:43 2026 +0000

    avformat/pdvenc: add Playdate video muxer
    
    Add a muxer for the Playdate PDV container format.
    
    The muxer writes the frame table and packet layout required by the
    Playdate runtime. It requires seekable output and a predeclared
    maximum number of frames (-max_frames).
    
    Includes validation for single video stream input, dimension and
    framerate checks, and bounded payload/table offset checks. The frame
    entry table is allocated once in write_header() using max_frames + 1.
    
    Document the muxer in doc/muxers.texi and add a Changelog entry.
---
 Changelog                |   1 +
 doc/muxers.texi          |  13 +++
 libavformat/Makefile     |   1 +
 libavformat/allformats.c |   1 +
 libavformat/pdvenc.c     | 232 +++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 248 insertions(+)

diff --git a/Changelog b/Changelog
index a4241958bc..35e00d47aa 100644
--- a/Changelog
+++ b/Changelog
@@ -4,6 +4,7 @@ releases are sorted from youngest to oldest.
 version <next>:
 - Extend AMF Color Converter (vf_vpp_amf) HDR capabilities
 - LCEVC track muxing support in MP4 muxer
+- Playdate video encoder and muxer
 
 
 version 8.1:
diff --git a/doc/muxers.texi b/doc/muxers.texi
index d8b3ca4a8c..d9e475dbf7 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -3274,6 +3274,19 @@ ogg files can be safely chained.
 
 @end table
 
+@section pdv
+
+Playdate Video muxer.
+
+This muxer writes the Playdate video container used by Panic's Playdate SDK.
+It requires a seekable output and a single PDV video stream.
+
+@table @option
+@item max_frames @var{frames}
+Reserve space for at most @var{frames} video frames in the file header. This
+option is mandatory.
+@end table
+
 @anchor{rcwtenc}
 @section rcwt
 
diff --git a/libavformat/Makefile b/libavformat/Makefile
index ec8aa551d7..7c2fcad93e 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -502,6 +502,7 @@ OBJS-$(CONFIG_PCM_U8_MUXER)              += pcmenc.o 
rawenc.o
 OBJS-$(CONFIG_PCM_VIDC_DEMUXER)          += pcmdec.o pcm.o
 OBJS-$(CONFIG_PCM_VIDC_MUXER)            += pcmenc.o rawenc.o
 OBJS-$(CONFIG_PDV_DEMUXER)               += pdvdec.o
+OBJS-$(CONFIG_PDV_MUXER)                 += pdvenc.o
 OBJS-$(CONFIG_PJS_DEMUXER)               += pjsdec.o subtitles.o
 OBJS-$(CONFIG_PMP_DEMUXER)               += pmpdec.o
 OBJS-$(CONFIG_PP_BNK_DEMUXER)            += pp_bnk.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index 6ec361fb7b..a491e3a7e8 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -385,6 +385,7 @@ extern const FFInputFormat  ff_pcm_u16le_demuxer;
 extern const FFOutputFormat ff_pcm_u16le_muxer;
 extern const FFInputFormat  ff_pcm_u8_demuxer;
 extern const FFOutputFormat ff_pcm_u8_muxer;
+extern const FFOutputFormat ff_pdv_muxer;
 extern const FFInputFormat  ff_pdv_demuxer;
 extern const FFInputFormat  ff_pjs_demuxer;
 extern const FFInputFormat  ff_pmp_demuxer;
diff --git a/libavformat/pdvenc.c b/libavformat/pdvenc.c
new file mode 100644
index 0000000000..72fc611acc
--- /dev/null
+++ b/libavformat/pdvenc.c
@@ -0,0 +1,232 @@
+/*
+ * PDV muxer
+ *
+ * Copyright (c) 2026 Priyanshu Thapliyal <[email protected]>
+ *
+ * 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 "libavutil/mem.h"
+#include "libavutil/opt.h"
+#include "libavutil/rational.h"
+#include "avformat.h"
+#include "avio_internal.h"
+#include "mux.h"
+
+#define PDV_MAGIC "Playdate VID\x00\x00\x00\x00"
+#define PDV_MAX_FRAMES UINT16_MAX
+#define PDV_MAX_OFFSET ((1U << 30) - 1)
+
+typedef struct PDVMuxContext {
+    uint32_t *entries;
+    int nb_frames;
+    int max_frames;
+    uint32_t fps_bits;
+    int64_t nb_frames_pos;
+    int64_t table_pos;
+    int64_t payload_start;
+} PDVMuxContext;
+
+static void pdv_deinit(AVFormatContext *s)
+{
+    PDVMuxContext *pdv = s->priv_data;
+
+    av_freep(&pdv->entries);
+}
+
+static int pdv_get_fps(AVFormatContext *s, AVStream *st, uint32_t *fps_bits)
+{
+    AVRational rate = st->avg_frame_rate;
+    const AVRational zero = { 0, 1 };
+
+    if (!rate.num || !rate.den)
+        rate = av_inv_q(st->time_base);
+    if (!rate.num || !rate.den) {
+        av_log(s, AV_LOG_ERROR, "A valid frame rate is required for PDV 
output.\n");
+        return AVERROR(EINVAL);
+    }
+
+    if (av_cmp_q(rate, zero) <= 0) {
+        av_log(s, AV_LOG_ERROR, "Invalid frame rate for PDV output.\n");
+        return AVERROR(EINVAL);
+    }
+
+    *fps_bits = av_q2intfloat(rate);
+    return 0;
+}
+
+static int pdv_write_header(AVFormatContext *s)
+{
+    PDVMuxContext *pdv = s->priv_data;
+    AVStream *st;
+    int ret;
+
+    if (!(s->pb->seekable & AVIO_SEEKABLE_NORMAL)) {
+        av_log(s, AV_LOG_ERROR, "PDV muxer requires seekable output.\n");
+        return AVERROR(EINVAL);
+    }
+
+    if (s->nb_streams != 1) {
+        av_log(s, AV_LOG_ERROR, "PDV muxer supports exactly one stream.\n");
+        return AVERROR(EINVAL);
+    }
+
+    st = s->streams[0];
+
+    if (st->codecpar->width <= 0 || st->codecpar->height <= 0) {
+        av_log(s, AV_LOG_ERROR, "Invalid output dimensions.\n");
+        return AVERROR(EINVAL);
+    }
+    if (st->codecpar->width > UINT16_MAX || st->codecpar->height > UINT16_MAX) 
{
+        av_log(s, AV_LOG_ERROR, "Output dimensions exceed PDV limits.\n");
+        return AVERROR(EINVAL);
+    }
+
+    ret = pdv_get_fps(s, st, &pdv->fps_bits);
+    if (ret < 0)
+        return ret;
+
+    if (pdv->max_frames < 1 || pdv->max_frames > PDV_MAX_FRAMES) {
+        av_log(s, AV_LOG_ERROR,
+               "The -max_frames option must be set to a value in [1, %u].\n",
+               PDV_MAX_FRAMES);
+        return AVERROR(EINVAL);
+    }
+
+    pdv->entries = av_malloc_array(pdv->max_frames + 1, sizeof(*pdv->entries));
+    if (!pdv->entries)
+        return AVERROR(ENOMEM);
+
+    avio_write(s->pb, PDV_MAGIC, 16);
+    pdv->nb_frames_pos = avio_tell(s->pb);
+    avio_wl16(s->pb, 0);
+    avio_wl16(s->pb, 0);
+    avio_wl32(s->pb, pdv->fps_bits);
+    avio_wl16(s->pb, st->codecpar->width);
+    avio_wl16(s->pb, st->codecpar->height);
+
+    pdv->table_pos = avio_tell(s->pb);
+    ffio_fill(s->pb, 0, 4LL * (pdv->max_frames + 1));
+    pdv->payload_start = avio_tell(s->pb);
+
+    if (pdv->nb_frames_pos < 0 || pdv->table_pos < 0 || pdv->payload_start < 0)
+        return AVERROR(EIO);
+
+    return 0;
+}
+
+static int pdv_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    PDVMuxContext *pdv = s->priv_data;
+    int64_t offset = avio_tell(s->pb);
+    const uint32_t max_table_gap = 4U * pdv->max_frames;
+
+    if (offset < 0)
+        return AVERROR(EIO);
+    offset -= pdv->payload_start;
+    if (offset < 0)
+        return AVERROR(EIO);
+
+    if (pkt->size <= 0)
+        return AVERROR_INVALIDDATA;
+    if (pdv->nb_frames >= pdv->max_frames) {
+        av_log(s, AV_LOG_ERROR, "Too many frames for PDV output.\n");
+        return AVERROR(EINVAL);
+    }
+    if (offset > PDV_MAX_OFFSET - max_table_gap ||
+        pkt->size > PDV_MAX_OFFSET - max_table_gap - offset) {
+        av_log(s, AV_LOG_ERROR, "PDV payload exceeds container limits.\n");
+        return AVERROR(EINVAL);
+    }
+
+    pdv->entries[pdv->nb_frames] = ((uint32_t)offset << 2) |
+                                   (pkt->flags & AV_PKT_FLAG_KEY ? 1 : 2);
+    avio_write(s->pb, pkt->data, pkt->size);
+
+    pdv->nb_frames++;
+
+    return 0;
+}
+
+static int pdv_write_trailer(AVFormatContext *s)
+{
+    PDVMuxContext *pdv = s->priv_data;
+    int64_t payload_size = avio_tell(s->pb);
+    const uint32_t table_gap = 4U * (pdv->max_frames - pdv->nb_frames);
+    int ret;
+
+    if (payload_size < 0)
+        return AVERROR(EIO);
+    payload_size -= pdv->payload_start;
+    if (payload_size < 0 || payload_size > PDV_MAX_OFFSET - table_gap)
+        return AVERROR(EINVAL);
+
+    pdv->entries[pdv->nb_frames] = (uint32_t)payload_size << 2;
+
+    if ((ret = avio_seek(s->pb, pdv->nb_frames_pos, SEEK_SET)) < 0)
+        return ret;
+    avio_wl16(s->pb, pdv->nb_frames);
+
+    if ((ret = avio_seek(s->pb, pdv->table_pos, SEEK_SET)) < 0)
+        return ret;
+    for (int i = 0; i <= pdv->nb_frames; i++) {
+        const uint32_t frame_off = (pdv->entries[i] >> 2) + table_gap;
+
+        if (frame_off > PDV_MAX_OFFSET)
+            return AVERROR(EINVAL);
+        avio_wl32(s->pb, frame_off << 2 | (pdv->entries[i] & 3));
+    }
+
+    if ((ret = avio_seek(s->pb, pdv->payload_start + payload_size, SEEK_SET)) 
< 0)
+        return ret;
+
+    return 0;
+}
+
+#define OFFSET(x) offsetof(PDVMuxContext, x)
+#define ENC AV_OPT_FLAG_ENCODING_PARAM
+static const AVOption options[] = {
+    { "max_frames", "maximum number of frames reserved in table (mandatory)",
+      OFFSET(max_frames), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, PDV_MAX_FRAMES, 
ENC },
+    { NULL },
+};
+
+static const AVClass pdv_muxer_class = {
+    .class_name = "PDV muxer",
+    .item_name  = av_default_item_name,
+    .option     = options,
+    .version    = LIBAVUTIL_VERSION_INT,
+    .category   = AV_CLASS_CATEGORY_MUXER,
+};
+
+const FFOutputFormat ff_pdv_muxer = {
+    .p.name           = "pdv",
+    .p.long_name      = NULL_IF_CONFIG_SMALL("PlayDate Video"),
+    .p.extensions     = "pdv",
+    .p.priv_class     = &pdv_muxer_class,
+    .p.audio_codec    = AV_CODEC_ID_NONE,
+    .p.video_codec    = AV_CODEC_ID_PDV,
+    .p.subtitle_codec = AV_CODEC_ID_NONE,
+    .priv_data_size   = sizeof(PDVMuxContext),
+    .p.flags          = AVFMT_NOTIMESTAMPS,
+    .flags_internal   = FF_OFMT_FLAG_MAX_ONE_OF_EACH |
+                        FF_OFMT_FLAG_ONLY_DEFAULT_CODECS,
+    .write_header     = pdv_write_header,
+    .write_packet     = pdv_write_packet,
+    .write_trailer    = pdv_write_trailer,
+    .deinit           = pdv_deinit,
+};

_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to