From c96d9b385773b6ddce435fbdeb9919004a4bdb5d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 7 Nov 2026 20:14:42 -0500 Subject: [PATCH] Make CodecContext.initial_padding writable add_stream_from_template copies the source's AAC priming, so a segment cut from the middle of a stream and muxed to Matroska drops its first 1024 samples. Let callers clear it on the output stream. Fixes #2509 --- CHANGELOG.rst | 1 + av/codec/context.py | 8 ++++++- av/codec/context.pyi | 3 +-- tests/test_remux.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4a481de97..e9737aad9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,7 @@ Features: - Add ``InputContainer.programs``, exposing FFmpeg programs such as MPEG-TS channels, with their streams, metadata, and a settable ``discard``. - Add ``Frame.decode_error_flags`` as a ``DecodeError`` flag and ``VideoFrame.repeat_pict``. - Add ``Stream.cctx`` as the name for a stream's codec context; ``Stream.codec_context`` remains as an alias. +- ``CodecContext.initial_padding`` is now writable, so a remuxed segment can clear the priming copied by ``add_stream_from_template()`` (:issue:`2509`). - Deprecate ``AudioStream.format``, ``AudioStream.layout``, ``VideoStream.format``, ``VideoStream.thread_count``, and ``VideoStream.thread_type``, which pass through to the codec context; use ``stream.cctx.format``, ``stream.cctx.layout``, ``stream.cctx.thread_count``, and ``stream.cctx.thread_type`` instead. Passing these to ``add_stream()`` is not deprecated. Fixes: diff --git a/av/codec/context.py b/av/codec/context.py index e3a3ec86e..121e516b6 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -1145,12 +1145,18 @@ def initial_padding(self): for gapless playback. Set by libavcodec when encoding, and taken from the stream parameters - when decoding. + when decoding. When remuxing, set it on the output stream's context to + change what the container records, e.g. ``0`` for a segment that does + not start with the priming samples. Wraps :ffmpeg:`AVCodecContext.initial_padding`. """ return self.ptr.initial_padding + @initial_padding.setter + def initial_padding(self, value: cython.int): + self.ptr.initial_padding = value + @property def trailing_padding(self): """Audio only. Padding samples appended by the encoder, which must be diff --git a/av/codec/context.pyi b/av/codec/context.pyi index b10ae088d..d72249690 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -148,6 +148,7 @@ class CodecContext: rc_buffer_size: int compression_level: int bits_per_raw_sample: int + initial_padding: int trailing_padding: int stats_in: str | None thread_count: int @@ -187,8 +188,6 @@ class CodecContext: @property def active_thread_type(self) -> ThreadType: ... @property - def initial_padding(self) -> int: ... - @property def seek_preroll(self) -> int: ... @property def stats_out(self) -> str | None: ... diff --git a/tests/test_remux.py b/tests/test_remux.py index 65c3c8a56..8aa5d7675 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -231,3 +231,53 @@ def test_remux_keeps_keyframe_with_none_dts() -> None: with av.open(output, "r") as container: first_out = next(p for p in container.demux(video=0) if p.size) assert first_out.is_keyframe + + +def test_remux_clear_initial_padding() -> None: + """A segment cut from the middle of an AAC stream has no priming samples. + + Matroska stores the template's initial_padding as CodecDelay, so unless it + is cleared the decoder drops the segment's first 1024 real samples. + """ + source = io.BytesIO() + with av.open(source, "w", format="mp4") as out: + stream = out.add_stream("aac", rate=48000, layout="mono") + for i in range(20): + frame = av.AudioFrame.from_ndarray( + np.zeros((1, 1024), dtype="f4"), format="fltp", layout="mono" + ) + frame.sample_rate = 48000 + frame.pts = i * 1024 + for packet in stream.encode(frame): + out.mux(packet) + for packet in stream.encode(): + out.mux(packet) + + source.seek(0) + segment = io.BytesIO() + with ( + av.open(source, "r") as input_, + av.open(segment, "w", format="matroska") as out, + ): + in_audio = input_.streams.audio[0] + assert in_audio.cctx.initial_padding == 1024 + + out_audio = out.add_stream_from_template(in_audio) + assert out_audio.cctx.initial_padding == 1024 + out_audio.cctx.initial_padding = 0 + + packets = [p for p in input_.demux(in_audio) if p.dts is not None][10:] + start = packets[0].pts + assert start is not None + for packet in packets: + assert packet.pts is not None and packet.dts is not None + packet.pts -= start + packet.dts -= start + packet.stream = out_audio + out.mux(packet) + + segment.seek(0) + with av.open(segment, "r") as container: + audio = container.streams.audio[0] + assert audio.cctx.initial_padding == 0 + assert sum(f.samples for f in container.decode(audio)) == len(packets) * 1024