1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4"""Read/Write video using FFMPEG
5
6.. note::
7 We are in the process of (slowly) replacing this plugin with a new one that
8 is based on `pyav <https://pyav.org/docs/stable/>`_. It is faster and more
9 flexible than the plugin documented here. Check the :mod:`pyav
10 plugin's documentation <imageio.plugins.pyav>` for more information about
11 this plugin.
12
13Backend Library: https://github.com/imageio/imageio-ffmpeg
14
15.. note::
16 To use this plugin you have to install its backend::
17
18 pip install imageio[ffmpeg]
19
20
21The ffmpeg format provides reading and writing for a wide range of movie formats
22such as .avi, .mpeg, .mp4, etc. as well as the ability to read streams from
23webcams and USB cameras. It is based on ffmpeg and is inspired by/based `moviepy
24<https://github.com/Zulko/moviepy/>`_ by Zulko.
25
26Parameters for reading
27----------------------
28fps : scalar
29 The number of frames per second of the input stream. Default None (i.e.
30 read at the file's native fps). One can use this for files with a
31 variable fps, or in cases where imageio is unable to correctly detect
32 the fps. In case of trouble opening camera streams, it may help to set an
33 explicit fps value matching a framerate supported by the camera.
34loop : bool
35 If True, the video will rewind as soon as a frame is requested
36 beyond the last frame. Otherwise, IndexError is raised. Default False.
37 Setting this to True will internally call ``count_frames()``,
38 and set the reader's length to that value instead of inf.
39size : str | tuple
40 The frame size (i.e. resolution) to read the images, e.g.
41 (100, 100) or "640x480". For camera streams, this allows setting
42 the capture resolution. For normal video data, ffmpeg will
43 rescale the data.
44dtype : str | type
45 The dtype for the output arrays. Determines the bit-depth that
46 is requested from ffmpeg. Supported dtypes: uint8, uint16.
47 Default: uint8.
48pixelformat : str
49 The pixel format for the camera to use (e.g. "yuyv422" or
50 "gray"). The camera needs to support the format in order for
51 this to take effect. Note that the images produced by this
52 reader are always RGB.
53input_params : list
54 List additional arguments to ffmpeg for input file options.
55 (Can also be provided as ``ffmpeg_params`` for backwards compatibility)
56 Example ffmpeg arguments to use aggressive error handling:
57 ['-err_detect', 'aggressive']
58output_params : list
59 List additional arguments to ffmpeg for output file options (i.e. the
60 stream being read by imageio).
61print_info : bool
62 Print information about the video file as reported by ffmpeg.
63
64Parameters for writing
65----------------------
66fps : scalar
67 The number of frames per second. Default 10.
68codec : str
69 the video codec to use. Default 'libx264', which represents the
70 widely available mpeg4. Except when saving .wmv files, then the
71 defaults is 'msmpeg4' which is more commonly supported for windows
72quality : float | None
73 Video output quality. Default is 5. Uses variable bit rate. Highest
74 quality is 10, lowest is 0. Set to None to prevent variable bitrate
75 flags to FFMPEG so you can manually specify them using output_params
76 instead. Specifying a fixed bitrate using 'bitrate' disables this
77 parameter.
78bitrate : int | None
79 Set a constant bitrate for the video encoding. Default is None causing
80 'quality' parameter to be used instead. Better quality videos with
81 smaller file sizes will result from using the 'quality' variable
82 bitrate parameter rather than specifying a fixed bitrate with this
83 parameter.
84pixelformat: str
85 The output video pixel format. Default is 'yuv420p' which most widely
86 supported by video players.
87input_params : list
88 List additional arguments to ffmpeg for input file options (i.e. the
89 stream that imageio provides).
90output_params : list
91 List additional arguments to ffmpeg for output file options.
92 (Can also be provided as ``ffmpeg_params`` for backwards compatibility)
93 Example ffmpeg arguments to use only intra frames and set aspect ratio:
94 ['-intra', '-aspect', '16:9']
95ffmpeg_log_level: str
96 Sets ffmpeg output log level. Default is "warning".
97 Values can be "quiet", "panic", "fatal", "error", "warning", "info"
98 "verbose", or "debug". Also prints the FFMPEG command being used by
99 imageio if "info", "verbose", or "debug".
100macro_block_size: int
101 Size constraint for video. Width and height, must be divisible by this
102 number. If not divisible by this number imageio will tell ffmpeg to
103 scale the image up to the next closest size
104 divisible by this number. Most codecs are compatible with a macroblock
105 size of 16 (default), some can go smaller (4, 8). To disable this
106 automatic feature set it to None or 1, however be warned many players
107 can't decode videos that are odd in size and some codecs will produce
108 poor results or fail. See https://en.wikipedia.org/wiki/Macroblock.
109audio_path : str | None
110 Audio path of any audio that needs to be written. Defaults to nothing,
111 so no audio will be written. Please note, when writing shorter video
112 than the original, ffmpeg will not truncate the audio track; it
113 will maintain its original length and be longer than the video.
114audio_codec : str | None
115 The audio codec to use. Defaults to nothing, but if an audio_path has
116 been provided ffmpeg will attempt to set a default codec.
117
118Notes
119-----
120If you are using anaconda and ``anaconda/ffmpeg`` you will not be able to
121encode/decode H.264 (likely due to licensing concerns). If you need this
122format on anaconda install ``conda-forge/ffmpeg`` instead.
123
124You can use the ``IMAGEIO_FFMPEG_EXE`` environment variable to force using a
125specific ffmpeg executable.
126
127To get the number of frames before having read them all, you can use the
128``reader.count_frames()`` method (the reader will then use
129``imageio_ffmpeg.count_frames_and_secs()`` to get the exact number of frames,
130note that this operation can take a few seconds on large files). Alternatively,
131the number of frames can be estimated from the fps and duration in the meta data
132(though these values themselves are not always present/reliable).
133
134"""
135
136import re
137import sys
138import time
139import logging
140import platform
141import threading
142import subprocess as sp
143import imageio_ffmpeg
144
145import numpy as np
146
147from ..core import Format, image_as_uint
148
149logger = logging.getLogger(__name__)
150
151# Get camera format
152if sys.platform.startswith("win"):
153 CAM_FORMAT = "dshow" # dshow or vfwcap
154elif sys.platform.startswith("linux"):
155 CAM_FORMAT = "video4linux2"
156elif sys.platform.startswith("darwin"):
157 CAM_FORMAT = "avfoundation"
158else: # pragma: no cover
159 CAM_FORMAT = "unknown-cam-format"
160
161
162def download(directory=None, force_download=False): # pragma: no cover
163 raise RuntimeError(
164 "imageio.ffmpeg.download() has been deprecated. "
165 "Use 'pip install imageio-ffmpeg' instead.'"
166 )
167
168
169# For backwards compatibility - we dont use this ourselves
170def get_exe(): # pragma: no cover
171 """Wrapper for imageio_ffmpeg.get_ffmpeg_exe()"""
172
173 return imageio_ffmpeg.get_ffmpeg_exe()
174
175
176class FfmpegFormat(Format):
177 """Read/Write ImageResources using FFMPEG.
178
179 See :mod:`imageio.plugins.ffmpeg`
180 """
181
182 def _can_read(self, request):
183 # Read from video stream?
184 # Note that we could write the _video flag here, but a user might
185 # select this format explicitly (and this code is not run)
186 if re.match(r"<video(\d+)>", request.filename):
187 return True
188
189 # Read from file that we know?
190 if request.extension in self.extensions:
191 return True
192
193 def _can_write(self, request):
194 if request.extension in self.extensions:
195 return True
196
197 # --
198
199 class Reader(Format.Reader):
200 _frame_catcher = None
201 _read_gen = None
202
203 def _get_cam_inputname(self, index):
204 if sys.platform.startswith("linux"):
205 return "/dev/" + self.request._video[1:-1]
206
207 elif sys.platform.startswith("win"):
208 # Ask ffmpeg for list of dshow device names
209 ffmpeg_api = imageio_ffmpeg
210 cmd = [
211 ffmpeg_api.get_ffmpeg_exe(),
212 "-list_devices",
213 "true",
214 "-f",
215 CAM_FORMAT,
216 "-i",
217 "dummy",
218 ]
219 # Set `shell=True` in sp.run to prevent popup of a command
220 # line window in frozen applications. Note: this would be a
221 # security vulnerability if user-input goes into the cmd.
222 # Note that the ffmpeg process returns with exit code 1 when
223 # using `-list_devices` (or `-list_options`), even if the
224 # command is successful, so we set `check=False` explicitly.
225 completed_process = sp.run(
226 cmd,
227 stdout=sp.PIPE,
228 stderr=sp.PIPE,
229 encoding="utf-8",
230 shell=True,
231 check=False,
232 )
233
234 # Return device name at index
235 try:
236 name = parse_device_names(completed_process.stderr)[index]
237 except IndexError:
238 raise IndexError("No ffdshow camera at index %i." % index)
239 return "video=%s" % name
240
241 elif sys.platform.startswith("darwin"):
242 # Appears that newer ffmpeg builds don't support -list-devices
243 # on OS X. But you can directly open the camera by index.
244 name = str(index)
245 return name
246
247 else: # pragma: no cover
248 return "??"
249
250 def _open(
251 self,
252 loop=False,
253 size=None,
254 dtype=None,
255 pixelformat=None,
256 print_info=False,
257 ffmpeg_params=None,
258 input_params=None,
259 output_params=None,
260 fps=None,
261 ):
262 # Get generator functions
263 self._ffmpeg_api = imageio_ffmpeg
264 # Process input args
265 self._arg_loop = bool(loop)
266 if size is None:
267 self._arg_size = None
268 elif isinstance(size, tuple):
269 self._arg_size = "%ix%i" % size
270 elif isinstance(size, str) and "x" in size:
271 self._arg_size = size
272 else:
273 raise ValueError('FFMPEG size must be tuple of "NxM"')
274 if pixelformat is None:
275 pass
276 elif not isinstance(pixelformat, str):
277 raise ValueError("FFMPEG pixelformat must be str")
278 if dtype is None:
279 self._dtype = np.dtype("uint8")
280 else:
281 self._dtype = np.dtype(dtype)
282 allowed_dtypes = ["uint8", "uint16"]
283 if self._dtype.name not in allowed_dtypes:
284 raise ValueError(
285 "dtype must be one of: {}".format(", ".join(allowed_dtypes))
286 )
287 self._arg_pixelformat = pixelformat
288 self._arg_input_params = input_params or []
289 self._arg_output_params = output_params or []
290 self._arg_input_params += ffmpeg_params or [] # backward compat
291 # Write "_video"_arg - indicating webcam support
292 self.request._video = None
293 regex_match = re.match(r"<video(\d+)>", self.request.filename)
294 if regex_match:
295 self.request._video = self.request.filename
296 # Get local filename
297 if self.request._video:
298 index = int(regex_match.group(1))
299 self._filename = self._get_cam_inputname(index)
300 else:
301 self._filename = self.request.get_local_filename()
302 # When passed to ffmpeg on command line, carets need to be escaped.
303 self._filename = self._filename.replace("^", "^^")
304 # Determine pixel format and depth
305 self._depth = 3
306 if self._dtype.name == "uint8":
307 self._pix_fmt = "rgb24"
308 self._bytes_per_channel = 1
309 else:
310 self._pix_fmt = "rgb48le"
311 self._bytes_per_channel = 2
312 # Initialize parameters
313 self._pos = -1
314 self._meta = {"plugin": "ffmpeg"}
315 self._lastread = None
316
317 # Calculating this from fps and duration is not accurate,
318 # and calculating it exactly with ffmpeg_api.count_frames_and_secs
319 # takes too long to do for each video. But we need it for looping.
320 self._nframes = float("inf")
321 if self._arg_loop and not self.request._video:
322 self._nframes = self.count_frames()
323 self._meta["nframes"] = self._nframes
324
325 # Specify input framerate? (only on macOS)
326 # Ideally we'd get the supported framerate from the metadata, but we get the
327 # metadata when we boot ffmpeg ... maybe we could refactor this so we can
328 # get the metadata beforehand, but for now we'll just give it 2 tries on MacOS,
329 # one with fps 30 and one with fps 15.
330 need_input_fps = need_output_fps = False
331 if self.request._video and platform.system().lower() == "darwin":
332 if "-framerate" not in str(self._arg_input_params):
333 need_input_fps = True
334 if not self.request.kwargs.get("fps", None):
335 need_output_fps = True
336 if need_input_fps:
337 self._arg_input_params.extend(["-framerate", str(float(30))])
338 if need_output_fps:
339 self._arg_output_params.extend(["-r", str(float(30))])
340
341 # Start ffmpeg subprocess and get meta information
342 try:
343 self._initialize()
344 except IndexError:
345 # Specify input framerate again, this time different.
346 if need_input_fps:
347 self._arg_input_params[-1] = str(float(15))
348 self._initialize()
349 else:
350 raise
351
352 # For cameras, create thread that keeps reading the images
353 if self.request._video:
354 self._frame_catcher = FrameCatcher(self._read_gen)
355
356 # For reference - but disabled, because it is inaccurate
357 # if self._meta["nframes"] == float("inf"):
358 # if self._meta.get("fps", 0) > 0:
359 # if self._meta.get("duration", 0) > 0:
360 # n = round(self._meta["duration"] * self._meta["fps"])
361 # self._meta["nframes"] = int(n)
362
363 def _close(self):
364 # First close the frame catcher, because we cannot close the gen
365 # if the frame catcher thread is using it
366 if self._frame_catcher is not None:
367 self._frame_catcher.stop_me()
368 self._frame_catcher = None
369 if self._read_gen is not None:
370 self._read_gen.close()
371 self._read_gen = None
372
373 def count_frames(self):
374 """Count the number of frames. Note that this can take a few
375 seconds for large files. Also note that it counts the number
376 of frames in the original video and does not take a given fps
377 into account.
378 """
379 # This would have been nice, but this does not work :(
380 # oargs = []
381 # if self.request.kwargs.get("fps", None):
382 # fps = float(self.request.kwargs["fps"])
383 # oargs += ["-r", "%.02f" % fps]
384 cf = self._ffmpeg_api.count_frames_and_secs
385 return cf(self._filename)[0]
386
387 def _get_length(self):
388 return self._nframes # only not inf if loop is True
389
390 def _get_data(self, index):
391 """Reads a frame at index. Note for coders: getting an
392 arbitrary frame in the video with ffmpeg can be painfully
393 slow if some decoding has to be done. This function tries
394 to avoid fectching arbitrary frames whenever possible, by
395 moving between adjacent frames."""
396 # Modulo index (for looping)
397 if self._arg_loop and self._nframes < float("inf"):
398 index %= self._nframes
399
400 if index == self._pos:
401 return self._lastread, dict(new=False)
402 elif index < 0:
403 raise IndexError("Frame index must be >= 0")
404 elif index >= self._nframes:
405 raise IndexError("Reached end of video")
406 else:
407 if (index < self._pos) or (index > self._pos + 100):
408 self._initialize(index)
409 else:
410 self._skip_frames(index - self._pos - 1)
411 result, is_new = self._read_frame()
412 self._pos = index
413 return result, dict(new=is_new)
414
415 def _get_meta_data(self, index):
416 return self._meta
417
418 def _initialize(self, index=0):
419 # Close the current generator, and thereby terminate its subprocess
420 if self._read_gen is not None:
421 self._read_gen.close()
422
423 iargs = []
424 oargs = []
425
426 # Create input args
427 iargs += self._arg_input_params
428 if self.request._video:
429 iargs += ["-f", CAM_FORMAT]
430 if self._arg_pixelformat:
431 iargs += ["-pix_fmt", self._arg_pixelformat]
432 if self._arg_size:
433 iargs += ["-s", self._arg_size]
434 elif index > 0: # re-initialize / seek
435 # Note: only works if we initialized earlier, and now have meta
436 # Some info here: https://trac.ffmpeg.org/wiki/Seeking
437 # There are two ways to seek, one before -i (input_params) and
438 # after (output_params). The former is fast, because it uses
439 # keyframes, the latter is slow but accurate. According to
440 # the article above, the fast method should also be accurate
441 # from ffmpeg version 2.1, however in version 4.1 our tests
442 # start failing again. Not sure why, but we can solve this
443 # by combining slow and fast. Seek the long stretch using
444 # the fast method, and seek the last 10s the slow way.
445 starttime = index / self._meta["fps"]
446 seek_slow = min(10, starttime)
447 seek_fast = starttime - seek_slow
448 # We used to have this epsilon earlier, when we did not use
449 # the slow seek. I don't think we need it anymore.
450 # epsilon = -1 / self._meta["fps"] * 0.1
451 iargs += ["-ss", "%.06f" % (seek_fast)]
452 oargs += ["-ss", "%.06f" % (seek_slow)]
453
454 # Output args, for writing to pipe
455 if self._arg_size:
456 oargs += ["-s", self._arg_size]
457 if self.request.kwargs.get("fps", None):
458 fps = float(self.request.kwargs["fps"])
459 oargs += ["-r", "%.02f" % fps]
460 oargs += self._arg_output_params
461
462 # Get pixelformat and bytes per pixel
463 pix_fmt = self._pix_fmt
464 bpp = self._depth * self._bytes_per_channel
465
466 # Create generator
467 rf = self._ffmpeg_api.read_frames
468 self._read_gen = rf(
469 self._filename, pix_fmt, bpp, input_params=iargs, output_params=oargs
470 )
471
472 # Read meta data. This start the generator (and ffmpeg subprocess)
473 if self.request._video:
474 # With cameras, catch error and turn into IndexError
475 try:
476 meta = self._read_gen.__next__()
477 except IOError as err:
478 err_text = str(err)
479 if "darwin" in sys.platform:
480 if "Unknown input format: 'avfoundation'" in err_text:
481 err_text += (
482 "Try installing FFMPEG using "
483 "home brew to get a version with "
484 "support for cameras."
485 )
486 raise IndexError(
487 "No (working) camera at {}.\n\n{}".format(
488 self.request._video, err_text
489 )
490 )
491 else:
492 self._meta.update(meta)
493 elif index == 0:
494 self._meta.update(self._read_gen.__next__())
495 else:
496 self._read_gen.__next__() # we already have meta data
497
498 def _skip_frames(self, n=1):
499 """Reads and throws away n frames"""
500 for i in range(n):
501 self._read_gen.__next__()
502 self._pos += n
503
504 def _read_frame(self):
505 # Read and convert to numpy array
506 w, h = self._meta["size"]
507 framesize = w * h * self._depth * self._bytes_per_channel
508 # t0 = time.time()
509
510 # Read frame
511 if self._frame_catcher: # pragma: no cover - camera thing
512 s, is_new = self._frame_catcher.get_frame()
513 else:
514 s = self._read_gen.__next__()
515 is_new = True
516
517 # Check
518 if len(s) != framesize:
519 raise RuntimeError(
520 "Frame is %i bytes, but expected %i." % (len(s), framesize)
521 )
522
523 result = np.frombuffer(s, dtype=self._dtype).copy()
524 result = result.reshape((h, w, self._depth))
525 # t1 = time.time()
526 # print('etime', t1-t0)
527
528 # Store and return
529 self._lastread = result
530 return result, is_new
531
532 # --
533
534 class Writer(Format.Writer):
535 _write_gen = None
536
537 def _open(
538 self,
539 fps=10,
540 codec="libx264",
541 bitrate=None,
542 pixelformat="yuv420p",
543 ffmpeg_params=None,
544 input_params=None,
545 output_params=None,
546 ffmpeg_log_level="quiet",
547 quality=5,
548 macro_block_size=16,
549 audio_path=None,
550 audio_codec=None,
551 ):
552 self._ffmpeg_api = imageio_ffmpeg
553 self._filename = self.request.get_local_filename()
554 self._pix_fmt = None
555 self._depth = None
556 self._size = None
557
558 def _close(self):
559 if self._write_gen is not None:
560 self._write_gen.close()
561 self._write_gen = None
562
563 def _append_data(self, im, meta):
564 # Get props of image
565 h, w = im.shape[:2]
566 size = w, h
567 depth = 1 if im.ndim == 2 else im.shape[2]
568
569 # Ensure that image is in uint8
570 im = image_as_uint(im, bitdepth=8)
571 # To be written efficiently, ie. without creating an immutable
572 # buffer, by calling im.tobytes() the array must be contiguous.
573 if not im.flags.c_contiguous:
574 # checkign the flag is a micro optimization.
575 # the image will be a numpy subclass. See discussion
576 # https://github.com/numpy/numpy/issues/11804
577 im = np.ascontiguousarray(im)
578
579 # Set size and initialize if not initialized yet
580 if self._size is None:
581 map = {1: "gray", 2: "gray8a", 3: "rgb24", 4: "rgba"}
582 self._pix_fmt = map.get(depth, None)
583 if self._pix_fmt is None:
584 raise ValueError("Image must have 1, 2, 3 or 4 channels")
585 self._size = size
586 self._depth = depth
587 self._initialize()
588
589 # Check size of image
590 if size != self._size:
591 raise ValueError("All images in a movie should have same size")
592 if depth != self._depth:
593 raise ValueError(
594 "All images in a movie should have same " "number of channels"
595 )
596
597 assert self._write_gen is not None # Check status
598
599 # Write. Yes, we can send the data in as a numpy array
600 self._write_gen.send(im)
601
602 def set_meta_data(self, meta):
603 raise RuntimeError(
604 "The ffmpeg format does not support setting " "meta data."
605 )
606
607 def _initialize(self):
608 # Close existing generator
609 if self._write_gen is not None:
610 self._write_gen.close()
611
612 # Get parameters
613 # Use None to let imageio-ffmpeg (or ffmpeg) select good results
614 fps = self.request.kwargs.get("fps", 10)
615 codec = self.request.kwargs.get("codec", None)
616 bitrate = self.request.kwargs.get("bitrate", None)
617 quality = self.request.kwargs.get("quality", None)
618 input_params = self.request.kwargs.get("input_params") or []
619 output_params = self.request.kwargs.get("output_params") or []
620 output_params += self.request.kwargs.get("ffmpeg_params") or []
621 pixelformat = self.request.kwargs.get("pixelformat", None)
622 macro_block_size = self.request.kwargs.get("macro_block_size", 16)
623 ffmpeg_log_level = self.request.kwargs.get("ffmpeg_log_level", None)
624 audio_path = self.request.kwargs.get("audio_path", None)
625 audio_codec = self.request.kwargs.get("audio_codec", None)
626
627 macro_block_size = macro_block_size or 1 # None -> 1
628
629 # Create generator
630 self._write_gen = self._ffmpeg_api.write_frames(
631 self._filename,
632 self._size,
633 pix_fmt_in=self._pix_fmt,
634 pix_fmt_out=pixelformat,
635 fps=fps,
636 quality=quality,
637 bitrate=bitrate,
638 codec=codec,
639 macro_block_size=macro_block_size,
640 ffmpeg_log_level=ffmpeg_log_level,
641 input_params=input_params,
642 output_params=output_params,
643 audio_path=audio_path,
644 audio_codec=audio_codec,
645 )
646
647 # Seed the generator (this is where the ffmpeg subprocess starts)
648 self._write_gen.send(None)
649
650
651class FrameCatcher(threading.Thread):
652 """Thread to keep reading the frame data from stdout. This is
653 useful when streaming from a webcam. Otherwise, if the user code
654 does not grab frames fast enough, the buffer will fill up, leading
655 to lag, and ffmpeg can also stall (experienced on Linux). The
656 get_frame() method always returns the last available image.
657 """
658
659 def __init__(self, gen):
660 self._gen = gen
661 self._frame = None
662 self._frame_is_new = False
663 self._lock = threading.RLock()
664 threading.Thread.__init__(self)
665 self.daemon = True # do not let this thread hold up Python shutdown
666 self._should_stop = False
667 self.start()
668
669 def stop_me(self):
670 self._should_stop = True
671 while self.is_alive():
672 time.sleep(0.001)
673
674 def get_frame(self):
675 while self._frame is None: # pragma: no cover - an init thing
676 time.sleep(0.001)
677 with self._lock:
678 is_new = self._frame_is_new
679 self._frame_is_new = False # reset
680 return self._frame, is_new
681
682 def run(self):
683 # This runs in the worker thread
684 try:
685 while not self._should_stop:
686 time.sleep(0) # give control to other threads
687 frame = self._gen.__next__()
688 with self._lock:
689 self._frame = frame
690 self._frame_is_new = True
691 except (StopIteration, EOFError):
692 pass
693
694
695def parse_device_names(ffmpeg_output):
696 """Parse the output of the ffmpeg -list-devices command"""
697 # Collect device names - get [friendly_name, alt_name] of each
698 device_names = []
699 in_video_devices = False
700 for line in ffmpeg_output.splitlines():
701 if line.startswith("[dshow"):
702 logger.debug(line)
703 line = line.split("]", 1)[1].strip()
704 if in_video_devices and line.startswith('"'):
705 friendly_name = line[1:-1]
706 device_names.append([friendly_name, ""])
707 elif in_video_devices and line.lower().startswith("alternative name"):
708 alt_name = line.split(" name ", 1)[1].strip()[1:-1]
709 if sys.platform.startswith("win"):
710 alt_name = alt_name.replace("&", "^&") # Tested to work
711 else:
712 alt_name = alt_name.replace("&", "\\&") # Does this work?
713 device_names[-1][-1] = alt_name
714 elif "video devices" in line:
715 in_video_devices = True
716 elif "devices" in line:
717 # set False for subsequent "devices" sections
718 in_video_devices = False
719 # Post-process, see #441
720 # prefer friendly names, use alt name if two cams have same friendly name
721 device_names2 = []
722 for friendly_name, alt_name in device_names:
723 if friendly_name not in device_names2:
724 device_names2.append(friendly_name)
725 elif alt_name:
726 device_names2.append(alt_name)
727 else:
728 device_names2.append(friendly_name) # duplicate, but not much we can do
729 return device_names2