Skip to content

Commit 4ebeb4a

Browse files
committed
Prevent codec reconfiguration after opening (fixes #2232)
1 parent 3ae2d70 commit 4ebeb4a

7 files changed

Lines changed: 47 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Features:
4040

4141
Fixes:
4242

43+
- Prevent crashes and corrupted output when structural codec properties are changed after an output stream has been opened by :gh-user:`WyattBlue`, reported by :gh-user:`oakaigh` (:issue:`2232`).
4344
- Fix a crash when using a stream that has no ``CodecContext`` (a demuxed stream with no available decoder, such as one from a truncated file, or a stream created by ``add_mux_stream``); decoding now raises ``DecoderNotFoundError``, encoding now raises ``EncoderNotFoundError``, and ``BitStreamFilterContext`` accepts such a stream as ``out_stream`` by :gh-user:`WyattBlue`, reported by :gh-user:`justinrmiller` (:issue:`2344`).
4445

4546
v18.0.0

av/audio/codeccontext.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def sample_rate(self):
6262

6363
@sample_rate.setter
6464
def sample_rate(self, value: cython.int):
65+
self._assert_not_open("sample_rate")
6566
self.ptr.sample_rate = value
6667

6768
@property
@@ -88,6 +89,7 @@ def layout(self):
8889

8990
@layout.setter
9091
def layout(self, value):
92+
self._assert_not_open("layout")
9193
layout: AudioLayout = AudioLayout(value)
9294
self.ptr.ch_layout = layout.layout
9395

@@ -102,5 +104,6 @@ def format(self):
102104

103105
@format.setter
104106
def format(self, value):
107+
self._assert_not_open("format")
105108
format: AudioFormat = AudioFormat(value)
106109
self.ptr.sample_fmt = format.sample_fmt

av/codec/context.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ cdef class CodecContext:
1717

1818
cdef lib.AVCodecParserContext *parser
1919
cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel)
20+
cdef _assert_not_open(self, name)
2021

2122
# Public API.
2223
cdef readonly bint is_open

av/codec/context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ def _init(
287287
self.ptr.thread_count = 0 # use as many threads as there are CPUs.
288288
self.ptr.thread_type = 0x02 # thread within a frame. Does not change the API.
289289

290+
@cython.cfunc
291+
def _assert_not_open(self, name):
292+
if self.is_open:
293+
raise RuntimeError(f"Cannot change {name} after codec is open.")
294+
290295
@property
291296
def flags(self):
292297
"""

av/container/output.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ def add_stream(
103103
:param \\**kwargs: Set attributes for the stream.
104104
:rtype: The new :class:`~av.stream.Stream`.
105105
106+
.. warning::
107+
108+
Configure every output stream before muxing the first packet. Writing
109+
the file header opens all stream codec contexts, after which structural
110+
properties such as format, dimensions, layout, and rate cannot change.
111+
106112
"""
107113

108114
codec_obj: Codec = Codec(codec_name, "w")

av/video/codeccontext.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def format(self):
193193

194194
@format.setter
195195
def format(self, format: VideoFormat):
196+
self._assert_not_open("format")
196197
self.ptr.pix_fmt = format.pix_fmt
197198
self.ptr.width = format.width
198199
self.ptr.height = format.height
@@ -205,6 +206,7 @@ def width(self):
205206

206207
@width.setter
207208
def width(self, value: cython.uint):
209+
self._assert_not_open("width")
208210
self.ptr.width = value
209211

210212
@property
@@ -215,6 +217,7 @@ def height(self):
215217

216218
@height.setter
217219
def height(self, value: cython.uint):
220+
self._assert_not_open("height")
218221
self.ptr.height = value
219222

220223
@property
@@ -251,6 +254,7 @@ def pix_fmt(self):
251254

252255
@pix_fmt.setter
253256
def pix_fmt(self, value):
257+
self._assert_not_open("pix_fmt")
254258
self.ptr.pix_fmt = get_pix_fmt(value)
255259

256260
@property
@@ -277,6 +281,7 @@ def sw_format(self):
277281

278282
@sw_format.setter
279283
def sw_format(self, value):
284+
self._assert_not_open("sw_format")
280285
self.ptr.sw_pix_fmt = get_pix_fmt(value)
281286

282287
@property
@@ -290,6 +295,7 @@ def framerate(self):
290295

291296
@framerate.setter
292297
def framerate(self, value):
298+
self._assert_not_open("framerate")
293299
to_avrational(value, cython.address(self.ptr.framerate))
294300

295301
@property

tests/test_encode.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import math
55
import os
66
from fractions import Fraction
7+
from typing import cast
78

89
import numpy as np
910
import pytest
@@ -268,6 +269,30 @@ def test_subtitle_muxing(self) -> None:
268269

269270

270271
class TestEncodeStreamSemantics(TestCase):
272+
def test_reconfigure_stream_after_mux(self) -> None:
273+
output_bytes = io.BytesIO()
274+
with av.open(output_bytes, "w", format="mp4") as output:
275+
first = cast(VideoStream, output.add_stream("ffv1", rate=30))
276+
second = cast(VideoStream, output.add_stream("ffv1", rate=30))
277+
278+
first.format = av.VideoFormat("bgr0", width=16, height=16)
279+
frame = VideoFrame(16, 16, "bgr0")
280+
frame.pts = 0
281+
frame.time_base = Fraction(1, 30)
282+
output.mux(first.encode(frame))
283+
284+
# Muxing the first packet writes the header and opens every stream.
285+
# Changing the second encoder now used to corrupt FFV1 state and crash.
286+
assert second.codec_context.is_open
287+
with pytest.raises(RuntimeError, match="Cannot change format"):
288+
second.format = av.VideoFormat("bgr0", width=16, height=16)
289+
with pytest.raises(RuntimeError, match="Cannot change width"):
290+
second.width = 16
291+
with pytest.raises(RuntimeError, match="Cannot change height"):
292+
second.height = 16
293+
with pytest.raises(RuntimeError, match="Cannot change pix_fmt"):
294+
second.pix_fmt = "bgr0"
295+
271296
def test_stream_index(self) -> None:
272297
with av.open(self.sandboxed("output.mov"), "w") as output:
273298
vstream = output.add_stream("mpeg4", 24)

0 commit comments

Comments
 (0)