Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/imageio-2.35.1-py3.8.egg/imageio/plugins/ffmpeg.py: 3%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

305 statements  

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