Skip to content

Commit 08d2cc1

Browse files
BUG: Upgrade pyav plugin to support av v14 (#1112)
Note: av v14 no longer supports the "libx264" codec. Use "h264" instead :) --------- Co-authored-by: Sebastian Wallkötter <sebastian@wallkoetter.net>
1 parent 5487474 commit 08d2cc1

File tree

3 files changed

+68
-26
lines changed

3 files changed

+68
-26
lines changed

.github/workflows/pypy_tests.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@ on:
55

66
jobs:
77
pypy:
8-
name: "${{ matrix.os }} / ${{ matrix.pyversion }}"
8+
name: "${{ matrix.os }} / pypy-3.10"
99
runs-on: ${{ matrix.os }}
1010
strategy:
1111
fail-fast: true
1212
matrix:
13-
os: ["ubuntu-latest", "macos-12"]
14-
pyversion: ["pypy-3.9", "pypy-3.10"]
13+
os: ["ubuntu-latest", "macos-latest"]
1514
steps:
1615
- uses: actions/checkout@v4
1716
- name: Set up pypy
1817
uses: actions/setup-python@v5
1918
with:
20-
python-version: "${{ matrix.pyversion }}"
19+
python-version: "pypy-3.10"
2120
- name: MacOS Numpy Fix
2221
if: runner.os == 'macOS'
2322
run: |

imageio/plugins/pyav.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,12 @@
177177

178178
from fractions import Fraction
179179
from math import ceil
180-
from typing import Any, Dict, List, Optional, Tuple, Union, Generator
180+
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
181181

182182
import av
183183
import av.filter
184184
import numpy as np
185+
from av.codec.context import Flags
185186
from numpy.lib.stride_tricks import as_strided
186187

187188
from ..core import Request
@@ -258,6 +259,39 @@ def _get_frame_shape(frame: av.VideoFrame) -> Tuple[int, ...]:
258259
return tuple(shape)
259260

260261

262+
def _get_frame_type(picture_type: int) -> str:
263+
"""Return a human-readable name for provided picture type
264+
265+
Parameters
266+
----------
267+
picture_type : int
268+
The picture type extracted from Frame.pict_type
269+
270+
Returns
271+
-------
272+
picture_name : str
273+
A human readable name of the picture type
274+
275+
"""
276+
277+
if not isinstance(picture_type, int):
278+
# old pyAV versions send an enum, not an int
279+
return picture_type.name
280+
281+
picture_types = [
282+
"NONE",
283+
"I",
284+
"P",
285+
"B",
286+
"S",
287+
"SI",
288+
"SP",
289+
"BI",
290+
]
291+
292+
return picture_types[picture_type]
293+
294+
261295
class PyAVPlugin(PluginV3):
262296
"""Support for pyAV as backend.
263297
@@ -308,7 +342,7 @@ def __init__(self, request: Request, *, container: str = None, **kwargs) -> None
308342
self._container = av.open(request.get_file(), **kwargs)
309343
self._video_stream = self._container.streams.video[0]
310344
self._decoder = self._container.decode(video=0)
311-
except av.AVError:
345+
except av.FFmpegError:
312346
if isinstance(request.raw_uri, bytes):
313347
msg = "PyAV does not support these `<bytes>`"
314348
else:
@@ -458,13 +492,16 @@ def read(
458492

459493
# reset stream container, because threading model can't change after
460494
# first access
461-
self._video_stream.close()
462495
self._video_stream = self._container.streams.video[0]
463496

464497
return frames
465498

466-
if thread_type is not None and thread_type != self._video_stream.thread_type:
499+
if thread_type is not None and not (
500+
self._video_stream.thread_type == thread_type
501+
or self._video_stream.thread_type.name == thread_type
502+
):
467503
self._video_stream.thread_type = thread_type
504+
468505
if (
469506
thread_count != 0
470507
and thread_count != self._video_stream.codec_context.thread_count
@@ -474,7 +511,10 @@ def read(
474511
self._video_stream.codec_context.thread_count = thread_count
475512

476513
if constant_framerate is None:
477-
constant_framerate = not self._container.format.variable_fps
514+
# "variable_fps" is now a flag (handle got removed). Full list at
515+
# https://pyav.org/docs/stable/api/container.html#module-av.format
516+
variable_fps = bool(self._container.format.flags & 0x400)
517+
constant_framerate = not variable_fps
478518

479519
# note: cheap for contigous incremental reads
480520
self._seek(index, constant_framerate=constant_framerate)
@@ -750,7 +790,10 @@ def metadata(
750790
return metadata
751791

752792
if constant_framerate is None:
753-
constant_framerate = not self._container.format.variable_fps
793+
# "variable_fps" is now a flag (handle got removed). Full list at
794+
# https://pyav.org/docs/stable/api/container.html#module-av.format
795+
variable_fps = bool(self._container.format.flags & 0x400)
796+
constant_framerate = not variable_fps
754797

755798
self._seek(index, constant_framerate=constant_framerate)
756799
desired_frame = next(self._decoder)
@@ -762,7 +805,7 @@ def metadata(
762805
"key_frame": bool(desired_frame.key_frame),
763806
"time": desired_frame.time,
764807
"interlaced_frame": bool(desired_frame.interlaced_frame),
765-
"frame_type": desired_frame.pict_type.name,
808+
"frame_type": _get_frame_type(desired_frame.pict_type),
766809
}
767810
)
768811

@@ -781,10 +824,7 @@ def close(self) -> None:
781824
self._flush_writer()
782825

783826
if self._video_stream is not None:
784-
try:
785-
self._video_stream.close()
786-
except ValueError:
787-
pass # stream already closed
827+
self._video_stream = None
788828

789829
if self._container is not None:
790830
self._container.close()
@@ -815,7 +855,7 @@ def init_video_stream(
815855
Parameters
816856
----------
817857
codec : str
818-
The codec to use, e.g. ``"libx264"`` or ``"vp9"``.
858+
The codec to use, e.g. ``"h264"`` or ``"vp9"``.
819859
fps : float
820860
The desired framerate of the video stream (frames per second).
821861
pixel_format : str
@@ -850,7 +890,10 @@ def init_video_stream(
850890
if max_keyframe_interval is not None:
851891
stream.gop_size = max_keyframe_interval
852892
if force_keyframes is not None:
853-
stream.closed_gop = force_keyframes
893+
if force_keyframes:
894+
stream.codec_context.flags |= Flags.closed_gop
895+
else:
896+
stream.codec_context.flags &= ~Flags.closed_gop
854897

855898
self._video_stream = stream
856899

tests/test_pyav.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ def test_mp4_writing(tmp_path, test_images):
6666
frames,
6767
extension=".mp4",
6868
plugin="pyav",
69-
codec="libx264",
69+
codec="h264",
7070
)
7171

72-
# libx264 writing is not deterministic and RGB -> YUV is lossy
72+
# h264 writing is not deterministic and RGB -> YUV is lossy
7373
# so I have no good ideas how to do serious assertions on the file
7474
assert mp4_bytes is not None
7575

@@ -224,7 +224,7 @@ def test_write_bytes(test_images):
224224
img,
225225
extension=".mp4",
226226
plugin="pyav",
227-
codec="libx264",
227+
codec="h264",
228228
)
229229

230230
assert img_bytes is not None
@@ -354,7 +354,7 @@ def test_multiple_writes(test_images):
354354
plugin="pyav",
355355
format="rgba",
356356
):
357-
file.write(frame, is_batch=False, codec="libx264", in_pixel_format="rgba")
357+
file.write(frame, is_batch=False, codec="h264", in_pixel_format="rgba")
358358

359359
actual = out_buffer.getvalue()
360360
assert actual is not None
@@ -509,7 +509,7 @@ def test_procedual_writing_with_filter(test_images):
509509

510510
def test_rotation_flag_metadata(test_images, tmp_path):
511511
with iio.imopen(tmp_path / "test.mp4", "w", plugin="pyav") as file:
512-
file.init_video_stream("libx264")
512+
file.init_video_stream("h264")
513513
file.container_metadata["comment"] = "This video has a rotation flag."
514514
file.video_stream_metadata["rotate"] = "90"
515515

@@ -555,7 +555,7 @@ def test_keyframe_intervals(test_images):
555555
)
556556

557557
out_file.init_video_stream(
558-
"libx264", max_keyframe_interval=5, force_keyframes=True
558+
"h264", max_keyframe_interval=5, force_keyframes=True
559559
)
560560

561561
for idx in range(50):
@@ -568,10 +568,10 @@ def test_keyframe_intervals(test_images):
568568
with iio.imopen(buffer, "r", plugin="pyav") as file:
569569
n_frames = file.properties().shape[0]
570570
for idx in range(1, n_frames):
571-
medatada = file.metadata(index=idx)
572-
if medatada["frame_type"] == "I":
571+
metatada = file.metadata(index=idx)
572+
if metatada["frame_type"] == "I":
573573
i_dist.append(idx)
574-
if medatada["key_frame"]:
574+
if metatada["key_frame"]:
575575
key_dist.append(idx)
576576

577577
assert np.max(np.diff(i_dist)) <= 5

0 commit comments

Comments
 (0)