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 } .\n Incomplete'
1525+ f' frame read: expected { self ._num_bytes_per_image } bytes, but got'
1526+ f' { len (data )} .\n ffmpeg 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 :
0 commit comments