trim.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """
  2. Requires PySceneDetect, which in turn requires numpy and opencv compiled with ffmpeg support
  3. steps:
  4. 1) merge the video if necessary
  5. 2) find first scene change
  6. 3) find previous iframe
  7. 4) optionally trim silence from end of video?
  8. 5) cut the video
  9. in the future, do this separately from archive merging, perhaps through a handler running in the Controller
  10. https://superuser.com/questions/554620/how-to-get-time-stamp-of-closest-keyframe-before-a-given-timestamp-with-ffmpeg
  11. ffprobe -select_streams v -show_frames <INPUT>
  12. """
  13. from types import SimpleNamespace
  14. import subprocess
  15. from showroom.archive.probe import get_iframes2
  16. import os.path
  17. from .constants import ffmpeg
  18. from itertools import zip_longest
  19. class DumbNamespace(SimpleNamespace):
  20. def __getattr__(self, name):
  21. try:
  22. return super().__getattr__(name)
  23. except AttributeError:
  24. return None
  25. def detect_first_scene(path, start_minutes=0, end_minutes=12, threshold=20.0):
  26. """
  27. Detect transition from static image into the actual program.
  28. Requires PySceneDetect and OpenCV compiled with FFmpeg support.
  29. :param path: path to file
  30. :param start_minutes: when to start looking
  31. :param end_minutes: when to stop looking
  32. :param threshold: how big a change in frames to detect
  33. :return:
  34. """
  35. import scenedetect
  36. # detect_scenes_file is unfortunately not really designed to be used like this
  37. # it's tightly coupled to the command line arguments passed by scenedetect.cli
  38. # TODO: Rewrite the necessary PySceneDetect functions so they aren't retarded.
  39. # or write my own detector that stops after finding a match, see detect_threshold
  40. scene_detectors = scenedetect.detectors.get_available()
  41. args = DumbNamespace(threshold=threshold,
  42. detection_method='content',
  43. downscale_factor=2,
  44. start_time=[0, start_minutes, 0],
  45. duration=[0, end_minutes, 0],
  46. quiet_mode=True,
  47. # end custom arguments, begin defaults
  48. min_scene_len=15,
  49. frame_skip=0)
  50. scene_manager = scenedetect.manager.SceneManager(args=args, scene_detectors=scene_detectors)
  51. video_fps, frames_read, frames_processed = scenedetect.detect_scenes_file(path, scene_manager)
  52. scene_list_sec = [x / float(video_fps) for x in scene_manager.scene_list]
  53. return scene_list_sec[0]
  54. def detect_start_iframe(path, max_pts_time):
  55. search_interval = '{}%{}'.format(max(0.0, max_pts_time - 60.0), max_pts_time)
  56. iframes = get_iframes2(path, search_interval)
  57. return iframes[-1]
  58. def detect_end_of_video(path, min_pts_time):
  59. # find the
  60. pass
  61. def detect_threshold(path):
  62. # find the ideal threshold to use for content detection
  63. # or alternatively write a different detector that looks at more than just two frames
  64. pass
  65. def trim_video(srcpath, destpath, start_pts_time, end_pts_time=None):
  66. args = [ffmpeg]
  67. if not (start_pts_time is None or int(start_pts_time) == 0):
  68. args.extend(['-ss', str(start_pts_time)])
  69. args.extend(['-i', srcpath])
  70. if end_pts_time:
  71. args.extend(['-to', str(end_pts_time - (start_pts_time if not start_pts_time is None else 0))])
  72. args.extend([
  73. '-c', 'copy',
  74. '-movflags', '+faststart',
  75. '-avoid_negative_ts', 'make_zero',
  76. destpath
  77. ])
  78. print(args)
  79. try:
  80. p = subprocess.Popen(
  81. args,
  82. stdin=subprocess.DEVNULL,
  83. stdout=subprocess.PIPE,
  84. stderr=subprocess.STDOUT,
  85. universal_newlines=True
  86. )
  87. except TypeError:
  88. print(srcpath, destpath, args)
  89. raise
  90. result = p.communicate()
  91. # TODO: parse result?
  92. def time_code_to_seconds(time_code):
  93. """
  94. Converts a time code to seconds.
  95. """
  96. try:
  97. seconds = float(time_code or 0)
  98. except ValueError:
  99. pass
  100. else:
  101. if seconds <= 0:
  102. return None
  103. else:
  104. return seconds
  105. if ':' in time_code:
  106. if time_code.count(':') == 2:
  107. hours, minutes, seconds = time_code.split(':')
  108. elif time_code.count(':') == 1:
  109. minutes, seconds = time_code.split(':')
  110. hours = 0
  111. else:
  112. raise ValueError('Unrecognised time string') # TODO: more testing, or use datetime or whatever that other lib is called
  113. hours = float(hours or 0)
  114. minutes = float(minutes or 0)
  115. seconds = float(seconds or 0)
  116. else:
  117. seconds = float(time_code or 0)
  118. return hours*60*60 + minutes*60 + seconds
  119. def seconds_to_time_code(seconds):
  120. try:
  121. seconds = float(seconds or 0)
  122. except ValueError:
  123. if seconds is None:
  124. return 0
  125. else:
  126. print('Failed to parse seconds value: {}'.format(seconds))
  127. return None
  128. if seconds <= 0:
  129. return None
  130. hours, seconds = seconds//3600, seconds % 3600
  131. minutes, seconds = seconds//60, seconds % 60
  132. seconds, milliseconds = seconds//1, round(seconds % 1 * 1000)
  133. if hours == 0:
  134. if minutes == 0:
  135. intervals = (seconds,)
  136. else:
  137. intervals = (minutes, seconds)
  138. else:
  139. intervals = (hours, minutes, seconds)
  140. return '{}.{:03d}'.format(':'.join(['{:02d}'.format(int(e)) for e in intervals]), milliseconds)
  141. def trim_videos(video_list, output_dir, trim_starts=(), trim_ends=()):
  142. # find start iframe
  143. len(video_list)
  144. args = zip_longest(video_list, trim_starts, trim_ends, fillvalue=None)
  145. for video, trim_start, trim_end in args:
  146. if trim_start:
  147. trim_start = time_code_to_seconds(trim_start)
  148. if trim_end:
  149. trim_end = time_code_to_seconds(trim_end)
  150. start_pts = None
  151. if trim_start:
  152. start_pts = float(detect_start_iframe(video, trim_start))
  153. video_name, video_ext = os.path.split(video)[-1].rsplit('.', 1)
  154. final_video = '{}-[{}-{}].{}'.format(
  155. video_name,
  156. seconds_to_time_code(start_pts) or '0',
  157. seconds_to_time_code(trim_end) or '',
  158. video_ext
  159. )
  160. output_path = os.path.join(output_dir, final_video)
  161. print('Trimming {} from {} -> {}'.format(video, start_pts, output_path))
  162. trim_video(video, output_path, start_pts, trim_end)