ffmpeg.py 29 KB

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