Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

123 rader
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()