You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 line
4.8 KiB

  1. """Classes for producing videos"""
  2. from abc import ABC
  3. import logging
  4. import subprocess
  5. import tempfile
  6. # for visualisations:
  7. import matplotlib.pyplot as plt
  8. class Producer(ABC):
  9. """Generic producer interface."""
  10. def __init__(self, features):
  11. """All producers should take a list of features as input"""
  12. def produce(self):
  13. """All Producers should produce something!"""
  14. class VideoProducer(Producer):
  15. """Video producer interface."""
  16. class FfmpegVideoProducer(VideoProducer):
  17. """Produce videos using ffmpeg"""
  18. # TODO: consider output filename options
  19. def __init__(self, features):
  20. if not features:
  21. raise ValueError("No features provided")
  22. # TODO: consider if we want to permit empty features (producing no video)
  23. self.features = features
  24. def _ffmpeg_feature_to_clip(self, feature=None, output_filepath=None):
  25. """use ffmpeg to produve a video clip from a feature"""
  26. OVERWRITE = True # TODO: consider making this a config option
  27. if not feature or not feature.interval:
  28. raise ValueError("No feature provided")
  29. if not output_filepath:
  30. raise ValueError("No output filepath provided")
  31. ffmpeg_prefix = ["ffmpeg", "-y"] if OVERWRITE else ["ffmpeg"]
  32. ffmpeg_suffix = ["-r", "60", "-c:v", "libx264", "-crf", "26", "-c:a", "aac", "-preset", "ultrafast"]
  33. # TODO: match framerate of input video
  34. # TODO: adjustable encoding options
  35. seek = ["-ss", str(feature.interval.start)]
  36. duration = ["-t", str(feature.interval.duration)]
  37. ffmpeg_args = ffmpeg_prefix + seek + ["-i"] + [feature.path] + duration + ffmpeg_suffix + [output_filepath]
  38. logging.info(f"ffmpeg_args: {ffmpeg_args}")
  39. subprocess.run(ffmpeg_args, stdout=None, stderr=None)
  40. def _ffmpeg_concat_clips(self, clips=None, output_filepath=None):
  41. """use ffmpeg to concatenate clips into a single video"""
  42. OVERWRITE = True
  43. ffmpeg_prefix = ["ffmpeg"]
  44. ffmpeg_prefix += ["-y"] if OVERWRITE else []
  45. ffmpeg_prefix += ["-f", "concat", "-safe", "0", "-i"]
  46. # there is a method to do this via process substitution, but it's not portable
  47. # so we'll use the input file list method
  48. if not clips:
  49. raise ValueError("No clips provided")
  50. if not output_filepath:
  51. raise ValueError("No output filepath provided")
  52. # generate a temporary file with the list of clips
  53. join_file = tempfile.NamedTemporaryFile(mode="w")
  54. for clip in clips:
  55. join_file.write(f"file '{clip}'\n")
  56. join_file.flush()
  57. ffmpeg_args = ffmpeg_prefix + [join_file.name] + ["-c", "copy", output_filepath]
  58. logging.info(f"ffmpeg_args: {ffmpeg_args}")
  59. subprocess.run(ffmpeg_args, stdout=None, stderr=None)
  60. join_file.close()
  61. def produce(self):
  62. OUTPUT_DIR = "/tmp/" # TODO: make this a config option
  63. clips = []
  64. for num, feature in enumerate(self.features):
  65. output_filepath = f"{OUTPUT_DIR}/highlight_{num}.mp4"
  66. self._ffmpeg_feature_to_clip(feature, output_filepath)
  67. clips.append(output_filepath)
  68. # concatenate the clips
  69. output_filepath = f"{OUTPUT_DIR}/highlights.mp4"
  70. self._ffmpeg_concat_clips(clips, output_filepath)
  71. logging.info(f"Produced video: {output_filepath}")
  72. class VisualisationProducer(Producer):
  73. """Visualisation producer -- illustrate the features we have extracted"""
  74. def __init__(self, features):
  75. if not features:
  76. raise ValueError("No features provided")
  77. self.features = features
  78. def produce(self):
  79. """Produce visualisation"""
  80. # basic idea: use matplotlib to plot:
  81. # - a wide line segment representing the source video[s]
  82. # - shorter line segments representing the features extracted where:
  83. # + width represents duration
  84. # + colour represents feature type
  85. # + position represents time
  86. # - save as image
  87. plotted_source_videos = []
  88. fig, ax = plt.subplots()
  89. for feature in self.features:
  90. # plot source video line if not done already
  91. if feature.source not in plotted_source_videos:
  92. # use video duration as width
  93. # ax.plot([0, feature.source.duration()], [0, 0], color='black', linewidth=10)
  94. ax.broken_barh([(0, feature.source.duration())], (0, 5), facecolors='grey')
  95. plotted_source_videos.append(feature.source)
  96. # plot feature line
  97. # ax.plot([feature.interval.start, feature.interval.end], [1, 1], color='red', linewidth=5)
  98. ax.broken_barh([(feature.interval.start, feature.interval.duration)], (10, 5), facecolors='red')
  99. # save the plot
  100. plt.savefig("/tmp/visualisation.png")
  101. plt.close()