Skip to content

Commit abf2cc1

Browse files
author
The mediapy Authors
committed
Support configurable ffmpeg sandbox timeouts.
PiperOrigin-RevId: 907544350
1 parent 0501236 commit abf2cc1

4 files changed

Lines changed: 36 additions & 6 deletions

File tree

mediapy/__init__.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2025 The mediapy Authors.
1+
# Copyright 2026 The mediapy Authors.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -1192,6 +1192,7 @@ def _run_ffmpeg(
11921192
encoding: None = None, # No encoding -> bytes
11931193
allowed_input_files: Sequence[str] | None = None,
11941194
allowed_output_files: Sequence[str] | None = None,
1195+
sandbox_max_run_time_secs: int | None = None,
11951196
) -> subprocess.Popen[bytes]:
11961197
...
11971198

@@ -1205,6 +1206,7 @@ def _run_ffmpeg(
12051206
encoding: str = ..., # Encoding -> str
12061207
allowed_input_files: Sequence[str] | None = None,
12071208
allowed_output_files: Sequence[str] | None = None,
1209+
sandbox_max_run_time_secs: int | None = None,
12081210
) -> subprocess.Popen[str]:
12091211
...
12101212

@@ -1217,6 +1219,7 @@ def _run_ffmpeg(
12171219
encoding: str | None = None,
12181220
allowed_input_files: Sequence[str] | None = None,
12191221
allowed_output_files: Sequence[str] | None = None,
1222+
sandbox_max_run_time_secs: int | None = None,
12201223
) -> subprocess.Popen[bytes] | subprocess.Popen[str]:
12211224
"""Runs ffmpeg with the given args.
12221225
@@ -1228,6 +1231,8 @@ def _run_ffmpeg(
12281231
encoding: Same as in `subprocess.Popen`.
12291232
allowed_input_files: The input files to allow for ffmpeg.
12301233
allowed_output_files: The output files to allow for ffmpeg.
1234+
sandbox_max_run_time_secs: The maximum time in seconds to run the sandbox.
1235+
If None, the default limit is 30 minutes.
12311236
12321237
Returns:
12331238
The subprocess.Popen object with running ffmpeg process.
@@ -1238,9 +1243,11 @@ def _run_ffmpeg(
12381243
env: Any = None # pylint: disable=unused-variable
12391244
ffmpeg_path = _get_ffmpeg_path()
12401245

1241-
# Allowed input and output files are not supported in open source.
1246+
# Sandbox max runtime, allowed input and ouput files are not supported in
1247+
# open source.
12421248
del allowed_input_files
12431249
del allowed_output_files
1250+
del sandbox_max_run_time_secs
12441251

12451252
argv.append(ffmpeg_path)
12461253
argv.extend(ffmpeg_args)
@@ -1402,6 +1409,8 @@ class VideoReader(_VideoIO):
14021409
bps: The estimated bitrate of the video stream in bits per second, retrieved
14031410
from the video header.
14041411
stream_index: The stream index to read from. The default is 0.
1412+
sandbox_max_run_time_secs: The maximum time in seconds to run the sandbox.
1413+
If None, the default limit is 30 minutes. Unused in open source.
14051414
"""
14061415

14071416
path_or_url: _Path
@@ -1422,6 +1431,7 @@ def __init__(
14221431
stream_index: int = 0,
14231432
output_format: str = 'rgb',
14241433
dtype: _DTypeLike = np.uint8,
1434+
sandbox_max_run_time_secs: int | None = None,
14251435
):
14261436
if output_format not in {'rgb', 'yuv', 'gray'}:
14271437
raise ValueError(
@@ -1433,6 +1443,7 @@ def __init__(
14331443
self.dtype = np.dtype(dtype)
14341444
if self.dtype.type not in (np.uint8, np.uint16):
14351445
raise ValueError(f'Type {dtype} is not np.uint8 or np.uint16.')
1446+
self.sandbox_max_run_time_secs = sandbox_max_run_time_secs
14361447
self._read_via_local_file: Any = None
14371448
self._popen: subprocess.Popen[bytes] | None = None
14381449
self._proc: subprocess.Popen[bytes] | None = None
@@ -1475,6 +1486,7 @@ def __enter__(self) -> 'VideoReader':
14751486
stdout=subprocess.PIPE,
14761487
stderr=subprocess.PIPE,
14771488
allowed_input_files=[tmp_name],
1489+
sandbox_max_run_time_secs=self.sandbox_max_run_time_secs,
14781490
)
14791491
self._proc = self._popen.__enter__()
14801492
except Exception:
@@ -1491,6 +1503,9 @@ def read(self) -> _NDArray | None:
14911503
Returns:
14921504
A numpy array in the format specified by `output_format`, i.e., a 3D
14931505
array with 3 color channels, except for format 'gray' which is 2D.
1506+
1507+
Raises:
1508+
RuntimeError: If there is an error reading from the output file.
14941509
"""
14951510
assert self._proc, 'Error: reading from an already closed context.'
14961511
stdout = self._proc.stdout
@@ -1499,7 +1514,17 @@ def read(self) -> _NDArray | None:
14991514
if not data: # Due to either end-of-file or subprocess error.
15001515
self.close() # Raises exception if subprocess had error.
15011516
return None # To indicate end-of-file.
1502-
assert len(data) == self._num_bytes_per_image
1517+
if len(data) != self._num_bytes_per_image:
1518+
self._proc.wait()
1519+
stderr = self._proc.stderr
1520+
stderr_output = ''
1521+
if stderr is not None:
1522+
stderr_output = stderr.read().decode('utf-8', errors='replace').strip()
1523+
raise RuntimeError(
1524+
f'ffmpeg exited with code {self._proc.returncode}.\nIncomplete'
1525+
f' frame read: expected {self._num_bytes_per_image} bytes, but got'
1526+
f' {len(data)}.\nffmpeg stderr:\n{stderr_output}'
1527+
)
15031528
image = np.frombuffer(data, dtype=self.dtype)
15041529
if self.output_format == 'rgb':
15051530
image = image.reshape(*self.shape, 3)
@@ -1574,6 +1599,8 @@ class VideoWriter(_VideoIO):
15741599
'yuv420p' (2x2-subsampled chroma), 'yuv444p' (full-res chroma),
15751600
'yuv420p10le' (10-bit per channel), etc. The default (None) selects
15761601
'yuv420p' if all shape dimensions are even, else 'yuv444p'.
1602+
sandbox_max_run_time_secs: The maximum time in seconds to run the sandbox.
1603+
If None, the default limit is 30 minutes. Unused in open source.
15771604
"""
15781605

15791606
def __init__(
@@ -1591,6 +1618,7 @@ def __init__(
15911618
input_format: str = 'rgb',
15921619
dtype: _DTypeLike = np.uint8,
15931620
encoded_format: str | None = None,
1621+
sandbox_max_run_time_secs: int | None = None,
15941622
) -> None:
15951623
_check_2d_shape(shape)
15961624
if fps is None and metadata:
@@ -1645,6 +1673,7 @@ def __init__(
16451673
self.input_format = input_format
16461674
self.dtype = dtype
16471675
self.encoded_format = encoded_format
1676+
self.sandbox_max_run_time_secs = sandbox_max_run_time_secs
16481677
if num_rate_specifications == 0 and not ffmpeg_args:
16491678
qp = 20 if math.prod(self.shape) <= 640 * 480 else 28
16501679
self._bitrate_args = (
@@ -1707,6 +1736,7 @@ def __enter__(self) -> 'VideoWriter':
17071736
stdin=subprocess.PIPE,
17081737
stderr=subprocess.PIPE,
17091738
allowed_output_files=[tmp_name],
1739+
sandbox_max_run_time_secs=self.sandbox_max_run_time_secs,
17101740
)
17111741
self._proc = self._popen.__enter__()
17121742
except Exception:

mediapy_examples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2025 The mediapy Authors.
1+
# Copyright 2026 The mediapy Authors.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

mediapy_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2025 The mediapy Authors.
1+
# Copyright 2026 The mediapy Authors.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

pdoc_files/make.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2025 The mediapy Authors.
1+
# Copyright 2026 The mediapy Authors.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)