Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion av/codec/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions av/codec/context.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: ...
Expand Down
50 changes: 50 additions & 0 deletions tests/test_remux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading