From e0ec54ba64ca7c6ec1f206a17722e2dcd13ace8a Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 28 Apr 2026 17:30:24 -0600 Subject: [PATCH 1/2] Copy time_base in add_stream_from_template --- av/container/output.py | 4 ++++ tests/test_remux.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/av/container/output.py b/av/container/output.py index d0c51cc6b..4e868d60c 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -262,6 +262,10 @@ def add_stream_from_template( # Reset the codec tag assuming we are remuxing. ctx.codec_tag = 0 + # Copy the template's stream time_base + stream.time_base = template.ptr.time_base + ctx.time_base = template.ptr.time_base + # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER diff --git a/tests/test_remux.py b/tests/test_remux.py index 37a571883..155a74ccd 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -3,6 +3,8 @@ import av import av.datasets +from .common import fate_suite + def test_video_remux() -> None: input_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") @@ -79,3 +81,24 @@ def test_add_mux_stream_no_codec_context() -> None: # repr should not crash assert "video/" in repr(video_stream) assert "audio/" in repr(audio_stream) + + +def test_add_stream_from_template_copies_time_base() -> None: + """add_stream_from_template must propagate the source stream's time_base. + + AVCodecParameters does not carry time_base, so without an explicit copy + the output stream's time_base stays as None + """ + video_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + with av.open(video_path) as input_, av.open(io.BytesIO(), "w", format="mp4") as output: + in_video = input_.streams.video[0] + out_video = output.add_stream_from_template(in_video) + assert out_video.time_base is not None + assert out_video.time_base == in_video.time_base + + audio_path = fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") + with av.open(audio_path) as input_, av.open(io.BytesIO(), "w", format="wav") as output: + in_audio = input_.streams.audio[0] + out_audio = output.add_stream_from_template(in_audio) + assert out_audio.time_base is not None + assert out_audio.time_base == in_audio.time_base From 3c80b90e86d4d6d48d73005bf3cc691bba61adb2 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 28 Apr 2026 17:43:40 -0600 Subject: [PATCH 2/2] Make ruff happy --- tests/test_remux.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_remux.py b/tests/test_remux.py index 155a74ccd..9c4aecfe8 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -90,14 +90,20 @@ def test_add_stream_from_template_copies_time_base() -> None: the output stream's time_base stays as None """ video_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") - with av.open(video_path) as input_, av.open(io.BytesIO(), "w", format="mp4") as output: + with ( + av.open(video_path) as input_, + av.open(io.BytesIO(), "w", format="mp4") as output, + ): in_video = input_.streams.video[0] out_video = output.add_stream_from_template(in_video) assert out_video.time_base is not None assert out_video.time_base == in_video.time_base audio_path = fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") - with av.open(audio_path) as input_, av.open(io.BytesIO(), "w", format="wav") as output: + with ( + av.open(audio_path) as input_, + av.open(io.BytesIO(), "w", format="wav") as output, + ): in_audio = input_.streams.audio[0] out_audio = output.add_stream_from_template(in_audio) assert out_audio.time_base is not None