Skip to content

Commit d91e30e

Browse files
committed
Extract codec extradata in add_mux_stream for Matroska
Muxing packets into Matroska failed in avformat_write_header because the muxer needs CodecPrivate (the codec extradata) before the first packet. For codec-less mux streams whose format embeds a global header, buffer packets and run the extract_extradata bitstream filter to recover the parameter sets from the bitstream, then set them on codecpar before the header is written. Streams give up after their first packet, so already length-prefixed packets fall back to the muxer's default behavior with no regression. closes #2198
1 parent 107817d commit d91e30e

7 files changed

Lines changed: 174 additions & 3 deletions

File tree

‎CHANGELOG.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Features:
4242

4343
Fixes:
4444

45-
- Nothing (yet)
45+
- Fix ``add_mux_stream`` producing unwritable Matroska files by extracting codec extradata from the bitstream before the header is written by :gh-user:`WyattBlue` (:issue:`2198`).
4646

4747
v17.1.0
4848
-------

‎av/container/core.pxd‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ cdef class Container:
3737
cdef readonly dict metadata
3838

3939
# Private API.
40-
cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done
40+
cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done, extradata_planned
4141
cdef _assert_open(self)
4242
cdef int err_check(self, int value) except -1
4343

‎av/container/output.pxd‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
cimport libav as lib
22

33
from av.container.core cimport Container
4+
from av.packet cimport Packet
45
from av.stream cimport Stream
56

67

78
cdef class OutputContainer(Container):
89
cdef lib.AVPacket *packet_ptr
10+
cdef dict _extradata_bsfs
11+
cdef list _buffered_packets
12+
cdef _mux_one(self, Packet packet)
13+
cdef _buffer_for_extradata(self, Packet packet)
14+
cdef _try_extract_extradata(self, Packet packet)
915
cpdef start_encoding(self)

‎av/container/output.py‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import cython
44
from cython.cimports import libav as lib
5+
from cython.cimports.av.bitstream import BitStreamFilterContext
56
from cython.cimports.av.codec.codec import Codec
67
from cython.cimports.av.codec.context import CodecContext, wrap_codec_context
78
from cython.cimports.av.container.streams import StreamContainer
@@ -10,10 +11,39 @@
1011
from cython.cimports.av.packet import Packet
1112
from cython.cimports.av.stream import Stream, wrap_stream
1213
from cython.cimports.av.utils import dict_to_avdict, to_avrational
14+
from cython.cimports.libc.stdint import uint8_t
15+
from cython.cimports.libc.string import memcpy, memset
16+
17+
18+
@cython.cfunc
19+
def _set_codecpar_extradata(
20+
stream: cython.pointer[lib.AVStream],
21+
data: cython.pointer[uint8_t],
22+
size: cython.int,
23+
):
24+
buf: cython.p_uchar = cython.cast(
25+
cython.p_uchar, lib.av_malloc(size + lib.AV_INPUT_BUFFER_PADDING_SIZE)
26+
)
27+
if buf == cython.NULL:
28+
raise MemoryError("Could not allocate extradata")
29+
30+
memcpy(buf, data, size)
31+
memset(buf + size, 0, lib.AV_INPUT_BUFFER_PADDING_SIZE)
32+
33+
lib.av_freep(cython.address(stream.codecpar.extradata))
34+
stream.codecpar.extradata = buf
35+
stream.codecpar.extradata_size = size
1336

1437

1538
@cython.cfunc
1639
def close_output(self: OutputContainer):
40+
if self.packet_ptr != cython.NULL and self._buffered_packets:
41+
buffered: list = self._buffered_packets
42+
self._buffered_packets = []
43+
packet: Packet
44+
for packet in buffered:
45+
self._mux_one(packet)
46+
1747
self.streams = StreamContainer()
1848
if self._myflag & 12 == 4: # enum.started and not enum.done
1949
# If the underlying Python IO file was already closed (e.g. during GC
@@ -38,6 +68,8 @@ class OutputContainer(Container):
3868
def __cinit__(self, *args, **kwargs):
3969
self.streams = StreamContainer()
4070
self.metadata = {}
71+
self._extradata_bsfs = {}
72+
self._buffered_packets = []
4173
with cython.nogil:
4274
self.packet_ptr = lib.av_packet_alloc()
4375

@@ -558,6 +590,13 @@ def mux(self, packets):
558590
self.mux_one(packet)
559591

560592
def mux_one(self, packet: Packet):
593+
if not (self._myflag & 4) and self._buffer_for_extradata(packet):
594+
return
595+
596+
self._mux_one(packet)
597+
598+
@cython.cfunc
599+
def _mux_one(self, packet: Packet):
561600
self.start_encoding()
562601

563602
# Assert the packet is in stream time.
@@ -577,3 +616,86 @@ def mux_one(self, packet: Packet):
577616
with cython.nogil:
578617
ret: cython.int = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr)
579618
self.err_check(ret)
619+
620+
@cython.cfunc
621+
def _buffer_for_extradata(self, packet: Packet):
622+
"""Buffer ``packet`` until extradata is known for all mux streams that
623+
need it. Returns True if the packet was buffered (caller should stop)."""
624+
if not (self._myflag & 16): # extradata_planned
625+
self._myflag |= 16
626+
if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER:
627+
stream: Stream
628+
for stream in self.streams:
629+
if (
630+
stream.codec_context is not None
631+
or stream.ptr.codecpar.extradata != cython.NULL
632+
):
633+
continue
634+
try:
635+
bsf = BitStreamFilterContext(
636+
"extract_extradata", in_stream=stream
637+
)
638+
except Exception:
639+
continue # Codec does not support extradata extraction.
640+
self._extradata_bsfs[stream.ptr.index] = bsf
641+
642+
if not self._extradata_bsfs:
643+
return False # Nothing to wait for; mux normally.
644+
645+
self._try_extract_extradata(packet)
646+
self._buffered_packets.append(packet)
647+
if self._extradata_bsfs:
648+
return True # Still waiting on some stream's extradata.
649+
650+
# All extradata is resolved: write the header and flush buffered packets.
651+
buffered: list = self._buffered_packets
652+
self._buffered_packets = []
653+
buffered_packet: Packet
654+
for buffered_packet in buffered:
655+
self._mux_one(buffered_packet)
656+
return True
657+
658+
@cython.cfunc
659+
def _try_extract_extradata(self, packet: Packet):
660+
idx: cython.int = packet.ptr.stream_index
661+
if idx not in self._extradata_bsfs:
662+
return
663+
664+
bsf_wrapper: BitStreamFilterContext = self._extradata_bsfs[idx]
665+
bsf: cython.pointer[lib.AVBSFContext] = bsf_wrapper.ptr
666+
667+
tmp: cython.pointer[lib.AVPacket] = lib.av_packet_alloc()
668+
if tmp == cython.NULL:
669+
raise MemoryError("Could not allocate packet")
670+
671+
size: cython.size_t = 0
672+
sd: cython.pointer[uint8_t]
673+
try:
674+
# Clone the packet so the filter does not consume the caller's data.
675+
if lib.av_packet_ref(tmp, packet.ptr) < 0:
676+
return
677+
678+
if lib.av_bsf_send_packet(bsf, tmp) < 0:
679+
lib.av_packet_unref(tmp) # send failed; we still own the ref
680+
return
681+
682+
# The filter rejects packets that are already length-prefixed rather
683+
# than annex-b, returning an error here; treat that and EOF/EAGAIN
684+
# alike as "no in-band extradata".
685+
while lib.av_bsf_receive_packet(bsf, tmp) == 0:
686+
sd = lib.av_packet_get_side_data(
687+
tmp, lib.AV_PKT_DATA_NEW_EXTRADATA, cython.address(size)
688+
)
689+
if sd != cython.NULL and size > 0:
690+
_set_codecpar_extradata(
691+
self.ptr.streams[idx], sd, cython.cast(cython.int, size)
692+
)
693+
lib.av_packet_unref(tmp)
694+
break
695+
lib.av_packet_unref(tmp)
696+
finally:
697+
lib.av_packet_free(cython.address(tmp))
698+
# A stream's first packet is the only reliable place to find in-band
699+
# parameter sets, so stop waiting on this stream regardless of the
700+
# result, falling back to the muxer's default behavior.
701+
del self._extradata_bsfs[idx]

‎include/avcodec.pxd‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ cdef extern from "libavcodec/avcodec.h" nogil:
298298
cdef char* avcodec_get_name(AVCodecID id)
299299
cdef int avcodec_open2(AVCodecContext *ctx, const AVCodec *codec, AVDictionary **options)
300300
cdef enum AVPacketSideDataType:
301+
AV_PKT_DATA_NEW_EXTRADATA
301302
AV_PKT_DATA_DISPLAYMATRIX
302303
cdef struct AVPacketSideData:
303304
uint8_t *data

‎scripts/build-deps‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ echo ./configure
6767
--enable-debug=3 \
6868
--enable-libx264 \
6969
--disable-bsfs \
70-
--enable-bsf=chomp,h264_mp4toannexb,setts \
70+
--enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \
7171
--enable-sse \
7272
--enable-avx \
7373
--enable-avx2 \

‎tests/test_remux.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import io
2+
from fractions import Fraction
23

34
import numpy as np
45
import pytest
@@ -87,6 +88,47 @@ def test_add_mux_stream_no_codec_context() -> None:
8788
assert "audio/<nocodec>" in repr(audio_stream)
8889

8990

91+
def test_add_mux_stream_matroska_extradata() -> None:
92+
"""Regression test for #2198.
93+
94+
Muxing pre-encoded H.264 packets into Matroska used to fail in
95+
``avformat_write_header`` because the muxer needs ``CodecPrivate`` (the codec
96+
extradata) before the first packet is written. When the packets are annex-b
97+
with in-band parameter sets but the stream has no extradata,
98+
``add_mux_stream`` extracts it from the bitstream so the file is written and
99+
stays decodable.
100+
"""
101+
if av.codec.Codec("h264", "w").name != "libx264":
102+
pytest.skip("requires libx264")
103+
104+
# Encode without a global header, so the packets carry annex-b parameter
105+
# sets in-band and the codec context exposes no extradata to copy.
106+
cc = av.CodecContext.create("libx264", "w")
107+
cc.width, cc.height, cc.pix_fmt = 320, 240, "yuv420p"
108+
cc.time_base = Fraction(1, 24)
109+
cc.framerate = Fraction(24, 1)
110+
111+
packets = []
112+
for i in range(24):
113+
frame = av.VideoFrame.from_ndarray(
114+
np.zeros((240, 320, 3), dtype="uint8"), format="rgb24"
115+
)
116+
frame.pts = i
117+
frame.time_base = Fraction(1, 24)
118+
packets.extend(cc.encode(frame))
119+
packets.extend(cc.encode(None))
120+
assert cc.extradata is None # nothing to copy onto the stream
121+
122+
buf = io.BytesIO()
123+
with av.open(buf, "w", format="matroska") as output:
124+
out_stream = output.add_mux_stream("libx264", rate=24, width=320, height=240)
125+
for packet in packets:
126+
packet.stream = out_stream
127+
output.mux(packet)
128+
129+
assert _decoded_frame_count(buf) == 24
130+
131+
90132
def test_add_stream_from_template_copies_time_base() -> None:
91133
"""add_stream_from_template must propagate the source stream's time_base.
92134

0 commit comments

Comments
 (0)