|
17 | 17 |
|
18 | 18 | import math |
19 | 19 | import queue |
| 20 | +import sys |
20 | 21 | import threading |
21 | 22 | import typing as ty |
22 | 23 | from dataclasses import dataclass |
@@ -300,70 +301,115 @@ def run( |
300 | 301 | total=len(scene_list) * self._num_images, unit="images", dynamic_ncols=True |
301 | 302 | ) |
302 | 303 |
|
| 304 | + timecode_list = self.generate_timecode_list(scene_list) |
| 305 | + image_filenames = {i: [] for i in range(len(timecode_list))} |
| 306 | + |
303 | 307 | filename_template = Template(self._image_name_template) |
| 308 | + logger.debug("Writing images with template %s", filename_template.template) |
304 | 309 | scene_num_format = "%0" |
305 | 310 | scene_num_format += str(max(3, math.floor(math.log(len(scene_list), 10)) + 1)) + "d" |
306 | 311 | image_num_format = "%0" |
307 | 312 | image_num_format += str(math.floor(math.log(self._num_images, 10)) + 2) + "d" |
308 | 313 |
|
309 | | - timecode_list = self.generate_timecode_list(scene_list) |
310 | | - image_filenames = {i: [] for i in range(len(timecode_list))} |
311 | | - logger.debug("Writing images with template %s", filename_template.template) |
| 314 | + def format_filename(scene_number: int, image_number: int, image_timecode: FrameTimecode): |
| 315 | + return "%s.%s" % ( |
| 316 | + filename_template.safe_substitute( |
| 317 | + VIDEO_NAME=video.name, |
| 318 | + SCENE_NUMBER=scene_num_format % (scene_number + 1), |
| 319 | + IMAGE_NUMBER=image_num_format % (image_number + 1), |
| 320 | + FRAME_NUMBER=image_timecode.get_frames(), |
| 321 | + TIMESTAMP_MS=int(image_timecode.get_seconds() * 1000), |
| 322 | + TIMECODE=image_timecode.get_timecode().replace(":", ";"), |
| 323 | + ), |
| 324 | + self._image_extension, |
| 325 | + ) |
312 | 326 |
|
313 | 327 | MAX_QUEUED_ENCODE_FRAMES = 4 |
314 | 328 | MAX_QUEUED_SAVE_IMAGES = 4 |
315 | 329 | encode_queue = queue.Queue(MAX_QUEUED_ENCODE_FRAMES) |
316 | 330 | save_queue = queue.Queue(MAX_QUEUED_SAVE_IMAGES) |
317 | | - encode_thread = threading.Thread( |
318 | | - target=self._image_encode_thread, |
319 | | - args=(video, encode_queue, save_queue, self._image_extension), |
320 | | - daemon=True, |
321 | | - ) |
322 | | - save_thread = threading.Thread( |
323 | | - target=self._save_files_thread, |
324 | | - args=(save_queue, progress_bar), |
325 | | - daemon=True, |
| 331 | + error_queue = queue.Queue(2) # Queue size must be the same as the # of worker threads! |
| 332 | + |
| 333 | + def check_error_queue(): |
| 334 | + try: |
| 335 | + return error_queue.get(block=False) |
| 336 | + except queue.Empty: |
| 337 | + pass |
| 338 | + return None |
| 339 | + |
| 340 | + def launch_thread(callable, *args, **kwargs): |
| 341 | + def capture_errors(callable, *args, **kwargs): |
| 342 | + try: |
| 343 | + return callable(*args, **kwargs) |
| 344 | + # Errors we capture in `error_queue` will be re-raised by this thread. |
| 345 | + except: # noqa: E722 |
| 346 | + error_queue.put(sys.exc_info()) |
| 347 | + |
| 348 | + thread = threading.Thread( |
| 349 | + target=capture_errors, |
| 350 | + args=( |
| 351 | + callable, |
| 352 | + *args, |
| 353 | + ), |
| 354 | + kwargs=kwargs, |
| 355 | + daemon=True, |
| 356 | + ) |
| 357 | + thread.start() |
| 358 | + return thread |
| 359 | + |
| 360 | + def checked_put(work_queue: queue.Queue, item: ty.Any): |
| 361 | + error = None |
| 362 | + while True: |
| 363 | + try: |
| 364 | + work_queue.put(item, timeout=0.1) |
| 365 | + break |
| 366 | + except queue.Full: |
| 367 | + error = check_error_queue() |
| 368 | + if error is None: |
| 369 | + continue |
| 370 | + if error is not None: |
| 371 | + raise error[1].with_traceback(error[2]) |
| 372 | + |
| 373 | + encode_thread = launch_thread( |
| 374 | + self._encode_images, |
| 375 | + video, |
| 376 | + encode_queue, |
| 377 | + save_queue, |
| 378 | + self._image_extension, |
326 | 379 | ) |
327 | | - encode_thread.start() |
328 | | - save_thread.start() |
| 380 | + save_thread = launch_thread(self._save_images, save_queue, progress_bar) |
329 | 381 |
|
330 | 382 | for i, scene_timecodes in enumerate(timecode_list): |
331 | | - for j, image_timecode in enumerate(scene_timecodes): |
332 | | - video.seek(image_timecode) |
| 383 | + for j, timecode in enumerate(scene_timecodes): |
| 384 | + video.seek(timecode) |
333 | 385 | frame_im = video.read() |
334 | 386 | if frame_im is not None and frame_im is not False: |
335 | | - # TODO: Add extension to template. |
336 | | - # TODO: Allow NUM to be a valid suffix in addition to NUMBER. |
337 | | - file_path = "%s.%s" % ( |
338 | | - filename_template.safe_substitute( |
339 | | - VIDEO_NAME=video.name, |
340 | | - SCENE_NUMBER=scene_num_format % (i + 1), |
341 | | - IMAGE_NUMBER=image_num_format % (j + 1), |
342 | | - FRAME_NUMBER=image_timecode.get_frames(), |
343 | | - TIMESTAMP_MS=int(image_timecode.get_seconds() * 1000), |
344 | | - TIMECODE=image_timecode.get_timecode().replace(":", ";"), |
345 | | - ), |
346 | | - self._image_extension, |
347 | | - ) |
| 387 | + file_path = format_filename(i, j, timecode) |
348 | 388 | image_filenames[i].append(file_path) |
349 | | - encode_queue.put((frame_im, get_and_create_path(file_path, output_dir))) |
| 389 | + checked_put( |
| 390 | + encode_queue, (frame_im, get_and_create_path(file_path, output_dir)) |
| 391 | + ) |
350 | 392 | else: |
351 | 393 | completed = False |
352 | 394 | break |
353 | 395 |
|
354 | | - # *WARNING*: We do not handle errors or exceptions yet, and this can deadlock on errors! |
355 | | - encode_queue.put((None, None)) |
356 | | - save_queue.put((None, None)) |
| 396 | + checked_put(encode_queue, (None, None)) |
| 397 | + checked_put(save_queue, (None, None)) |
357 | 398 | encode_thread.join() |
358 | 399 | save_thread.join() |
| 400 | + |
| 401 | + error = check_error_queue() |
| 402 | + if error is not None: |
| 403 | + raise error[1].with_traceback(error[2]) |
| 404 | + |
359 | 405 | if progress_bar is not None: |
360 | 406 | progress_bar.close() |
361 | 407 | if not completed: |
362 | 408 | logger.error("Could not generate all output images.") |
363 | 409 |
|
364 | 410 | return image_filenames |
365 | 411 |
|
366 | | - def _image_encode_thread( |
| 412 | + def _encode_images( |
367 | 413 | self, |
368 | 414 | video: VideoStream, |
369 | 415 | encode_queue: queue.Queue, |
@@ -393,7 +439,7 @@ def _image_encode_thread( |
393 | 439 | continue |
394 | 440 | save_queue.put((encoded, dest_path)) |
395 | 441 |
|
396 | | - def _save_files_thread(self, save_queue: queue.Queue, progress_bar: tqdm): |
| 442 | + def _save_images(self, save_queue: queue.Queue, progress_bar: tqdm): |
397 | 443 | while True: |
398 | 444 | encoded, dest_path = save_queue.get() |
399 | 445 | if encoded is None: |
|
0 commit comments