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: 0 additions & 1 deletion docs/api/codec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ Descriptors
.. autoattribute:: Codec.is_decoder
.. autoattribute:: Codec.is_encoder

.. autoattribute:: Codec.descriptor
.. autoattribute:: Codec.name
.. autoattribute:: Codec.long_name
.. autoattribute:: Codec.type
Expand Down
1 change: 0 additions & 1 deletion docs/api/container.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ Formats
.. autoattribute:: ContainerFormat.name
.. autoattribute:: ContainerFormat.long_name

.. autoattribute:: ContainerFormat.options
.. autoattribute:: ContainerFormat.input
.. autoattribute:: ContainerFormat.output
.. autoattribute:: ContainerFormat.is_input
Expand Down
7 changes: 5 additions & 2 deletions examples/basics/remux.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
for packet in input_.demux(in_stream):
print(packet)

# We need to skip the "flushing" packets that `demux` generates.
if packet.dts is None:
# We need to skip the empty "flushing" packet that `demux` generates at the
# end. Don't test `packet.dts is None` here: a valid keyframe can legitimately
# have no DTS (e.g. the leading reordered packets of a B-frame stream in MKV),
# and skipping it would drop the keyframe and corrupt the output.
if packet.size == 0:
continue

# We need to assign the packet to the new stream.
Expand Down
2 changes: 1 addition & 1 deletion examples/subtitles/remux.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
out_stream = output.add_stream_from_template(in_stream)

for packet in input_.demux(in_stream):
if packet.dts is None:
if packet.size == 0:
continue
packet.stream = out_stream
output.mux(packet)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def test_subtitle_muxing(self) -> None:
out_stream = output.add_stream_from_template(in_stream)

for packet in input_.demux(in_stream):
if packet.dts is None:
if packet.size == 0:
continue
packet.stream = out_stream
output.mux(packet)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def test_skip_samples_remux(self) -> None:
with av.open(output_path, "w") as out:
out_stream = out.add_stream_from_template(audio_stream)
for pkt in inp.demux(audio_stream):
if pkt.dts is None:
if pkt.size == 0:
continue
if pkt.has_sidedata("skip_samples"):
sdata = pkt.get_sidedata("skip_samples")
Expand Down
83 changes: 81 additions & 2 deletions tests/test_remux.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import io

import numpy as np
import pytest

import av
import av.datasets

Expand All @@ -16,7 +19,7 @@ def test_video_remux() -> None:
out_stream = output.add_stream_from_template(in_stream)

for packet in input_.demux(in_stream):
if packet.dts is None:
if packet.size == 0: # skip the flushing packet, not keyframes with no DTS
continue

packet.stream = out_stream
Expand Down Expand Up @@ -58,7 +61,7 @@ def test_add_mux_stream_video() -> None:
out_stream.time_base = in_stream.time_base

for packet in input_.demux(in_stream):
if packet.dts is None:
if packet.size == 0:
continue
packet.stream = out_stream
output.mux(packet)
Expand Down Expand Up @@ -109,3 +112,79 @@ def test_add_stream_from_template_copies_time_base() -> None:
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


def _make_b_frame_mkv(n: int = 48) -> io.BytesIO:
"""Encode `n` frames with B-frames into an in-memory MKV.

Matroska stores only presentation timestamps, so when this is demuxed again
libavformat cannot reconstruct a DTS for the leading reordered packets and
leaves it as None -- including on the very first packet, which is the
keyframe. That is exactly the layout that broke the remux example in #1917.
"""
buf = io.BytesIO()
with av.open(buf, "w", format="matroska") as out:
stream = out.add_stream("h264", rate=30)
stream.width, stream.height, stream.pix_fmt = 160, 120, "yuv420p"
stream.options = {"bf": "3", "g": "30"}
for i in range(n):
img = np.full((120, 160, 3), (i * 5) % 256, dtype="uint8")
frame = av.VideoFrame.from_ndarray(img, format="rgb24")
for packet in stream.encode(frame):
out.mux(packet)
for packet in stream.encode(None):
out.mux(packet)
buf.seek(0)
return buf


def _decoded_frame_count(buf: io.BytesIO) -> int:
buf.seek(0)
with av.open(buf, "r") as container:
return sum(1 for _ in container.decode(video=0))


def test_remux_keeps_keyframe_with_none_dts() -> None:
"""Regression test for #1917.

A keyframe can legitimately demux with ``dts is None`` (B-frame stream in a
PTS-only container like MKV). The remux loop must skip only the empty
flushing packet (``size == 0``), not every ``dts is None`` packet, otherwise
the keyframe is dropped and the output is undecodable.
"""
if av.codec.Codec("h264", "w").name != "libx264":
pytest.skip("requires libx264")

source = _make_b_frame_mkv()
expected_frames = _decoded_frame_count(source)
assert expected_frames > 0

# Precondition: the first packet really is a keyframe without a DTS, which is
# what the old `dts is None` filter would have wrongly discarded.
source.seek(0)
with av.open(source, "r") as input_:
first = next(p for p in input_.demux(input_.streams.video[0]) if p.size)
assert first.is_keyframe
assert first.dts is None

source.seek(0)
output = io.BytesIO()
with (
av.open(source, "r") as input_,
av.open(output, "w", format="matroska") as out,
):
in_video = input_.streams.video[0]
out_video = out.add_stream_from_template(in_video)
for packet in input_.demux(in_video):
if packet.size == 0: # the flushing packet, not a keyframe with no DTS
continue
packet.stream = out_video
out.mux(packet)

# The keyframe survived: every frame still decodes and the first packet of
# the remuxed stream is a keyframe.
assert _decoded_frame_count(output) == expected_frames
output.seek(0)
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
4 changes: 2 additions & 2 deletions tests/test_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def test_attachment_stream(self) -> None:
out_v = out1.add_stream_from_template(in_v)

for packet in input_.demux(in_v):
if packet.dts is None:
if packet.size == 0:
continue
packet.stream = out_v
out1.mux(packet)
Expand All @@ -382,7 +382,7 @@ def test_attachment_stream(self) -> None:
stream_map[s.index] = oc.add_stream_from_template(s)

for packet in ic.demux(ic.streams.video):
if packet.dts is None:
if packet.size == 0:
continue
updated_stream = stream_map.get(packet.stream.index)
if isinstance(updated_stream, av.video.stream.VideoStream):
Expand Down
Loading