Skip to content

Commit c97bf57

Browse files
committed
-removed streams options
-added docs.options entry for -map
1 parent 26dcc5a commit c97bf57

6 files changed

Lines changed: 375 additions & 43 deletions

File tree

docsrc/options.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ af str X X Audio filtergraph (leave output pad unlabeled
3131
crf int X X H.264 video encoding constant quality factor (0-51)
3232
========== ========= = = = = ============================================================
3333

34-
Special handling of `s` output option
35-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34+
`s` output option
35+
^^^^^^^^^^^^^^^^^
3636

3737
FFmpeg's :code:`-s` output option sets the output video frame size by using the scale video filter. However,
3838
it does not allow non-positive values for width and height which the scale filter accepts.
@@ -53,6 +53,16 @@ n (n>0) Specifying the output size to be n pixels
5353
Note that passing both :code:`s` with a non-positive value and :code:`vf`
5454
will raise an exception.
5555

56+
`map` output options
57+
^^^^^^^^^^^^^^^^^^^^
58+
59+
The output option `-map` is the (only?) FFmpeg option, which could be specified multiple times
60+
in command line input. This goes against :py:mod:`ffmpegio`'s FFmpeg dict structure, and so `map`
61+
argument is handled differently from the others. First, `map` argument must be a non-`str` sequence,
62+
and each of its element is converted to `-map` option. Furthermore, each element could be a str or
63+
else a sequence which items are then stringified and joined together with `':'`.
64+
65+
5666
Video Pixel Formats :code:`pix_fmt`
5767
-----------------------------------
5868

src/ffmpegio/configure.py

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -435,21 +435,15 @@ def clear_loglevel(args):
435435
pass
436436

437437

438-
def finalize_media_read_opts(args, streams=None):
438+
def finalize_media_read_opts(args):
439439
"""finalize multiple-input media reader setup
440440
441441
:param args: FFmpeg dict
442442
:type args: dict
443-
:param streams: list of file + stream specifiers or filtergraph label to output, alias of `map` option,
444-
defaults to None, which outputs at most one video and one audio, selected by FFmpeg
445-
:type streams: seq(str), optional
446443
:return: use_ya8 flag - True to expect 16-bpc pixel to be ya8 pix_fmt instead of gray16le pix_fmt
447444
:rtype: bool
448445
"""
449446

450-
# number of input files
451-
ninputs = len(args["inputs"])
452-
453447
# get output options, create new
454448
options = args["outputs"][0][1]
455449
if options is None:
@@ -483,31 +477,4 @@ def finalize_media_read_opts(args, streams=None):
483477
for k in utils.find_stream_options(options, "sample_fmt"):
484478
options[f"c:a" + k[10:]] = utils.get_audio_format(options[k])[0]
485479

486-
# map
487-
if streams is not None:
488-
# separate file ids (if present)
489-
def split_file_id(s):
490-
try:
491-
m = re.match(r"(\d+):", s)
492-
s = s[m.end() :]
493-
return (int(m[1]), s[m.end() :]) if m else (None, s)
494-
except:
495-
try:
496-
assert len(s) == 2
497-
return (int(s[0]), s[1])
498-
except:
499-
return (None, s)
500-
501-
streams = [split_file_id(s) for s in streams]
502-
503-
if ninputs == 1:
504-
streams = [(0, s[1]) if s[0] is None else s for s in streams]
505-
elif any((s for s in streams if s[0] is None)):
506-
raise ValueError(
507-
'multi-url mode requires to specify the file associated with each stream. e.g., "0:v:0"'
508-
)
509-
510-
# overrides map option if set
511-
options["map"] = streams
512-
513480
return ya8 > 0

src/ffmpegio/media.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
66
from .utils import avi
77

88

9-
def read(*urls, streams=None, progress=None, show_log=None, **options):
9+
def read(*urls, progress=None, show_log=None, **options):
1010
"""Read video frames
1111
1212
:param *urls: URLs of the media files to read.
1313
:type *urls: tuple(str)
14-
:param streams: list of file + stream specifiers or filtergraph label to output, alias of `map` option,
15-
defaults to None, which outputs at most one video and one audio, selected by FFmpeg
16-
:type streams: seq(str), optional
1714
:param progress: progress callback function, defaults to None
1815
:type progress: callable object, optional
1916
:param show_log: True to show FFmpeg log messages on the console,
@@ -61,7 +58,7 @@ def read(*urls, streams=None, progress=None, show_log=None, **options):
6158
configure.add_url(args, "input", url, {*inopts, *spec_inopts.get(i, {})})
6259

6360
# configure output options
64-
use_ya8 = configure.finalize_media_read_opts(args, streams)
61+
use_ya8 = configure.finalize_media_read_opts(args)
6562

6663
# run FFmpeg
6764
out = ffmpegprocess.run(

src/ffmpegio/streams/AviStreams.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
2+
class AviMediaReader:
3+
"""Read video frames
4+
5+
:param *urls: URLs of the media files to read.
6+
:type *urls: tuple(str)
7+
:param streams: list of file + stream specifiers or filtergraph label to output, alias of `map` option,
8+
defaults to None, which outputs at most one video and one audio, selected by FFmpeg
9+
:type streams: seq(str), optional
10+
:param progress: progress callback function, defaults to None
11+
:type progress: callable object, optional
12+
:param show_log: True to show FFmpeg log messages on the console,
13+
defaults to None (no show/capture)
14+
Ignored if stream format must be retrieved automatically.
15+
:type show_log: bool, optional
16+
:param use_ya8: True if piped video streams uses `ya8` pix_fmt instead of `gray16le`, default to None
17+
:type use_ya8: bool, optional
18+
:param \\**options: FFmpeg options, append '_in[input_url_id]' for input option names for specific
19+
input url or '_in' to be applied to all inputs. The url-specific option gets the
20+
preference (see :doc:`options` for custom options)
21+
:type \\**options: dict, optional
22+
23+
:return: frame rate and video frame data (dims: time x rows x cols x pix_comps)
24+
:rtype: (`fractions.Fraction`, `numpy.ndarray`)
25+
26+
Note: Only pass in multiple urls to implement complex filtergraph. It's significantly faster to run
27+
`ffmpegio.video.read()` for each url.
28+
29+
30+
Unlike :py:mod:`video` and :py:mod:`image`, video pixel formats are not autodetected. If output
31+
'pix_fmt' option is not explicitly set, 'rgb24' is used.
32+
33+
For audio streams, if 'sample_fmt' output option is not specified, 's16le'.
34+
35+
36+
streams = ['0:v:0','1:a:3'] # pick 1st file's 1st video stream and 2nd file's 4th audio stream
37+
38+
"""
39+
40+
def __init__(self, *urls, streams=None, progress=None, show_log=None, blocksize=None, **options):
41+
42+
self.dtype = None # :numpy.dtype: output data type
43+
self.shape = (
44+
None # :tuple of ints: dimension of each video frame or audio sample
45+
)
46+
self.itemsize = None #:int: number of bytes of each video frame or audio sample
47+
self.blocksize = None #:positive int: number of video frames or audio samples to read when used as an iterator
48+
49+
# get url/file stream
50+
url, stdin, input = configure.check_url(url, False)
51+
52+
input_options = utils.pop_extra_options(options, "_in")
53+
54+
ffmpeg_args = configure.empty()
55+
configure.add_url(ffmpeg_args, "input", url, input_options)
56+
configure.add_url(ffmpeg_args, "output", "-", options)
57+
58+
# abstract method to finalize the options => sets self.dtype and self.shape if known
59+
self._finalize(ffmpeg_args)
60+
61+
# create logger without assigning the source stream
62+
self._logger = _LogerThread(None, show_log)
63+
64+
# start FFmpeg
65+
self._proc = ffmpegprocess.Popen(
66+
ffmpeg_args,
67+
stdin=stdin,
68+
progress=progress,
69+
capture_log=True,
70+
close_stdin=True,
71+
close_stdout=False,
72+
close_stderr=False,
73+
)
74+
75+
# set the log source and start the logger
76+
self._logger.stderr = self._proc.stderr
77+
self._logger.start()
78+
79+
# if byte data is given, feed it
80+
if input is not None:
81+
self._proc.stdin.write(input)
82+
83+
# wait until output stream log is captured if output format is unknown
84+
try:
85+
if self.dtype is None or self.shape is None:
86+
info = self._logger.output_stream()
87+
self._finalize_array(info)
88+
else:
89+
self._logger.index("Output")
90+
except:
91+
if self._proc.poll() is None:
92+
raise self._logger.Exception
93+
else:
94+
raise ValueError("failed retrieve output data format")
95+
96+
self.itemsize = utils.get_itemsize(self.shape, self.dtype)
97+
98+
self.blocksize = blocksize or max(1024 ** 2 // self.itemsize, 1)
99+
100+
def close(self):
101+
"""Flush and close this stream. This method has no effect if the stream is already
102+
closed. Once the stream is closed, any read operation on the stream will raise
103+
a ValueError.
104+
105+
As a convenience, it is allowed to call this method more than once; only the first call,
106+
however, will have an effect.
107+
108+
"""
109+
self._proc.stdout.close()
110+
self._proc.stderr.close()
111+
try:
112+
self._proc.terminate()
113+
except:
114+
pass
115+
self._logger.join()
116+
117+
@property
118+
def closed(self):
119+
""":bool: True if the stream is closed."""
120+
return self._proc.poll() is not None
121+
122+
@property
123+
def lasterror(self):
124+
""":FFmpegError: Last error FFmpeg posted"""
125+
if self._proc.poll():
126+
return self._logger.Exception()
127+
else:
128+
return None
129+
130+
def __enter__(self):
131+
return self
132+
133+
def __exit__(self, exc_type, exc_value, traceback):
134+
self.close()
135+
136+
def __iter__(self):
137+
return self
138+
139+
def __next__(self):
140+
try:
141+
return self.read(self.blocksize)
142+
except:
143+
raise StopIteration
144+
145+
def readlog(self, n=None):
146+
if n is not None:
147+
self._logger.index(n)
148+
with self._logger._newline_mutex:
149+
return "\n".join(self._logger.logs or self._logger.logs[:n])
150+
151+
def read(self, n=-1):
152+
"""Read and return numpy.ndarray with up to n frames/samples. If
153+
the argument is omitted, None, or negative, data is read and
154+
returned until EOF is reached. An empty bytes object is returned
155+
if the stream is already at EOF.
156+
157+
If the argument is positive, and the underlying raw stream is not
158+
interactive, multiple raw reads may be issued to satisfy the byte
159+
count (unless EOF is reached first). But for interactive raw streams,
160+
at most one raw read will be issued, and a short result does not
161+
imply that EOF is imminent.
162+
163+
A BlockingIOError is raised if the underlying raw stream is in non
164+
blocking-mode, and has no data available at the moment."""
165+
166+
b = self._proc.stdout.read(n * self.itemsize if n > 0 else n)
167+
if not len(b):
168+
self._proc.stdout.close()
169+
return _as_array(b, self.shape, self.dtype)
170+
171+
def readinto(self, array):
172+
"""Read bytes into a pre-allocated, writable bytes-like object array and
173+
return the number of bytes read. For example, b might be a bytearray.
174+
175+
Like read(), multiple reads may be issued to the underlying raw stream,
176+
unless the latter is interactive.
177+
178+
A BlockingIOError is raised if the underlying raw stream is in non
179+
blocking-mode, and has no data available at the moment."""
180+
181+
return self._proc.stdout.readinto(memoryview(array).cast("b")) // self.itemsize
182+
183+
184+
class SimpleVideoReader(SimpleReaderBase):
185+
def _finalize(self, ffmpeg_args):
186+
# finalize FFmpeg arguments and output array
187+
188+
inurl, inopts = ffmpeg_args.get("inputs", [])[0]
189+
outopts = ffmpeg_args.get("outputs", [])[0][1]
190+
has_fg = configure.has_filtergraph(ffmpeg_args, "video")
191+
192+
pix_fmt = outopts.get("pix_fmt", None)
193+
if pix_fmt is None or (
194+
not has_fg
195+
and inurl not in ("-", "pipe:", "pipe:0")
196+
and not inopts.get("pix_fmt", None)
197+
):
198+
# must assign output rgb/grayscale pixel format
199+
info = probe.video_streams_basic(inurl, 0)[0]
200+
pix_fmt_in = info["pix_fmt"]
201+
s_in = (info["width"], info["height"])
202+
r_in = info["frame_rate"]
203+
else:
204+
pix_fmt_in = s_in = r_in = None
205+
206+
(
207+
self.dtype,
208+
self.shape,
209+
self.frame_rate,
210+
) = configure.finalize_video_read_opts(ffmpeg_args, pix_fmt_in, s_in, r_in)
211+
212+
# construct basic video filter if options specified
213+
configure.build_basic_vf(
214+
ffmpeg_args, utils.alpha_change(pix_fmt_in, pix_fmt, -1)
215+
)
216+
217+
def _finalize_array(self, info):
218+
# finalize array setup from FFmpeg log
219+
220+
self.frame_rate = info["r"]
221+
self.dtype, ncomp, _ = utils.get_video_format(info["pix_fmt"])
222+
self.shape = (*info["s"][::-1], ncomp)

0 commit comments

Comments
 (0)