Skip to content

Commit

Permalink
Vl 76 ffmpeg gpu acceleration (#88)
Browse files Browse the repository at this point in the history
* test: attempt to trim videos using GPU acceleration

* fix: fixed the ffmpeg preset issue between ffmpeg and nvenc codex

---------

Co-authored-by: Hanxiong Shi <[email protected]>
  • Loading branch information
shihanxiong and Hanxiong Shi authored Apr 23, 2023
1 parent 0586e01 commit fd5d19d
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 31 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### v1.6.1

- adjusted segment modal size
- ffmpeg now uses GPU acceleration instead of CPU

### v1.6.0

Expand Down
Binary file added src/dist/win32_video_loom_1_6_1.exe
Binary file not shown.
11 changes: 11 additions & 0 deletions src/test_video_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from video_utils import VideoUtils


def test_get_ffmpeg_preset_value_for_nvenc_h264():
assert VideoUtils.get_ffmpeg_preset_value_for_nvenc_h264("faster") == "fast"
assert VideoUtils.get_ffmpeg_preset_value_for_nvenc_h264("veryslow") == "slow"
assert VideoUtils.get_ffmpeg_preset_value_for_nvenc_h264("medium") == "medium"
assert (
VideoUtils.get_ffmpeg_preset_value_for_nvenc_h264("not-a-valie-preset")
== "medium"
)
77 changes: 46 additions & 31 deletions src/video_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from timeline_utils import TimelineUtils
from audio_utils import AudioUtils
from sys_utils import SysUtils
from video_utils import VideoUtils


# videos input
Expand All @@ -24,10 +25,12 @@ def __init__(self, container, **args):
self.video_list = []
self.trimmed_video_list = []
self.video_label_text = tk.StringVar(
value=f"Videos {len(self.video_list)} of 4")
value=f"Videos {len(self.video_list)} of 4"
)
self.output_directory = os.getcwd()
self.output_file_name = tk.StringVar(
value=f"{TimeUtils.get_current_timestamp()}.mp4")
value=f"{TimeUtils.get_current_timestamp()}.mp4"
)
self.output_width = 0
self.output_height = 0
self.is_filename_escaped = False
Expand All @@ -44,13 +47,11 @@ def __init__(self, container, **args):
self.columnconfigure(c_idx, weight=1)

# video label
video_label = ttk.Label(
self, textvariable=self.video_label_text, padding=(10))
video_label = ttk.Label(self, textvariable=self.video_label_text, padding=(10))
video_label.grid(row=0, columnspan=4, sticky="N")

# video rendering
self.video_renderer_component = VideoRendererFrame(
self, padding=(10, 0))
self.video_renderer_component = VideoRendererFrame(self, padding=(10, 0))
self.video_renderer_component.grid(row=2, columnspan=4, sticky="NEWS")

# video import / clear
Expand All @@ -65,12 +66,13 @@ def __init__(self, container, **args):
self.components = [
self.video_import_component,
self.video_renderer_component,
self.video_control_component
self.video_control_component,
]

def refresh(self):
self.video_label_text.set(
f"Videos {len(self.video_list)} of {self.max_num_of_videos}")
f"Videos {len(self.video_list)} of {self.max_num_of_videos}"
)
logging.debug(f"imported videos {self.video_list}")

for component in self.components:
Expand All @@ -94,7 +96,8 @@ def generate_video_with_ffmpeg(self):
start_time = datetime.now()
logging.info("kicking off video processing, hang tight")
logging.info(
f'using audio track {self.master.settings_component.audio_setting_component.get_audio_track()}')
f"using audio track {self.master.settings_component.audio_setting_component.get_audio_track()}"
)
logging.info("================timeline start================")
logging.info(self.master.timeline_component.get_timeline_text())
logging.info("================timeline end==================")
Expand All @@ -104,19 +107,25 @@ def generate_video_with_ffmpeg(self):
# validate timeline
timeline_utils = TimelineUtils()
error = timeline_utils.validate_timeline(
self.master.timeline_component.get_timeline_text())
self.master.timeline_component.get_timeline_text()
)

if error is None:
self.master.status_component.set_and_log_status(
"timeline validated")
self.master.status_component.set_and_log_status("timeline validated")
else:
self.master.status_component.set_and_log_status(error)
return

# determine processing speed
self.ffmpeg_preset_arg = f"-preset {self.master.settings_component.video_setting_component.get_ffmpeg_preset_value()}"
ffmpeg_preset_value = (
self.master.settings_component.video_setting_component.get_ffmpeg_preset_value()
)
self.ffmpeg_preset_arg = f"-preset {ffmpeg_preset_value}"
self.ffmpeg_nvenc_preset_arg = f"-preset {VideoUtils.get_ffmpeg_preset_value_for_nvenc_h264(ffmpeg_preset_value)}"

self.master.status_component.set_and_log_status(
f"Setting processing speed to be {self.master.settings_component.video_setting_component.get_ffmpeg_preset_value()}")
f"Setting processing speed to be {self.master.settings_component.video_setting_component.get_ffmpeg_preset_value()}"
)

# trim videos
self.process_trimmed_videos()
Expand All @@ -131,13 +140,15 @@ def generate_video_with_ffmpeg(self):
self.finalize_video(output_file, output_sound)
except Exception as err:
self.master.status_component.set_and_log_status(
"An error occurred while generating video :(")
"An error occurred while generating video :("
)
logging.error(f"{self.__class__.__name__}: {str(err)}")

# logging
end_time = datetime.now()
self.master.status_component.set_and_log_status(
f"video is ready! Taking total of {round((end_time - start_time).total_seconds(), 2)} seconds")
f"video is ready! Taking total of {round((end_time - start_time).total_seconds(), 2)} seconds"
)
self.master.app_refresh()

def calculate_output_resolutions(self):
Expand All @@ -153,22 +164,22 @@ def calculate_output_resolutions(self):
self.output_height = max(self.output_height, int(height))

self.master.status_component.set_and_log_status(
f"output resolution is determined at {self.output_width} x {self.output_height}")
f"output resolution is determined at {self.output_width} x {self.output_height}"
)

def process_trimmed_videos(self):
timeline_utils = TimelineUtils()
parsed_timeline_arr = timeline_utils.parse_timeline(
self.master.timeline_component.get_timeline_text())
self.master.timeline_component.get_timeline_text()
)

for idx, timeline in enumerate(parsed_timeline_arr):
video, start, end = timeline
trimmed_output = os.path.join(
self.output_directory, f"trimmed_{idx}.mp4")
trimmed_output = os.path.join(self.output_directory, f"trimmed_{idx}.mp4")
self.trimmed_video_list.append(trimmed_output)
cmd = f"ffmpeg -i {self.video_list[int(video) - 1]} -ss {start} -to {end} -vf scale={self.output_width}:{self.output_height} -c:a copy {self.ffmpeg_preset_arg} {FileUtils.escape_file_name(trimmed_output)}"
cmd = f"ffmpeg -hwaccel cuvid -c:v h264_cuvid -i {self.video_list[int(video) - 1]} -c:v h264_nvenc -ss {start} -to {end} -vf scale_cuda={self.output_width}:{self.output_height} -c:a copy {self.ffmpeg_nvenc_preset_arg} {FileUtils.escape_file_name(trimmed_output)}"
subprocess.check_output(cmd, shell=True)
self.master.status_component.set_and_log_status(
"completed trimming videos")
self.master.status_component.set_and_log_status("completed trimming videos")

def concatenate_trimmed_videos(self):
output_file = os.path.join(self.output_directory, "output.mp4")
Expand All @@ -185,25 +196,29 @@ def concatenate_trimmed_videos(self):
cmd = f"ffmpeg {input_args} -filter_complex {ffmpeg_filter} -c:v libx264 -crf 23 -y -vsync 2 {self.ffmpeg_preset_arg} {FileUtils.escape_file_name(output_file)}"
subprocess.check_output(cmd, shell=True)
self.master.status_component.set_and_log_status(
"completed concatenating trimmed videos")
"completed concatenating trimmed videos"
)

return output_file

def process_audio(self):
output_sound = os.path.join(self.output_directory, "audio.aac")
input_video = self.video_list[self.master.settings_component.audio_setting_component.get_audio_track(
) - 1]
input_video = self.video_list[
self.master.settings_component.audio_setting_component.get_audio_track() - 1
]
AudioUtils.generate_aac_from_mp4(
input_video, output_sound, self.ffmpeg_preset_arg)
input_video, output_sound, self.ffmpeg_preset_arg
)
self.master.status_component.set_and_log_status(
f"completed processing {output_sound} from video {input_video}")
f"completed processing {output_sound} from video {input_video}"
)

return output_sound

def finalize_video(self, output_file, output_sound):
final_file = os.path.join(
self.output_directory, self.output_file_name.get())
final_file = os.path.join(self.output_directory, self.output_file_name.get())
cmd = f"ffmpeg -i {FileUtils.escape_file_name(output_file)} -i {FileUtils.escape_file_name(output_sound)} -map 0:v -map 1:a -c copy -shortest -y -vsync 2 {self.ffmpeg_preset_arg} {FileUtils.escape_file_name(final_file)}"
subprocess.check_output(cmd, shell=True)
self.master.status_component.set_and_log_status(
"video is processed and ready for use")
"video is processed and ready for use"
)
30 changes: 30 additions & 0 deletions src/video_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class VideoUtils:
def __init__(self):
pass

@staticmethod
def get_ffmpeg_preset_value_for_nvenc_h264(ffmpeg_preset_value):
"""
this method is used to translate FFMPEG presets into nvenc encoder presets
when using CPU encoding, ffmpeg allows presets to be:
'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'
when using GPU encoding, ffmpeg nvenc_h264 allows presets to be:
'slow', 'medium', 'fast'
"""
if (
ffmpeg_preset_value == "ultrafast"
or ffmpeg_preset_value == "superfast"
or ffmpeg_preset_value == "veryfast"
or ffmpeg_preset_value == "faster"
or ffmpeg_preset_value == "fast"
):
return "fast"
elif (
ffmpeg_preset_value == "slow"
or ffmpeg_preset_value == "slower"
or ffmpeg_preset_value == "veryslow"
):
return "slow"
else:
return "medium"

0 comments on commit fd5d19d

Please sign in to comment.