From 4728787494fdd9ad98a026940f27d191e6c81762 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 13 Jan 2021 10:23:28 -0600 Subject: [PATCH 01/50] Beta Version 4.1.1b0 (#175) * Fixing #156 Copy was broken due to copy settings being renamed due to library update (thanks to leonardyan) * Fixing #172 FastFlix could not set profile setting copy (thanks to Etz) * Fixing calling delete on a profile without a source video could crash FastFlix --- CHANGES | 6 ++++++ fastflix/encoders/copy/settings_panel.py | 2 +- fastflix/models/config.py | 6 +++++- fastflix/version.py | 2 +- fastflix/widgets/panels/advanced_panel.py | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 9eb65963..1dfcc7e0 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,11 @@ # Changelog +## Version 4.1.1 + +* Fixing #156 Copy was broken due to copy settings being renamed due to library update (thanks to leonardyan) +* Fixing #172 FastFlix could not set profile setting copy (thanks to Etz) +* Fixing calling delete on a profile without a source video could crash FastFlix + ## Version 4.1.0 * Adding #118 #126 advanced panel with FFmpeg filters (thanks to Marco Ravich and remlap) diff --git a/fastflix/encoders/copy/settings_panel.py b/fastflix/encoders/copy/settings_panel.py index e08002fe..5b37b2fb 100644 --- a/fastflix/encoders/copy/settings_panel.py +++ b/fastflix/encoders/copy/settings_panel.py @@ -12,7 +12,7 @@ class Copy(SettingPanel): - profile_name = "copy" + profile_name = "copy_settings" def __init__(self, parent, main, app: FastFlixApp): super().__init__(parent, main, app) diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 950a9ffd..88e4ca3f 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -3,7 +3,7 @@ import shutil from distutils.version import StrictVersion from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional from appdirs import user_data_dir from box import Box, BoxError @@ -42,6 +42,8 @@ "copy_settings": CopySettings, } +outdated_settings = ("copy",) + class Profile(BaseModel): auto_crop: bool = False @@ -193,6 +195,8 @@ def load(self): continue profile = Profile() for setting_name, setting in v.items(): + if setting_name in outdated_settings: + continue if setting_name in setting_types.keys() and setting is not None: try: setattr(profile, setting_name, setting_types[setting_name](**setting)) diff --git a/fastflix/version.py b/fastflix/version.py index 197c9448..6722f42c 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "4.1.0" +__version__ = "4.1.1b0" __author__ = "Chris Griffith" diff --git a/fastflix/widgets/panels/advanced_panel.py b/fastflix/widgets/panels/advanced_panel.py index 1629f4c7..57c1559e 100644 --- a/fastflix/widgets/panels/advanced_panel.py +++ b/fastflix/widgets/panels/advanced_panel.py @@ -370,7 +370,7 @@ def vbv_check_changed(self): # (800 + 140) - 1080 == -140 def update_settings(self): - if self.updating: + if self.updating or not self.app.fastflix.current_video: return False self.updating = True self.app.fastflix.current_video.video_settings.video_speed = video_speeds[self.video_speed_widget.currentText()] From 621565494a01c943f155d9fc86f549fce56658ec Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 13 Jan 2021 12:42:44 -0600 Subject: [PATCH 02/50] version bump --- fastflix/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastflix/version.py b/fastflix/version.py index 6722f42c..31176207 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "4.1.1b0" +__version__ = "4.1.1" __author__ = "Chris Griffith" From 6866bc367289d3e753a002c53824c005aabec9ca Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 13 Jan 2021 17:52:12 -0600 Subject: [PATCH 03/50] * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) --- CHANGES | 4 ++++ fastflix/models/config.py | 1 + fastflix/version.py | 2 +- fastflix/widgets/main.py | 6 ++++-- fastflix/widgets/settings.py | 16 +++++++++++++++- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 1dfcc7e0..5a2a9da9 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,9 @@ # Changelog +## Version 4.2.0 + +* Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) + ## Version 4.1.1 * Fixing #156 Copy was broken due to copy settings being renamed due to library update (thanks to leonardyan) diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 88e4ca3f..2e1ecac3 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -121,6 +121,7 @@ class Config(BaseModel): flat_ui: bool = True language: str = "en" logging_level: int = 10 + crop_detect_points: int = 10 continue_on_failure: bool = True work_path: Path = fastflix_folder use_sane_audio: bool = True diff --git a/fastflix/version.py b/fastflix/version.py index 31176207..40af567d 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "4.1.1" +__version__ = "4.2.0b0" __author__ = "Chris Griffith" diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index ae51454a..a8caa3e4 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -781,7 +781,9 @@ def get_auto_crop(self): start_pos = self.start_time or self.app.fastflix.current_video.duration // 10 - blocks = math.ceil((self.app.fastflix.current_video.duration - start_pos) / 5) + blocks = math.ceil( + (self.app.fastflix.current_video.duration - start_pos) / (self.app.fastflix.config.crop_detect_points + 1) + ) if blocks < 1: blocks = 1 @@ -789,7 +791,7 @@ def get_auto_crop(self): x for x in range(int(start_pos), int(self.app.fastflix.current_video.duration), blocks) if x < self.app.fastflix.current_video.duration - ][:4] + ][: self.app.fastflix.config.crop_detect_points] if not times: return diff --git a/fastflix/widgets/settings.py b/fastflix/widgets/settings.py index 1137406b..50fac9c8 100644 --- a/fastflix/widgets/settings.py +++ b/fastflix/widgets/settings.py @@ -17,6 +17,7 @@ language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower()) known_language_list = ["English", "Chinese", "Italian", "French", "Spanish", "German"] +possible_detect_points = ["1", "2", "4", "6", "8", "10", "15", "20", "25", "50", "100"] # "Japanese", "Korean", "Hindi", "Russian", "Portuguese" @@ -105,18 +106,30 @@ def __init__(self, app: FastFlixApp, main, *args, **kwargs): self.flat_ui = QtWidgets.QCheckBox(t("Flat UI")) self.flat_ui.setChecked(self.app.fastflix.config.flat_ui) + self.crop_detect_points_widget = QtWidgets.QComboBox() + self.crop_detect_points_widget.addItems(possible_detect_points) + + try: + self.crop_detect_points_widget.setCurrentIndex( + possible_detect_points.index(str(self.app.fastflix.config.crop_detect_points)) + ) + except ValueError: + self.crop_detect_points_widget.setCurrentIndex(5) + layout.addWidget(self.use_sane_audio, 7, 0, 1, 2) layout.addWidget(self.disable_version_check, 8, 0, 1, 2) layout.addWidget(QtWidgets.QLabel(t("GUI Logging Level")), 9, 0) layout.addWidget(self.logger_level_widget, 9, 1) layout.addWidget(self.flat_ui, 10, 0, 1, 2) + layout.addWidget(QtWidgets.QLabel(t("Crop Detect Points")), 11, 0, 1, 1) + layout.addWidget(self.crop_detect_points_widget, 11, 1, 1, 1) button_layout = QtWidgets.QHBoxLayout() button_layout.addStretch() button_layout.addWidget(cancel) button_layout.addWidget(save) - layout.addLayout(button_layout, 11, 0, 1, 3) + layout.addLayout(button_layout, 12, 0, 1, 3) self.setLayout(layout) @@ -153,6 +166,7 @@ def save(self): log_level = (self.logger_level_widget.currentIndex() + 1) * 10 self.app.fastflix.config.logging_level = log_level logger.setLevel(log_level) + self.app.fastflix.config.crop_detect_points = int(self.crop_detect_points_widget.currentText()) self.main.config_update() self.app.fastflix.config.save() From 272f98ce101ea47802ae2bf1958ab4140e4c55d3 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 14 Jan 2021 14:38:10 -0600 Subject: [PATCH 04/50] * Fixing #180 Minor UI glitch, custom bitrate retains "k" when edited from queue (thanks to Etz) --- CHANGES | 1 + fastflix/encoders/common/setting_panel.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 5a2a9da9..6c636eb0 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,7 @@ ## Version 4.2.0 * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) +* Fixing #180 Minor UI glitch, custom bitrate retains "k" when edited from queue (thanks to Etz) ## Version 4.1.1 diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index 6a537144..5acfb0ee 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -369,7 +369,7 @@ def reload(self): break else: self.widgets.bitrate.setCurrentText("Custom") - self.widgets.custom_bitrate.setText(bitrate) + self.widgets.custom_bitrate.setText(bitrate.rstrip("k")) else: self.qp_radio.setChecked(True) self.bitrate_radio.setChecked(False) From d4448896e0b9f72393e61088ed259eeb476ec971 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 14 Jan 2021 15:37:50 -0600 Subject: [PATCH 05/50] * Adding ability to extract HDR10+ metadata * Fixing custom QP crashes FastFlix (thanks to J-mas) --- CHANGES | 2 + fastflix/encoders/common/setting_panel.py | 2 +- fastflix/encoders/hevc_x265/settings_panel.py | 44 ++++++++- fastflix/flix.py | 50 +++++++++- fastflix/models/config.py | 2 + fastflix/models/video.py | 1 + fastflix/widgets/background_tasks.py | 94 ++++++++++--------- fastflix/widgets/main.py | 4 +- 8 files changed, 149 insertions(+), 50 deletions(-) diff --git a/CHANGES b/CHANGES index 6c636eb0..5972743d 100644 --- a/CHANGES +++ b/CHANGES @@ -2,8 +2,10 @@ ## Version 4.2.0 +* Adding ability to extract HDR10+ metadata * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Fixing #180 Minor UI glitch, custom bitrate retains "k" when edited from queue (thanks to Etz) +* Fixing custom QP crashes FastFlix (thanks to J-mas) ## Version 4.1.1 diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index 5acfb0ee..a2aa35ad 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -404,7 +404,7 @@ def get_mode_settings(self) -> Tuple[str, Union[float, int, str]]: if not custom_value: logger.error("No value provided for custom QP/CRF value, defaulting to 30") return "qp", 30 - custom_value = float(self.widgets.custom_crf.text().rstrip(".")) + custom_value = float(self.widgets[f"custom_{self.qp_name}"].text().rstrip(".")) if custom_value.is_integer(): custom_value = int(custom_value) return "qp", custom_value diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index a88ec094..c104992b 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -10,8 +10,9 @@ from fastflix.language import t from fastflix.models.encode import x265Settings from fastflix.models.fastflix_app import FastFlixApp -from fastflix.resources import warning_icon +from fastflix.resources import warning_icon, loading_movie from fastflix.shared import link +from fastflix.widgets.background_tasks import ExtractHDR10 logger = logging.getLogger("fastflix") @@ -63,6 +64,7 @@ def get_breaker(): class HEVC(SettingPanel): profile_name = "x265" + hdr10plus_signal = QtCore.Signal(str) def __init__(self, parent, main, app: FastFlixApp): super().__init__(parent, main, app) @@ -73,6 +75,7 @@ def __init__(self, parent, main, app: FastFlixApp): self.mode = "CRF" self.updating_settings = False + self.extract_thread = None grid.addLayout(self.init_preset(), 0, 0, 1, 2) grid.addLayout(self.init_tune(), 1, 0, 1, 2) @@ -127,6 +130,7 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addWidget(guide_label, 12, 0, 1, 6) + self.hdr10plus_signal.connect(self.done_hdr10plus_extract) self.setLayout(grid) self.hide() @@ -151,10 +155,39 @@ def init_dhdr10_warning_and_opt(self): icon = QtGui.QIcon(warning_icon) label.setPixmap(icon.pixmap(22)) layout = QtWidgets.QHBoxLayout() + + self.extract_button = QtWidgets.QPushButton(t("Extract HDR10+")) + self.extract_button.hide() + self.extract_button.clicked.connect(self.extract_hdr10plus) + + self.extract_label = QtWidgets.QLabel(self) + self.extract_label.hide() + self.movie = QtGui.QMovie(loading_movie) + self.movie.setScaledSize(QtCore.QSize(25, 25)) + self.extract_label.setMovie(self.movie) + + layout.addWidget(self.extract_button) + layout.addWidget(self.extract_label) + layout.addWidget(label) layout.addLayout(self.init_dhdr10_opt()) return layout + def extract_hdr10plus(self): + self.extract_button.hide() + self.extract_label.show() + self.movie.start() + # self.extracting_hdr10 = True + self.extract_thrad = ExtractHDR10(self.app, self.main, signal=self.hdr10plus_signal) + self.extract_thrad.start() + + def done_hdr10plus_extract(self, metadata: str): + self.extract_button.show() + self.extract_label.hide() + self.movie.stop() + if Path(metadata).exists(): + self.widgets.hdr10plus_metadata.setText(metadata) + def init_x265_row(self): layout = QtWidgets.QHBoxLayout() layout.addLayout(self.init_hdr10()) @@ -511,6 +544,15 @@ def hdr_opts(): def new_source(self): super().new_source() self.setting_change() + if self.app.fastflix.current_video.hdr10_plus: + self.extract_button.show() + else: + self.extract_button.hide() + if self.extract_thread: + try: + self.extract_thread.terminate() + except Exception: + pass def update_video_encoder_settings(self): if not self.app.fastflix.current_video: diff --git a/fastflix/flix.py b/fastflix/flix.py index a7a7de90..5a16aeed 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -3,7 +3,7 @@ import os import re from pathlib import Path -from subprocess import PIPE, CompletedProcess, TimeoutExpired, run +from subprocess import PIPE, CompletedProcess, TimeoutExpired, run, Popen from typing import List, Tuple, Union import reusables @@ -443,3 +443,51 @@ def parse_hdr_details(app: FastFlixApp, **_): else: app.fastflix.current_video.master_display = master_display app.fastflix.current_video.cll = cll + + +def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): + if ( + not app.fastflix.current_video.master_display + or not config.hdr10plus_parser + or not config.hdr10plus_parser.exists() + ): + return + + process = Popen( + [ + config.ffmpeg, + "-y", + "-i", + unixy(app.fastflix.current_video.source), + "-map", + f"0:v", + "-loglevel", + "panic", + "-c:v", + "copy", + "-vbsf", + "hevc_mp4toannexb", + "-f", + "hevc", + "-", + ], + stdout=PIPE, + stderr=PIPE, + stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc + ) + + process_two = Popen( + [config.hdr10plus_parser, "--verify", "-"], + stdout=PIPE, + stderr=PIPE, + stdin=process.stdout, + encoding="utf-8", + ) + + try: + stdout, stderr = process_two.communicate() + except Exception: + logger.exception("Unexpected error while trying to detect HDR10+ metdata") + else: + if "Dynamic HDR10+ metadata detected." in stdout: + app.fastflix.current_video.hdr10_plus = True diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 2e1ecac3..db08edd4 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -118,6 +118,8 @@ class Config(BaseModel): config_path: Path = fastflix_folder / "fastflix.yaml" ffmpeg: Path = Field(default_factory=lambda: find_ffmpeg_file("ffmpeg")) ffprobe: Path = Field(default_factory=lambda: find_ffmpeg_file("ffprobe")) + hdr10plus_parser: Path = Field(default_factory=lambda: Path(shutil.which("hdr10plus_parser"))) + mkvpropedit: Path = Field(default_factory=lambda: Path(shutil.which("mkvpropedit"))) flat_ui: bool = True language: str = "en" logging_level: int = 10 diff --git a/fastflix/models/video.py b/fastflix/models/video.py index ef5fd043..d79aa87c 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -93,6 +93,7 @@ class Video(BaseModel): # HDR10 Details master_display: Box = None cll: str = "" + hdr10_plus: bool = False video_settings: VideoSettings = Field(default_factory=VideoSettings) status: Status = Field(default_factory=Status) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 5c31fedb..44082d45 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -108,49 +108,51 @@ def run(self): self.signal.emit() -# class ExtractHDR10(QtCore.QThread): -# def __init__(self, app: FastFlixApp, main, index, signal): -# super().__init__(main) -# self.main = main -# self.app = app -# self.index = index -# self.signal = signal -# -# def run(self): -# # VERIFY ffmpeg -loglevel panic -i input.mkv -c:v copy -vbsf hevc_mp4toannexb -f hevc - | hdr10plus_parser --verify - -# -# self.main.thread_logging_signal.emit(f'INFO:{t("Extracting HDR10+ metadata")}') -# -# process = Popen( -# [ -# self.app.fastflix.config.ffmpeg, -# "-y", -# "-i", -# self.main.input_video, -# "-map", -# f"0:{self.index}", -# "-loglevel", -# "panic", -# "-c:v", -# "copy", -# "-vbsf", -# "hevc_mp4toannexb", -# "-f", -# "hevc", -# "-" -# ], -# stdout=PIPE, -# stderr=PIPE, -# stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc -# ) -# -# process_two = Popen( -# ["hdr10plus_parser", "--verify", "-"], -# stdout=PIPE, -# stderr=PIPE, -# stdin=self.process.stdout, -# encoding="utf-8", -# ) -# -# stdout, stderr = process_two.communicate() -# +class ExtractHDR10(QtCore.QThread): + def __init__(self, app: FastFlixApp, main, signal): + super().__init__(main) + self.main = main + self.app = app + self.signal = signal + + def run(self): + + output = self.app.fastflix.current_video.work_path / "metadata.json" + + self.main.thread_logging_signal.emit(f'INFO:{t("Extracting HDR10+ metadata")} to {output}') + + process = Popen( + [ + self.app.fastflix.config.ffmpeg, + "-y", + "-i", + str(self.app.fastflix.current_video.source).replace("\\", "/"), + "-map", + f"0:{self.app.fastflix.current_video.video_settings.selected_track}", + "-loglevel", + "panic", + "-c:v", + "copy", + "-vbsf", + "hevc_mp4toannexb", + "-f", + "hevc", + "-", + ], + stdout=PIPE, + stderr=PIPE, + stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc + ) + + process_two = Popen( + ["hdr10plus_parser", "-o", str(output).replace("\\", "/"), "-"], + stdout=PIPE, + stderr=PIPE, + stdin=process.stdout, + encoding="utf-8", + cwd=str(self.app.fastflix.current_video.work_path), + ) + + stdout, stderr = process_two.communicate() + self.main.thread_logging_signal.emit(f"DEBUG: HDR10+ Extract: {stdout}") + self.signal.emit(str(output)) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index a8caa3e4..645dd1e2 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -27,6 +27,7 @@ get_auto_crop, parse, parse_hdr_details, + detect_hdr10_plus, ) from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp @@ -1124,8 +1125,9 @@ def update_video_info(self): tasks = [ Task(t("Parse Video details"), parse), Task(t("Extract covers"), extract_attachments), - Task(t("Determine HDR details"), parse_hdr_details), Task(t("Detecting Interlace"), detect_interlaced, dict(source=self.input_video)), + Task(t("Determine HDR details"), parse_hdr_details), + Task(t("Detect HDR10+"), detect_hdr10_plus), ] try: From f113bd522c2ef872ed0b7fba131b0640389c2aec Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 26 Jan 2021 18:23:31 -0600 Subject: [PATCH 06/50] * Adding Windows 10 toast style notification for queue complete success --- .github/workflows/build_windows.yaml | 2 +- CHANGES | 1 + FastFlix_Windows_Installer.spec | 2 +- FastFlix_Windows_OneFile.spec | 2 +- fastflix/encoders/common/helpers.py | 1 - fastflix/entry.py | 1 - fastflix/models/base.py | 35 ---------------------------- fastflix/widgets/main.py | 15 ++++++++++-- 8 files changed, 17 insertions(+), 42 deletions(-) delete mode 100644 fastflix/models/base.py diff --git a/.github/workflows/build_windows.yaml b/.github/workflows/build_windows.yaml index 1059c0d5..0aec98d3 100644 --- a/.github/workflows/build_windows.yaml +++ b/.github/workflows/build_windows.yaml @@ -30,7 +30,7 @@ jobs: shell: cmd run: | python -m pip install --upgrade pip setuptools --ignore-installed - python -m pip install --upgrade pypiwin32 wheel + python -m pip install --upgrade pypiwin32 wheel win10toast python -m pip install -r requirements-build.txt - name: Grab iso-639 lists diff --git a/CHANGES b/CHANGES index 2f95eb66..3c0d8428 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ * Adding ability to extract HDR10+ metadata * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) +* Adding Windows 10 toast style notification for queue complete success ## Version 4.1.2 diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index dd4ae782..76119c73 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -12,7 +12,7 @@ for root, dirs, files in os.walk('fastflix'): all_fastflix_files.append((os.path.join(root,file), root)) -all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys'] +all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'win10toast'] with open("requirements-build.txt", "r") as reqs: for line in reqs: package = line.split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'", '').strip() diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 88ce8eed..8a3880ef 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -11,7 +11,7 @@ for root, dirs, files in os.walk('fastflix'): for file in files: all_fastflix_files.append((os.path.join(root,file), root)) -all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys'] +all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'win10toast'] with open("requirements-build.txt", "r") as reqs: for line in reqs: package = line.split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'", '').strip() diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index bb95d461..00dca416 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -9,7 +9,6 @@ from fastflix.encoders.common.attachments import build_attachments from fastflix.encoders.common.audio import build_audio from fastflix.encoders.common.subtitles import build_subtitle -from fastflix.models.base import BaseDataClass from fastflix.models.fastflix import FastFlix null = "/dev/null" diff --git a/fastflix/entry.py b/fastflix/entry.py index c1aeda9a..17284a4f 100644 --- a/fastflix/entry.py +++ b/fastflix/entry.py @@ -66,7 +66,6 @@ def startup_options(): import fastflix.encoders.webp.main import fastflix.flix import fastflix.language - import fastflix.models.base import fastflix.models.config import fastflix.models.encode import fastflix.models.fastflix diff --git a/fastflix/models/base.py b/fastflix/models/base.py deleted file mode 100644 index 710b56da..00000000 --- a/fastflix/models/base.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -import logging -from multiprocessing import Queue - -logger = logging.getLogger("fastflix") - -ignore_list = [Queue] - -NO_OPTION = object() - - -class BaseDataClass: - def __setattr__(self, key, value): - if value is not None and key in self.__class__.__annotations__: - annotation = self.__class__.__annotations__[key] - if hasattr(annotation, "__args__") and getattr(annotation, "_name", "") == "Union": - annotation = annotation.__args__ - elif hasattr(annotation, "_name"): - # Assuming this is a typing object we can't handle - return super().__setattr__(key, value) - if annotation in ignore_list: - return super().__setattr__(key, value) - try: - if not isinstance(value, annotation): - raise ValueError( - f'"{key}" attempted to be set to "{value}" of type "{type(value)}" but must be of type "{annotation}"' - ) - except TypeError as err: - logger.debug(f"Could not validate type for {key} with {annotation}: {err}") - return super().__setattr__(key, value) - - # def get(self, item, default=NO_OPTION): - # if default != NO_OPTION: - # return getattr(self, item, default) - # return getattr(self, item) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 645dd1e2..93d1ac87 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -18,6 +18,11 @@ from pydantic import BaseModel, Field from qtpy import QtCore, QtGui, QtWidgets +try: + from win10toast import ToastNotifier +except ImportError: + ToastNotifier = None + from fastflix.encoders.common import helpers from fastflix.exceptions import FastFlixInternalException, FlixError from fastflix.flix import ( @@ -40,8 +45,9 @@ settings_icon, video_add_icon, video_playlist_icon, + main_icon, ) -from fastflix.shared import error_message, time_to_number, yes_no_message +from fastflix.shared import error_message, time_to_number, yes_no_message, message from fastflix.widgets.background_tasks import SubtitleFix, ThumbnailCreator from fastflix.widgets.progress_bar import ProgressBar, Task from fastflix.widgets.video_options import VideoOptions @@ -1564,7 +1570,12 @@ def conversion_complete(self, return_code): error_message(t("There was an error during conversion and the queue has stopped"), title=t("Error")) else: self.video_options.show_queue() - error_message(t("All queue items have completed"), title=t("Success")) + if ToastNotifier is not None: + ToastNotifier().show_toast( + "FastFlix", t("All queue items have completed"), icon_path=main_icon, threaded=True + ) + else: + message(t("All queue items have completed"), title=t("Success")) @reusables.log_exception("fastflix", show_traceback=False) def conversion_cancelled(self, data): From 011e360370145d5ce5020873e6d310f9ad83b42f Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 26 Jan 2021 18:29:56 -0600 Subject: [PATCH 07/50] * Adding canceled message and cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 3c0d8428..b5faf67f 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,7 @@ * Adding ability to extract HDR10+ metadata * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Adding Windows 10 toast style notification for queue complete success +* Adding canceled message and cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) ## Version 4.1.2 From 6f54210b496d1eff67fa64d9725a12b91944b898 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 26 Jan 2021 20:04:58 -0600 Subject: [PATCH 08/50] Starting to add nvenc encoder --- fastflix/application.py | 2 + fastflix/command_runner.py | 23 +-- fastflix/data/languages.yaml | 2 + fastflix/encoders/common/setting_panel.py | 3 +- fastflix/encoders/hevc_nvenc/__init__.py | 0 .../encoders/hevc_nvenc/command_builder.py | 41 ++++ fastflix/encoders/hevc_nvenc/main.py | 105 ++++++++++ .../encoders/hevc_nvenc/settings_panel.py | 185 ++++++++++++++++++ fastflix/models/config.py | 7 +- fastflix/models/encode.py | 10 + fastflix/models/video.py | 2 + 11 files changed, 356 insertions(+), 24 deletions(-) create mode 100644 fastflix/encoders/hevc_nvenc/__init__.py create mode 100644 fastflix/encoders/hevc_nvenc/command_builder.py create mode 100644 fastflix/encoders/hevc_nvenc/main.py create mode 100644 fastflix/encoders/hevc_nvenc/settings_panel.py diff --git a/fastflix/application.py b/fastflix/application.py index 50051c1c..98c0444a 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -54,6 +54,7 @@ def init_encoders(app: FastFlixApp, **_): from fastflix.encoders.svt_av1 import main as svt_av1_plugin from fastflix.encoders.vp9 import main as vp9_plugin from fastflix.encoders.webp import main as webp_plugin + from fastflix.encoders.hevc_nvenc import main as nvenc_plugin encoders = [ hevc_plugin, @@ -64,6 +65,7 @@ def init_encoders(app: FastFlixApp, **_): av1_plugin, rav1e_plugin, svt_av1_plugin, + nvenc_plugin, copy_plugin, ] diff --git a/fastflix/command_runner.py b/fastflix/command_runner.py index b9d1f8e5..4f90ce4f 100644 --- a/fastflix/command_runner.py +++ b/fastflix/command_runner.py @@ -156,7 +156,7 @@ def clean(self): self.started_at = None def kill(self, log=True): - if self.process_two and self.process.poll() is None: + if self.process_two and self.process_two.poll() is None: if log: logger.info(f"Killing worker process {self.process_two.pid}") try: @@ -165,6 +165,7 @@ def kill(self, log=True): except Exception as err: if log: logger.exception(f"Couldn't terminate process: {err}") + if self.process and self.process.poll() is None: if log: logger.info(f"Killing worker process {self.process.pid}") @@ -193,23 +194,3 @@ def resume(self): if not self.process: return False self.process.resume() - - -# if __name__ == "__main__": -# from queue import Queue -# -# logging.basicConfig(level=logging.DEBUG) -# br = BackgroundRunner(Queue()) -# import shutil -# -# ffmpeg = shutil.which("ffmpeg") -# br.start_piped_exec( -# command_one=shlex.split( -# rf'"{ffmpeg}" -loglevel panic -i C:\\Users\\Chris\\scoob_short.mkv -c:v copy -vbsf hevc_mp4toannexb -f hevc -' -# ), -# command_two=shlex.split(r'"C:\\Users\\Chris\\ffmpeg\\hdr10plus_parser.exe" --verify -'), -# work_dir=r"C:\Users\Chris", -# ) -# import time -# time.sleep(1) -# br.read_output() diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 38acfc4d..4e9df02e 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2777,3 +2777,5 @@ Bufsize: ita: Bufsize spa: Bufsize zho: Bufsize +Encoding errored: + eng: Encoding errored diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index a2aa35ad..0241c303 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -237,7 +237,8 @@ def _add_modes( custom_qp = True self.widgets[qp_name].setCurrentText("Custom") else: - self.widgets[qp_name].setCurrentIndex(default_qp_index) + if default_qp_index is not None: + self.widgets[qp_name].setCurrentIndex(default_qp_index) self.widgets[qp_name].currentIndexChanged.connect(lambda: self.mode_update()) self.widgets[f"custom_{qp_name}"] = QtWidgets.QLineEdit("30" if not custom_qp else str(qp_value)) diff --git a/fastflix/encoders/hevc_nvenc/__init__.py b/fastflix/encoders/hevc_nvenc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastflix/encoders/hevc_nvenc/command_builder.py b/fastflix/encoders/hevc_nvenc/command_builder.py new file mode 100644 index 00000000..d622e33e --- /dev/null +++ b/fastflix/encoders/hevc_nvenc/command_builder.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +import re +import secrets + +from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null +from fastflix.models.encode import NVENCSettings +from fastflix.models.fastflix import FastFlix + + +def build(fastflix: FastFlix): + settings: NVENCSettings = fastflix.current_video.video_settings.video_encoder_settings + + beginning, ending = generate_all(fastflix, "hevc_nvenc") + + beginning += f'{f"-tune {settings.tune}" if settings.tune else ""} ' f"{generate_color_details(fastflix)} " + + if settings.profile and settings.profile != "default": + beginning += f"-profile:v {settings.profile} " + + pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}" + + if settings.bitrate: + command_1 = ( + f"{beginning} -pass 1 " + f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}' + ) + command_2 = ( + f'{beginning} -pass 2 -passlogfile "{pass_log_file}" ' + f"-b:v {settings.bitrate} -preset {settings.preset} {settings.extra} " + ) + ending + return [ + Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"), + Command(command=re.sub("[ ]+", " ", command_2), name="Second pass bitrate", exe="ffmpeg"), + ] + + elif settings.crf: + command = f"{beginning} -crf {settings.crf} " f"-preset {settings.preset} {settings.extra} {ending}" + return [Command(command=re.sub("[ ]+", " ", command), name="Single pass CQP", exe="ffmpeg")] + + else: + return [] diff --git a/fastflix/encoders/hevc_nvenc/main.py b/fastflix/encoders/hevc_nvenc/main.py new file mode 100644 index 00000000..9a7a39e6 --- /dev/null +++ b/fastflix/encoders/hevc_nvenc/main.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +__author__ = "Chris Griffith" +from pathlib import Path + +import pkg_resources + +name = "HEVC (nvenc)" +requires = "cuda-llvm" + +video_extension = "mkv" +video_dimension_divisor = 1 +icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_x265.png")).resolve()) + +enable_subtitles = True +enable_audio = True +enable_attachments = True + +audio_formats = [ + "aac", + "aac_mf", + "libfdk_aac", + "ac3", + "ac3_fixed", + "ac3_mf", + "adpcm_adx", + "g722", + "g726", + "g726le", + "adpcm_ima_qt", + "adpcm_ima_ssi", + "adpcm_ima_wav", + "adpcm_ms", + "adpcm_swf", + "adpcm_yamaha", + "alac", + "libopencore_amrnb", + "libvo_amrwbenc", + "aptx", + "aptx_hd", + "comfortnoise", + "dca", + "eac3", + "flac", + "g723_1", + "libgsm", + "libgsm_ms", + "libilbc", + "mlp", + "mp2", + "mp2fixed", + "libtwolame", + "mp3_mf", + "libmp3lame", + "nellymoser", + "opus", + "libopus", + "pcm_alaw", + "pcm_dvd", + "pcm_f32be", + "pcm_f32le", + "pcm_f64be", + "pcm_f64le", + "pcm_mulaw", + "pcm_s16be", + "pcm_s16be_planar", + "pcm_s16le", + "pcm_s16le_planar", + "pcm_s24be", + "pcm_s24daud", + "pcm_s24le", + "pcm_s24le_planar", + "pcm_s32be", + "pcm_s32le", + "pcm_s32le_planar", + "pcm_s64be", + "pcm_s64le", + "pcm_s8", + "pcm_s8_planar", + "pcm_u16be", + "pcm_u16le", + "pcm_u24be", + "pcm_u24le", + "pcm_u32be", + "pcm_u32le", + "pcm_u8", + "pcm_vidc", + "real_144", + "roq_dpcm", + "s302m", + "sbc", + "sonic", + "sonicls", + "libspeex", + "truehd", + "tta", + "vorbis", + "libvorbis", + "wavpack", + "wmav1", + "wmav2", +] + +from fastflix.encoders.hevc_nvenc.command_builder import build +from fastflix.encoders.hevc_nvenc.settings_panel import NVENC as settings_panel diff --git a/fastflix/encoders/hevc_nvenc/settings_panel.py b/fastflix/encoders/hevc_nvenc/settings_panel.py new file mode 100644 index 00000000..83044250 --- /dev/null +++ b/fastflix/encoders/hevc_nvenc/settings_panel.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +import logging + +from box import Box +from qtpy import QtCore, QtWidgets + +from fastflix.encoders.common.setting_panel import SettingPanel +from fastflix.language import t +from fastflix.models.encode import NVENCSettings +from fastflix.models.fastflix_app import FastFlixApp +from fastflix.shared import link + +logger = logging.getLogger("fastflix") + + +presets = [ + "default", + "slow - hq 2 passes", + "medium - hq 1 pass", + "fast - hq 1 pass", + "hp", + "hq", + "bd", + "ll - low latency", + "llhq - low latency hq", + "llhp - low latency hp", + "lossless", + "losslesshp", + "p1 - fastest (lowest quality)", + "p2 - faster", + "p3 - fast", + "p4 - medium", + "p5 - slow", + "p6 - slower", + "p7 - slowest (best quality)", +] + +recommended_bitrates = [ + "800k (320x240p @ 30fps)", + "1000k (640x360p @ 30fps)", + "1500k (640x480p @ 30fps)", + "2000k (1280x720p @ 30fps)", + "5000k (1280x720p @ 60fps)", + "6000k (1080p @ 30fps)", + "9000k (1080p @ 60fps)", + "15000k (1440p @ 30fps)", + "25000k (1440p @ 60fps)", + "35000k (2160p @ 30fps)", + "50000k (2160p @ 60fps)", + "Custom", +] + +recommended_crfs = [ + "28", + "27", + "26", + "25", + "24", + "23", + "22", + "21", + "20", + "19", + "18", + "17", + "16", + "15", + "14", + "Custom", +] + +pix_fmts = ["8-bit: yuv420p", "10-bit: p010le"] + + +class NVENC(SettingPanel): + profile_name = "hevc_nvenc" + + def __init__(self, parent, main, app: FastFlixApp): + super().__init__(parent, main, app) + self.main = main + self.app = app + + grid = QtWidgets.QGridLayout() + + self.widgets = Box(mode=None) + + self.mode = "CRF" + self.updating_settings = False + + grid.addLayout(self.init_modes(), 0, 2, 5, 4) + grid.addLayout(self._add_custom(), 10, 0, 1, 6) + + grid.addLayout(self.init_preset(), 0, 0, 1, 2) + grid.addLayout(self.init_max_mux(), 1, 0, 1, 2) + grid.addLayout(self.init_tune(), 2, 0, 1, 2) + grid.addLayout(self.init_profile(), 3, 0, 1, 2) + grid.addLayout(self.init_pix_fmt(), 4, 0, 1, 2) + + grid.setRowStretch(9, 1) + + guide_label = QtWidgets.QLabel( + link("https://trac.ffmpeg.org/wiki/Encode/H.264", t("FFMPEG AVC / H.264 Encoding Guide")) + ) + guide_label.setAlignment(QtCore.Qt.AlignBottom) + guide_label.setOpenExternalLinks(True) + grid.addWidget(guide_label, 11, 0, 1, 6) + + self.setLayout(grid) + self.hide() + + def init_preset(self): + layout = self._add_combo_box( + label="Preset", + widget_name="preset", + options=presets, + tooltip=("preset: The slower the preset, the better the compression and quality"), + connect="default", + opt="preset", + ) + return layout + + def init_tune(self): + return self._add_combo_box( + label="Tune", + widget_name="tune", + tooltip="Tune the settings for a particular type of source or situation", + options=["hq - High quality", "ll - Low Latency", "ull - Ultra Low Latency", "lossless"], + opt="tune", + ) + + def init_profile(self): + return self._add_combo_box( + label="Profile_encoderopt", + widget_name="profile", + tooltip="Enforce an encode profile", + options=["main", "main10", "rext"], + opt="profile", + ) + + def init_pix_fmt(self): + return self._add_combo_box( + label="Bit Depth", + tooltip="Pixel Format (requires at least 10-bit for HDR)", + widget_name="pix_fmt", + options=pix_fmts, + opt="pix_fmt", + ) + + def init_modes(self): + return self._add_modes(recommended_bitrates, recommended_crfs, qp_name="cqp") + + def mode_update(self): + self.widgets.custom_crf.setDisabled(self.widgets.crf.currentText() != "Custom") + self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom") + self.main.build_commands() + + def setting_change(self, update=True): + if self.updating_settings: + return + self.updating_settings = True + + if update: + self.main.page_update() + self.updating_settings = False + + def update_video_encoder_settings(self): + tune = self.widgets.tune.currentText() + + settings = NVENCSettings( + preset=self.widgets.preset.currentText().split("-")[0].strip(), + max_muxing_queue_size=self.widgets.max_mux.currentText(), + profile=self.widgets.profile.currentText(), + pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(), + extra=self.ffmpeg_extras, + tune=tune.split("-")[0].strip() if tune.lower() != "default" else None, + extra_both_passes=self.widgets.extra_both_passes.isChecked(), + ) + encode_type, q_value = self.get_mode_settings() + settings.cqp = q_value if encode_type == "qp" else None + settings.bitrate = q_value if encode_type == "bitrate" else None + self.app.fastflix.current_video.video_settings.video_encoder_settings = settings + + def set_mode(self, x): + self.mode = x.text() + self.main.build_commands() diff --git a/fastflix/models/config.py b/fastflix/models/config.py index db08edd4..47aee1a6 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -20,6 +20,7 @@ rav1eSettings, x264Settings, x265Settings, + NVENCSettings, ) from fastflix.version import __version__ @@ -40,6 +41,7 @@ "gif": GIFSettings, "webp": WebPSettings, "copy_settings": CopySettings, + "hevc_nvenc": NVENCSettings, } outdated_settings = ("copy",) @@ -77,6 +79,7 @@ class Profile(BaseModel): gif: Optional[GIFSettings] = None webp: Optional[WebPSettings] = None copy_settings: Optional[CopySettings] = None + hevc_nvenc: Optional[NVENCSettings] = None empty_profile = Profile(x265=x265Settings()) @@ -118,8 +121,8 @@ class Config(BaseModel): config_path: Path = fastflix_folder / "fastflix.yaml" ffmpeg: Path = Field(default_factory=lambda: find_ffmpeg_file("ffmpeg")) ffprobe: Path = Field(default_factory=lambda: find_ffmpeg_file("ffprobe")) - hdr10plus_parser: Path = Field(default_factory=lambda: Path(shutil.which("hdr10plus_parser"))) - mkvpropedit: Path = Field(default_factory=lambda: Path(shutil.which("mkvpropedit"))) + hdr10plus_parser: Path = Field(default_factory=lambda: Path(shutil.which("hdr10plus_parser") or "") or None) + mkvpropedit: Path = Field(default_factory=lambda: Path(shutil.which("mkvpropedit") or "") or None) flat_ui: bool = True language: str = "en" logging_level: int = 10 diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index 9652fcc2..6242b8fb 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -73,6 +73,16 @@ class x264Settings(EncoderSettings): bitrate: Optional[str] = None +class NVENCSettings(EncoderSettings): + name = "HEVC (nvenc)" + preset: str = "p7" + profile: str = "default" + tune: Optional[str] = None + pix_fmt: str = "yuv420p" + cqp: Optional[Union[int, float]] = None + bitrate: Optional[str] = "6000k" + + class rav1eSettings(EncoderSettings): name = "AV1 (rav1e)" speed: str = "-1" diff --git a/fastflix/models/video.py b/fastflix/models/video.py index d79aa87c..ecb7185f 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -19,6 +19,7 @@ rav1eSettings, x264Settings, x265Settings, + NVENCSettings, ) __all__ = ["VideoSettings", "Status", "Video"] @@ -63,6 +64,7 @@ class VideoSettings(BaseModel): GIFSettings, WebPSettings, CopySettings, + NVENCSettings, ] = None audio_tracks: List[AudioTrack] = Field(default_factory=list) subtitle_tracks: List[SubtitleTrack] = Field(default_factory=list) From 4b0f50e9d61d98a44b3fa1617c4562f344f7e124 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 26 Jan 2021 21:56:57 -0600 Subject: [PATCH 09/50] Cleanup --- .github/workflows/build_windows.yaml | 2 +- CHANGES | 4 +- FastFlix_Windows_Installer.spec | 2 +- FastFlix_Windows_OneFile.spec | 2 +- fastflix/application.py | 2 +- fastflix/data/languages.yaml | 6 +++ fastflix/encoders/hevc_x265/settings_panel.py | 2 +- fastflix/flix.py | 2 +- fastflix/models/config.py | 2 +- fastflix/models/video.py | 2 +- fastflix/program_downloads.py | 5 ++- fastflix/shared.py | 42 +++++++++++++++++++ fastflix/widgets/container.py | 14 ++++++- fastflix/widgets/main.py | 17 +++----- 14 files changed, 79 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build_windows.yaml b/.github/workflows/build_windows.yaml index 0aec98d3..1059c0d5 100644 --- a/.github/workflows/build_windows.yaml +++ b/.github/workflows/build_windows.yaml @@ -30,7 +30,7 @@ jobs: shell: cmd run: | python -m pip install --upgrade pip setuptools --ignore-installed - python -m pip install --upgrade pypiwin32 wheel win10toast + python -m pip install --upgrade pypiwin32 wheel python -m pip install -r requirements-build.txt - name: Grab iso-639 lists diff --git a/CHANGES b/CHANGES index b5faf67f..21048e93 100644 --- a/CHANGES +++ b/CHANGES @@ -4,8 +4,8 @@ * Adding ability to extract HDR10+ metadata * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) -* Adding Windows 10 toast style notification for queue complete success -* Adding canceled message and cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) +* Adding Windows 10 notification for queue complete success +* Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) ## Version 4.1.2 diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index 76119c73..dd4ae782 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -12,7 +12,7 @@ for root, dirs, files in os.walk('fastflix'): all_fastflix_files.append((os.path.join(root,file), root)) -all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'win10toast'] +all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys'] with open("requirements-build.txt", "r") as reqs: for line in reqs: package = line.split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'", '').strip() diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 8a3880ef..88ce8eed 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -11,7 +11,7 @@ for root, dirs, files in os.walk('fastflix'): for file in files: all_fastflix_files.append((os.path.join(root,file), root)) -all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'win10toast'] +all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys'] with open("requirements-build.txt", "r") as reqs: for line in reqs: package = line.split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'", '').strip() diff --git a/fastflix/application.py b/fastflix/application.py index 98c0444a..2f5de151 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -49,12 +49,12 @@ def init_encoders(app: FastFlixApp, **_): from fastflix.encoders.avc_x264 import main as avc_plugin from fastflix.encoders.copy import main as copy_plugin from fastflix.encoders.gif import main as gif_plugin + from fastflix.encoders.hevc_nvenc import main as nvenc_plugin from fastflix.encoders.hevc_x265 import main as hevc_plugin from fastflix.encoders.rav1e import main as rav1e_plugin from fastflix.encoders.svt_av1 import main as svt_av1_plugin from fastflix.encoders.vp9 import main as vp9_plugin from fastflix.encoders.webp import main as webp_plugin - from fastflix.encoders.hevc_nvenc import main as nvenc_plugin encoders = [ hevc_plugin, diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 4e9df02e..b712b586 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2779,3 +2779,9 @@ Bufsize: zho: Bufsize Encoding errored: eng: Encoding errored +Download Cancelled: + eng: Download Cancelled +Extract HDR10+: + eng: Extract HDR10+ +Detect HDR10+: + eng: Detect HDR10+ diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index c104992b..d0c68245 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -10,7 +10,7 @@ from fastflix.language import t from fastflix.models.encode import x265Settings from fastflix.models.fastflix_app import FastFlixApp -from fastflix.resources import warning_icon, loading_movie +from fastflix.resources import loading_movie, warning_icon from fastflix.shared import link from fastflix.widgets.background_tasks import ExtractHDR10 diff --git a/fastflix/flix.py b/fastflix/flix.py index 5a16aeed..99e75e82 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -3,7 +3,7 @@ import os import re from pathlib import Path -from subprocess import PIPE, CompletedProcess, TimeoutExpired, run, Popen +from subprocess import PIPE, CompletedProcess, Popen, TimeoutExpired, run from typing import List, Tuple, Union import reusables diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 47aee1a6..5e5c644f 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -14,13 +14,13 @@ AOMAV1Settings, CopySettings, GIFSettings, + NVENCSettings, SVTAV1Settings, VP9Settings, WebPSettings, rav1eSettings, x264Settings, x265Settings, - NVENCSettings, ) from fastflix.version import __version__ diff --git a/fastflix/models/video.py b/fastflix/models/video.py index ecb7185f..2e3018ff 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -12,6 +12,7 @@ AudioTrack, CopySettings, GIFSettings, + NVENCSettings, SubtitleTrack, SVTAV1Settings, VP9Settings, @@ -19,7 +20,6 @@ rav1eSettings, x264Settings, x265Settings, - NVENCSettings, ) __all__ = ["VideoSettings", "Status", "Video"] diff --git a/fastflix/program_downloads.py b/fastflix/program_downloads.py index 5d64069d..427b6d14 100644 --- a/fastflix/program_downloads.py +++ b/fastflix/program_downloads.py @@ -47,7 +47,6 @@ def latest_ffmpeg(signal, stop_signal, **_): def stop_me(): nonlocal stop - message("Download Canceled") stop = True stop_signal.connect(stop_me) @@ -62,6 +61,7 @@ def stop_me(): raise if stop: + message(t("Download Cancelled")) return gpl_ffmpeg = [asset for asset in data["assets"] if asset["name"].endswith("win64-gpl.zip")] @@ -85,6 +85,7 @@ def stop_me(): if stop: f.close() Path(filename).unlink() + message(t("Download Cancelled")) return if filename.stat().st_size < 1000: @@ -102,6 +103,8 @@ def stop_me(): raise if stop: + Path(filename).unlink() + message(t("Download Cancelled")) return signal.emit(95) diff --git a/fastflix/shared.py b/fastflix/shared.py index f540af6d..5f39cd38 100644 --- a/fastflix/shared.py +++ b/fastflix/shared.py @@ -11,6 +11,30 @@ import pkg_resources import requests import reusables +from win32api import GetModuleHandle +from win32con import ( + CW_USEDEFAULT, + IMAGE_ICON, + LR_DEFAULTSIZE, + LR_LOADFROMFILE, + WM_USER, + WS_OVERLAPPED, + WS_SYSMENU, +) +from win32gui import ( + NIF_ICON, + NIF_INFO, + NIF_MESSAGE, + NIF_TIP, + NIM_ADD, + NIM_MODIFY, + WNDCLASS, + CreateWindow, + LoadImage, + RegisterClass, + Shell_NotifyIcon, + UpdateWindow, +) try: # PyInstaller creates a temp folder and stores path in _MEIPASS @@ -256,3 +280,21 @@ def timedelta_to_str(delta): output_string = output_string.split(".")[0] # Remove .XXX microseconds return output_string + + +def show_windows_notification(title, msg, icon_path): + wc = WNDCLASS() + hinst = wc.hInstance = GetModuleHandle(None) + wc.lpszClassName = "FastFlix" + tool_window = CreateWindow( + RegisterClass(wc), "Taskbar", WS_OVERLAPPED | WS_SYSMENU, 0, 0, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, hinst, None + ) + UpdateWindow(tool_window) + + icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE + icon = LoadImage(hinst, icon_path, IMAGE_ICON, 0, 0, icon_flags) + + flags = NIF_ICON | NIF_MESSAGE | NIF_TIP + nid = (tool_window, 0, flags, WM_USER + 20, icon, "Tooltip") + Shell_NotifyIcon(NIM_ADD, nid) + Shell_NotifyIcon(NIM_MODIFY, (tool_window, 0, NIF_INFO, WM_USER + 20, icon, "Balloon Tooltip", msg, 200, title, 4)) diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 727df493..862f4c9b 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -33,6 +33,7 @@ class Container(QtWidgets.QMainWindow): def __init__(self, app: FastFlixApp, **kwargs): super().__init__(None) self.app = app + self.pb = None self.logs = Logs() self.changes = Changes() @@ -50,6 +51,11 @@ def __init__(self, app: FastFlixApp, **kwargs): self.setWindowIcon(self.icon) def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + if self.pb: + try: + self.pb.stop_signal.emit() + except Exception: + pass if self.main.converting: sm = QtWidgets.QMessageBox() sm.setText(f"

{t('There is a conversion in process!')}

") @@ -190,7 +196,9 @@ def download_ffmpeg(self): ffmpeg = ffmpeg_folder / "ffmpeg.exe" ffprobe = ffmpeg_folder / "ffprobe.exe" try: - ProgressBar(self.app, [Task(t("Downloading FFmpeg"), latest_ffmpeg)], signal_task=True, can_cancel=True) + self.pb = ProgressBar( + self.app, [Task(t("Downloading FFmpeg"), latest_ffmpeg)], signal_task=True, can_cancel=True + ) except FastFlixInternalException: pass except Exception as err: @@ -201,12 +209,14 @@ def download_ffmpeg(self): else: self.app.fastflix.config.ffmpeg = ffmpeg self.app.fastflix.config.ffprobe = ffprobe + self.pb = None def clean_old_logs(self): try: - ProgressBar(self.app, [Task(t("Clean Old Logs"), clean_logs)], signal_task=True, can_cancel=False) + self.pb = ProgressBar(self.app, [Task(t("Clean Old Logs"), clean_logs)], signal_task=True, can_cancel=False) except Exception: error_message(t("Could not compress old logs"), traceback=True) + self.pb = None class OpenFolder(QtCore.QThread): diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 93d1ac87..27ba747e 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -18,21 +18,16 @@ from pydantic import BaseModel, Field from qtpy import QtCore, QtGui, QtWidgets -try: - from win10toast import ToastNotifier -except ImportError: - ToastNotifier = None - from fastflix.encoders.common import helpers from fastflix.exceptions import FastFlixInternalException, FlixError from fastflix.flix import ( + detect_hdr10_plus, detect_interlaced, extract_attachments, generate_thumbnail_command, get_auto_crop, parse, parse_hdr_details, - detect_hdr10_plus, ) from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp @@ -40,14 +35,14 @@ from fastflix.resources import ( black_x_icon, folder_icon, + main_icon, play_round_icon, profile_add_icon, settings_icon, video_add_icon, video_playlist_icon, - main_icon, ) -from fastflix.shared import error_message, time_to_number, yes_no_message, message +from fastflix.shared import error_message, message, time_to_number, yes_no_message, show_windows_notification from fastflix.widgets.background_tasks import SubtitleFix, ThumbnailCreator from fastflix.widgets.progress_bar import ProgressBar, Task from fastflix.widgets.video_options import VideoOptions @@ -1570,10 +1565,8 @@ def conversion_complete(self, return_code): error_message(t("There was an error during conversion and the queue has stopped"), title=t("Error")) else: self.video_options.show_queue() - if ToastNotifier is not None: - ToastNotifier().show_toast( - "FastFlix", t("All queue items have completed"), icon_path=main_icon, threaded=True - ) + if reusables.win_based: + show_windows_notification("FastFlix", t("All queue items have completed"), icon_path=main_icon) else: message(t("All queue items have completed"), title=t("Success")) From 625f63ae1413819fe578a9e97105a24b9b5ce0b9 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 27 Jan 2021 09:02:30 -0600 Subject: [PATCH 10/50] * Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz) --- CHANGES | 1 + fastflix/encoders/hevc_x265/command_builder.py | 6 ++++++ fastflix/models/config.py | 8 +++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 21048e93..f387e8b9 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Adding Windows 10 notification for queue complete success * Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) +* Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz) ## Version 4.1.2 diff --git a/fastflix/encoders/hevc_x265/command_builder.py b/fastflix/encoders/hevc_x265/command_builder.py index adf3c359..ab6238a4 100644 --- a/fastflix/encoders/hevc_x265/command_builder.py +++ b/fastflix/encoders/hevc_x265/command_builder.py @@ -74,6 +74,8 @@ color_matrix_mapping = {"bt2020_ncl": "bt2020nc", "bt2020_cl": "bt2020c"} +chromaloc_mapping = {"left": 0, "center": 1, "topleft": 2, "top": 3, "bottomleft": 4, "bottom": 5} + def build(fastflix: FastFlix): settings: x265Settings = fastflix.current_video.video_settings.video_encoder_settings @@ -134,6 +136,10 @@ def build(fastflix: FastFlix): x265_params.append(f"hdr10={'1' if settings.hdr10 else '0'}") + current_chroma_loc = fastflix.current_video.current_video_stream.get("chroma_location") + if current_chroma_loc in chromaloc_mapping: + x265_params.append(f"chromaloc={chromaloc_mapping[current_chroma_loc]}") + if settings.hdr10plus_metadata: x265_params.append(f"dhdr10-info='{settings.hdr10plus_metadata}'") diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 5e5c644f..d782d61b 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -121,8 +121,10 @@ class Config(BaseModel): config_path: Path = fastflix_folder / "fastflix.yaml" ffmpeg: Path = Field(default_factory=lambda: find_ffmpeg_file("ffmpeg")) ffprobe: Path = Field(default_factory=lambda: find_ffmpeg_file("ffprobe")) - hdr10plus_parser: Path = Field(default_factory=lambda: Path(shutil.which("hdr10plus_parser") or "") or None) - mkvpropedit: Path = Field(default_factory=lambda: Path(shutil.which("mkvpropedit") or "") or None) + hdr10plus_parser: Optional[Path] = Field( + default_factory=lambda: Path(shutil.which("hdr10plus_parser") or "") or None + ) + mkvpropedit: Optional[Path] = Field(default_factory=lambda: Path(shutil.which("mkvpropedit") or "") or None) flat_ui: bool = True language: str = "en" logging_level: int = 10 @@ -192,7 +194,7 @@ def load(self): "there may be non-recoverable errors while loading it." ) - paths = ("work_path", "ffmpeg", "ffprobe") + paths = ("work_path", "ffmpeg", "ffprobe", "hdr10plus_parser", "mkvpropedit") for key, value in data.items(): if key == "profiles": self.profiles = {} From 0a0d67c3bf43e0b928bdb3e9ff78441e4b91c1dc Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 27 Jan 2021 09:59:43 -0600 Subject: [PATCH 11/50] better logic to find hdr10plus_parser and mkvpropedit --- fastflix/models/config.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/fastflix/models/config.py b/fastflix/models/config.py index d782d61b..e2401ac3 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -116,15 +116,19 @@ def find_ffmpeg_file(name, raise_on_missing=False): return None +def where(filename: str) -> Optional[Path]: + if location := shutil.which(filename): + return Path(location) + return None + + class Config(BaseModel): version: str = __version__ config_path: Path = fastflix_folder / "fastflix.yaml" ffmpeg: Path = Field(default_factory=lambda: find_ffmpeg_file("ffmpeg")) ffprobe: Path = Field(default_factory=lambda: find_ffmpeg_file("ffprobe")) - hdr10plus_parser: Optional[Path] = Field( - default_factory=lambda: Path(shutil.which("hdr10plus_parser") or "") or None - ) - mkvpropedit: Optional[Path] = Field(default_factory=lambda: Path(shutil.which("mkvpropedit") or "") or None) + hdr10plus_parser: Optional[Path] = Field(default_factory=lambda: where("hdr10plus_parser")) + mkvpropedit: Optional[Path] = Field(default_factory=lambda: where("mkvpropedit")) flat_ui: bool = True language: str = "en" logging_level: int = 10 @@ -231,6 +235,10 @@ def load(self): self.ffprobe = find_ffmpeg_file("ffmpeg.ffprobe", raise_on_missing=True) except MissingFF: raise err from None + if not self.hdr10plus_parser: + self.hdr10plus_parser = where("hdr10plus_parser") + if not self.mkvpropedit: + self.mkvpropedit = where("mkvpropedit") self.profiles.update(get_preset_defaults()) if self.selected_profile not in self.profiles: From 20d088830bc39bea920b21a15612978190eda2f2 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 27 Jan 2021 17:45:52 -0600 Subject: [PATCH 12/50] * Adding #109 NVENC HEVC support based on FFmpeg * Fixing that returning item back from queue of a different encoder type would crash Fastflix --- CHANGES | 4 +- README.md | 23 +-- fastflix/application.py | 2 +- fastflix/data/encoders/icon_nvenc.png | Bin 0 -> 1611 bytes fastflix/encoders/avc_x264/command_builder.py | 8 +- fastflix/encoders/common/setting_panel.py | 68 +++++++- .../__init__.py | 0 .../ffmpeg_hevc_nvenc/command_builder.py | 40 +++++ .../{hevc_nvenc => ffmpeg_hevc_nvenc}/main.py | 8 +- .../settings_panel.py | 156 +++++++++++++++--- .../encoders/hevc_nvenc/command_builder.py | 41 ----- .../encoders/hevc_x265/command_builder.py | 10 +- fastflix/models/config.py | 6 +- fastflix/models/encode.py | 22 ++- fastflix/models/video.py | 4 +- fastflix/shared.py | 46 +++++- fastflix/widgets/container.py | 3 +- fastflix/widgets/main.py | 7 +- fastflix/widgets/video_options.py | 12 +- 19 files changed, 336 insertions(+), 124 deletions(-) create mode 100644 fastflix/data/encoders/icon_nvenc.png rename fastflix/encoders/{hevc_nvenc => ffmpeg_hevc_nvenc}/__init__.py (100%) create mode 100644 fastflix/encoders/ffmpeg_hevc_nvenc/command_builder.py rename fastflix/encoders/{hevc_nvenc => ffmpeg_hevc_nvenc}/main.py (88%) rename fastflix/encoders/{hevc_nvenc => ffmpeg_hevc_nvenc}/settings_panel.py (50%) delete mode 100644 fastflix/encoders/hevc_nvenc/command_builder.py diff --git a/CHANGES b/CHANGES index f387e8b9..bafed885 100644 --- a/CHANGES +++ b/CHANGES @@ -2,11 +2,13 @@ ## Version 4.2.0 -* Adding ability to extract HDR10+ metadata +* Adding #109 NVENC HEVC support based on FFmpeg +* Adding ability to extract HDR10+ metadata if hdr10plus_parser is detected on path * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Adding Windows 10 notification for queue complete success * Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) * Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz) +* Fixing that returning item back from queue of a different encoder type would crash Fastflix ## Version 4.1.2 diff --git a/README.md b/README.md index 8533548d..ed036e15 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ FastFlix keeps HDR10 metadata for x265, which will be expanded to AV1 libraries It needs `FFmpeg` (version 4.3 or greater) under the hood for the heavy lifting, and can work with a variety of encoders. +Please also grab [hdr10plus_parser](https://github.com/quietvoid/hdr10plus_parser/releases) and [mkvpropedit](https://mkvtoolnix.download/downloads.html) and make sure they are on the system path for full feature set. + **NEW**: Join us on [discord](https://discord.gg/GUBFP6f) or [reddit](https://www.reddit.com/r/FastFlix/)! Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki) for help or more details, and please report bugs or ideas in the [github issue tracker](https://github.com/cdgriffith/FastFlix/issues)! @@ -18,14 +20,15 @@ Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki FastFlix supports the following encoders when their required libraries are found in FFmpeg: -* HEVC (libx265)     x265 -* AVC (libx264)        x264 -* AV1 (librav1e)        rav1e -* AV1 (libaom-av1)   av1_aom -* AV1 (libsvtav1)       svt_av1 -* VP9 (libvpx)           vpg -* WEBP (libwebp)    vpg -* GIF (gif)                 gif +| Encoder | x265 | NVENC HEVC | x264 | rav1e | AOM AV1 | SVT AV1 | VP9 | WEBP | GIF | +| --------- | ---- | ---------- | ---- | ----- | ------- | ------- | --- | ---- | --- | +| HDR10 | ✓ | | | | | | ✓* | | | +| HDR10+ | ✓ | | | | | | | | | +| Audio | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| Subtitles | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | +| Covers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | +| bt.2020 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | + All of these are currently supported by [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds) which is the default FFmpeg downloaded. @@ -71,8 +74,8 @@ VP9 has limited support to copy some existing HDR10 metadata, usually from other ## HDR10+ -FastFlix supports using generated or [extracted JSON HDR10+ Metadata](https://github.com/cdgriffith/FastFlix/wiki/HDR10-Plus-Metadata-Extraction) with HEVC encodes via x265. However that is highly -dependant on a FFmpeg version that has been compiled with x265 that has HDR10+ support. [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds) +FastFlix supports using generated or [extracted JSON HDR10+ Metadata](https://github.com/cdgriffith/FastFlix/wiki/HDR10-Plus-Metadata-Extraction) with HEVC encodes via x265. However, that is highly +dependent on a FFmpeg version that has been compiled with x265 that has HDR10+ support. [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds) have this support as of 10/23/2020 and may require a [manual upgrade](https://github.com/cdgriffith/FastFlix/wiki/Updating-FFmpeg). ## HLG diff --git a/fastflix/application.py b/fastflix/application.py index 2f5de151..7d43da1a 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -49,7 +49,7 @@ def init_encoders(app: FastFlixApp, **_): from fastflix.encoders.avc_x264 import main as avc_plugin from fastflix.encoders.copy import main as copy_plugin from fastflix.encoders.gif import main as gif_plugin - from fastflix.encoders.hevc_nvenc import main as nvenc_plugin + from fastflix.encoders.ffmpeg_hevc_nvenc import main as nvenc_plugin from fastflix.encoders.hevc_x265 import main as hevc_plugin from fastflix.encoders.rav1e import main as rav1e_plugin from fastflix.encoders.svt_av1 import main as svt_av1_plugin diff --git a/fastflix/data/encoders/icon_nvenc.png b/fastflix/data/encoders/icon_nvenc.png new file mode 100644 index 0000000000000000000000000000000000000000..c217120448bfebb15a67a596503dd940759bc599 GIT binary patch literal 1611 zcmZXUdpOg39LImIVNGdK2?@n9MRF{bmCMSCxoyW~A(f=r+J@}02a&8Ij$1+_m2%r6 zvHhrobWxJWO0gXlrd*cDCQZ&dr$0KK@AG`WulMWo`TY5PKIgo#d!btTS^xk*G45`@ z3UU>fq@kjC`MSV}0;&_7U8bih8F zibaZ|WsEP@4{(qy(uT+9t3Hi={yOTJ`4}I!`u#$Z%_MJDYVbqoPFk8l-8i0C$uMqlGSsrv=Jnljr^twt~_$0q(SEj2xZ;Ttfsb`k@@zQTt zFP~{Z&BylzlOgY;Uhi*zuDQ34H;21}6{|`kdhIke+{r1Ef3WEpgw!w7$f^mcf0N~H zo@>Tp6L_1vxR9@DYd9aaIy_5u?`JEM1y0no}5If zs1YWi4KALNJb3{Ju$R?Oe?4qD8$+q>D?b)MzFWjt53(*GfOP1 zr*5;5`ssR0E;PntR#ewEnMX)$)9(Vzqy<8WWzGa4XYTFKLO7J&8hf?bkV`{GcaNa_ zZ&#a&QBTY*t1w+oQPquFE7e~&T|n*+FGx<3A4c^BHW5Xc)HY8tq&7PiX$Vr=bht%P z;mMqD(KX6N4E;BnTwd7jq2xnoBuynCmr{((Eu_!K$ren|3GfHXk3yYk?W%4F`Nk7? zuf9w{;4yQ7IIY=b>6+ncuuAy=fDS&eg&aIBZM;^bVF>C51=F@jsy(_@bjS4~ce~Sr z50J?3jVzgZxOL&fEO4W!VmLeUa7+^c^l2lJr{gR5b2fGRy#2{}l=Sr(Z`oXH5dVR% z$p!HR<#jgAtm9b-a9IFD^-}p!-z}1!)0Tu?)OO?7odbt`LkiN0 z9R$-ca}uR}Y@gdWr*#fZ)&`O@K{%_#^Z^TwwyeLimbHkQYsT;_z?O$XUST`w!m*wG z3>v~HA}vfbw6y?9L1@N-uR3>~Q1|o;j;ASX;!1cY3nVIjP}w0@M{T8(XA=dGQ7UBn z@}yJtGQ)p54X%qkD9%(KT>!vXw1-KFaA8n0-qiJB`=aEYG``S{fQnM40l* z7D)cn1=9F>glrAnz~`t@m?)&Q11~KifNygGiYsk%R>d|VxRxNY`niW_ZQI*!Mu**1 z*{l(EN5hTOy3mrdButW|Ca{MudiU9T2$DBB^vRUlL`)%&w691L{}hnM1|KRNjkXPB zw&1PY#qfJv{Tgu?|71vb2R|4_q}M`@&s3n3CcYqAS^3)B0?${GLq`ai_EanEM{cfk z))_8^cH|GGg&U7*C%mqc*1GoMPp9u&kM^0K*{RpDH4%a^;QKW{h~xEDKATD)HGbYk zyR`U(Ey#E?LBiQz7V?pT*f#d`7;6V_1CyGzVR1I{V8;GU-5lY)zY12|?BA4#kJ(}x zR3#aiami!GYPFoBvaz}4pK^{&dPS&=8Fr03EclmV3$rR~>EUMiI*)e45!TP&r`yVG zDBHE*Uz)5u#_I15WOpu&OpONKFEN<>g5LVS6<7ByVNp$4pD?6>ZgBs;h%soa8^`5f F%HO}#=EeX3 literal 0 HcmV?d00001 diff --git a/fastflix/encoders/avc_x264/command_builder.py b/fastflix/encoders/avc_x264/command_builder.py index d32a2be5..a55fcdc5 100644 --- a/fastflix/encoders/avc_x264/command_builder.py +++ b/fastflix/encoders/avc_x264/command_builder.py @@ -12,7 +12,7 @@ def build(fastflix: FastFlix): beginning, ending = generate_all(fastflix, "libx264") - beginning += f'{f"-tune {settings.tune}" if settings.tune else ""} ' f"{generate_color_details(fastflix)} " + beginning += f'{f"-tune:v {settings.tune}" if settings.tune else ""} {generate_color_details(fastflix)} ' if settings.profile and settings.profile != "default": beginning += f"-profile:v {settings.profile} " @@ -22,11 +22,11 @@ def build(fastflix: FastFlix): if settings.bitrate: command_1 = ( f"{beginning} -pass 1 " - f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}' + f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}' ) command_2 = ( f'{beginning} -pass 2 -passlogfile "{pass_log_file}" ' - f"-b:v {settings.bitrate} -preset {settings.preset} {settings.extra} " + f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} " ) + ending return [ Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"), @@ -34,7 +34,7 @@ def build(fastflix: FastFlix): ] elif settings.crf: - command = f"{beginning} -crf {settings.crf} " f"-preset {settings.preset} {settings.extra} {ending}" + command = f"{beginning} -crf:v {settings.crf} " f"-preset:v {settings.preset} {settings.extra} {ending}" return [Command(command=re.sub("[ ]+", " ", command), name="Single pass CRF", exe="ffmpeg")] else: diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index 0241c303..f0cc1b93 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -37,12 +37,14 @@ def determine_default(self, widget_name, opt, items: List, raise_error: bool = F elif widget_name in ("crf", "qp"): if not opt: return 6 - items = [x.split("(")[0].split("-")[0].strip() for x in items] opt = str(opt) elif widget_name == "bitrate": if not opt: return 5 - items = [x.split("(")[0].split("-")[0].strip() for x in items] + elif widget_name == "gpu": + if opt == -1: + return 0 + items = [x.split("(")[0].split("-")[0].strip() if x != "-1" else "-1" for x in items] if isinstance(opt, str): try: return items.index(opt) @@ -50,7 +52,7 @@ def determine_default(self, widget_name, opt, items: List, raise_error: bool = F if raise_error: raise FastFlixInternalException else: - logger.error(f"Could not set default for {widget_name} to {opt} as it's not in the list") + logger.error(f"Could not set default for {widget_name} to {opt} as it's not in the list: {items}") return 0 if isinstance(opt, bool): return int(opt) @@ -91,10 +93,56 @@ def _add_combo_box( return layout + def _add_text_box( + self, + label, + widget_name, + opt=None, + connect="default", + enabled=True, + default="", + tooltip="", + validator=None, + width=None, + ): + layout = QtWidgets.QHBoxLayout() + self.labels[widget_name] = QtWidgets.QLabel(t(label)) + if tooltip: + self.labels[widget_name].setToolTip(self.translate_tip(tooltip)) + + self.widgets[widget_name] = QtWidgets.QLineEdit() + + if opt: + default = str(self.app.fastflix.config.encoder_opt(self.profile_name, opt)) or default + self.opts[widget_name] = opt + self.widgets[widget_name].setText(default) + self.widgets[widget_name].setDisabled(not enabled) + if tooltip: + self.widgets[widget_name].setToolTip(self.translate_tip(tooltip)) + if connect: + if connect == "default": + self.widgets[widget_name].textChanged.connect(lambda: self.main.page_update(build_thumbnail=False)) + elif connect == "self": + self.widgets[widget_name].textChanged.connect(lambda: self.page_update()) + else: + self.widgets[widget_name].textChanged.connect(connect) + + if validator: + if validator == "int": + self.widgets[widget_name].setValidator(self.only_int) + if validator == "float": + self.widgets[widget_name].setValidator(self.only_int) + + if width: + self.widgets[widget_name].setFixedWidth(width) + + layout.addWidget(self.labels[widget_name]) + layout.addWidget(self.widgets[widget_name]) + + return layout + def _add_check_box(self, label, widget_name, opt, connect="default", enabled=True, checked=True, tooltip=""): layout = QtWidgets.QHBoxLayout() - # self.labels[widget_name] = QtWidgets.QLabel() - # self.labels[widget_name].setToolTip() self.widgets[widget_name] = QtWidgets.QCheckBox(t(label)) self.opts[widget_name] = opt @@ -171,6 +219,7 @@ def _add_modes( recommended_bitrates, recommended_qps, qp_name="crf", + add_qp=True, ): self.recommended_bitrates = recommended_bitrates self.recommended_qps = recommended_qps @@ -265,6 +314,9 @@ def _add_modes( layout.addWidget(qp_group_box, 0, 0) layout.addWidget(bitrate_group_box, 1, 0) + if not add_qp: + qp_group_box.hide() + return layout @property @@ -274,7 +326,7 @@ def ffmpeg_extras(self): def ffmpeg_extra_update(self): global ffmpeg_extra_command ffmpeg_extra_command = self.ffmpeg_extras_widget.text().strip() - self.main.page_update() + self.main.page_update(build_thumbnail=False) def new_source(self): if not self.app.fastflix.current_video or not self.app.fastflix.current_video.streams: @@ -353,12 +405,14 @@ def reload(self): self.widgets[widget_name].setCurrentIndex(data) else: self.widgets[widget_name].setCurrentText(data) + # Do smart check for cleaning up stuff + elif isinstance(self.widgets[widget_name], QtWidgets.QCheckBox): self.widgets[widget_name].setChecked(data) elif isinstance(self.widgets[widget_name], QtWidgets.QLineEdit): if widget_name == "x265_params": data = ":".join(data) - self.widgets[widget_name].setText(data or "") + self.widgets[widget_name].setText(str(data) or "") if getattr(self, "qp_radio", None): bitrate = getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, "bitrate", None) if bitrate: diff --git a/fastflix/encoders/hevc_nvenc/__init__.py b/fastflix/encoders/ffmpeg_hevc_nvenc/__init__.py similarity index 100% rename from fastflix/encoders/hevc_nvenc/__init__.py rename to fastflix/encoders/ffmpeg_hevc_nvenc/__init__.py diff --git a/fastflix/encoders/ffmpeg_hevc_nvenc/command_builder.py b/fastflix/encoders/ffmpeg_hevc_nvenc/command_builder.py new file mode 100644 index 00000000..1c5fcfe0 --- /dev/null +++ b/fastflix/encoders/ffmpeg_hevc_nvenc/command_builder.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import re +import secrets + +from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null +from fastflix.models.encode import FFmpegNVENCSettings +from fastflix.models.fastflix import FastFlix + + +def build(fastflix: FastFlix): + settings: FFmpegNVENCSettings = fastflix.current_video.video_settings.video_encoder_settings + + beginning, ending = generate_all(fastflix, "hevc_nvenc") + + beginning += f'{f"-tune:v {settings.tune}" if settings.tune else ""} {generate_color_details(fastflix)} -spatial_aq:v {settings.spatial_aq} -tier:v {settings.tier} -rc-lookahead:v {settings.rc_lookahead} -gpu {settings.gpu} -b_ref_mode {settings.b_ref_mode} ' + + if settings.profile: + beginning += f"-profile:v {settings.profile} " + + if settings.rc: + beginning += f"-rc:v {settings.rc} " + + if settings.level: + beginning += f"-level:v {settings.level} " + + pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}" + + command_1 = ( + f"{beginning} -pass 1 " + f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} -2pass 1 ' + f'{settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}' + ) + command_2 = ( + f'{beginning} -pass 2 -passlogfile "{pass_log_file}" -2pass 1 ' + f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} " + ) + ending + return [ + Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"), + Command(command=re.sub("[ ]+", " ", command_2), name="Second pass bitrate", exe="ffmpeg"), + ] diff --git a/fastflix/encoders/hevc_nvenc/main.py b/fastflix/encoders/ffmpeg_hevc_nvenc/main.py similarity index 88% rename from fastflix/encoders/hevc_nvenc/main.py rename to fastflix/encoders/ffmpeg_hevc_nvenc/main.py index 9a7a39e6..130825ba 100644 --- a/fastflix/encoders/hevc_nvenc/main.py +++ b/fastflix/encoders/ffmpeg_hevc_nvenc/main.py @@ -5,12 +5,12 @@ import pkg_resources -name = "HEVC (nvenc)" +name = "HEVC (NVENC)" requires = "cuda-llvm" video_extension = "mkv" video_dimension_divisor = 1 -icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_x265.png")).resolve()) +icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_nvenc.png")).resolve()) enable_subtitles = True enable_audio = True @@ -101,5 +101,5 @@ "wmav2", ] -from fastflix.encoders.hevc_nvenc.command_builder import build -from fastflix.encoders.hevc_nvenc.settings_panel import NVENC as settings_panel +from fastflix.encoders.ffmpeg_hevc_nvenc.command_builder import build +from fastflix.encoders.ffmpeg_hevc_nvenc.settings_panel import NVENC as settings_panel diff --git a/fastflix/encoders/hevc_nvenc/settings_panel.py b/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py similarity index 50% rename from fastflix/encoders/hevc_nvenc/settings_panel.py rename to fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py index 83044250..1248763e 100644 --- a/fastflix/encoders/hevc_nvenc/settings_panel.py +++ b/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py @@ -6,33 +6,33 @@ from fastflix.encoders.common.setting_panel import SettingPanel from fastflix.language import t -from fastflix.models.encode import NVENCSettings +from fastflix.models.encode import FFmpegNVENCSettings from fastflix.models.fastflix_app import FastFlixApp from fastflix.shared import link +from fastflix.exceptions import FastFlixInternalException logger = logging.getLogger("fastflix") presets = [ - "default", - "slow - hq 2 passes", - "medium - hq 1 pass", - "fast - hq 1 pass", + "slow", + "medium", + "fast", "hp", "hq", "bd", - "ll - low latency", - "llhq - low latency hq", - "llhp - low latency hp", + "ll", + "llhq", + "llhp", "lossless", "losslesshp", - "p1 - fastest (lowest quality)", - "p2 - faster", - "p3 - fast", - "p4 - medium", - "p5 - slow", - "p6 - slower", - "p7 - slowest (best quality)", + "p1", + "p2", + "p3", + "p4", + "p5", + "p6", + "p7", ] recommended_bitrates = [ @@ -73,7 +73,7 @@ class NVENC(SettingPanel): - profile_name = "hevc_nvenc" + profile_name = "ffmpeg_hevc_nvenc" def __init__(self, parent, main, app: FastFlixApp): super().__init__(parent, main, app) @@ -87,7 +87,7 @@ def __init__(self, parent, main, app: FastFlixApp): self.mode = "CRF" self.updating_settings = False - grid.addLayout(self.init_modes(), 0, 2, 5, 4) + grid.addLayout(self.init_modes(), 0, 2, 3, 4) grid.addLayout(self._add_custom(), 10, 0, 1, 6) grid.addLayout(self.init_preset(), 0, 0, 1, 2) @@ -95,6 +95,19 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_tune(), 2, 0, 1, 2) grid.addLayout(self.init_profile(), 3, 0, 1, 2) grid.addLayout(self.init_pix_fmt(), 4, 0, 1, 2) + grid.addLayout(self.init_tier(), 5, 0, 1, 2) + grid.addLayout(self.init_rc(), 6, 0, 1, 2) + grid.addLayout(self.init_spatial_aq(), 7, 0, 1, 2) + + a = QtWidgets.QHBoxLayout() + a.addLayout(self.init_rc_lookahead()) + a.addStretch(1) + a.addLayout(self.init_level()) + a.addStretch(1) + a.addLayout(self.init_gpu()) + a.addStretch(1) + a.addLayout(self.init_b_ref_mode()) + grid.addLayout(a, 3, 2, 1, 4) grid.setRowStretch(9, 1) @@ -109,7 +122,7 @@ def __init__(self, parent, main, app: FastFlixApp): self.hide() def init_preset(self): - layout = self._add_combo_box( + return self._add_combo_box( label="Preset", widget_name="preset", options=presets, @@ -117,14 +130,13 @@ def init_preset(self): connect="default", opt="preset", ) - return layout def init_tune(self): return self._add_combo_box( label="Tune", widget_name="tune", - tooltip="Tune the settings for a particular type of source or situation", - options=["hq - High quality", "ll - Low Latency", "ull - Ultra Low Latency", "lossless"], + tooltip="Tune the settings for a particular type of source or situation\nhq - High Quality, ll - Low Latency, ull - Ultra Low Latency", + options=["hq", "ll", "ull", "lossless"], opt="tune", ) @@ -146,11 +158,97 @@ def init_pix_fmt(self): opt="pix_fmt", ) + def init_tier(self): + return self._add_combo_box( + label="Tier", + tooltip="Set the encoding tier", + widget_name="tier", + options=["main", "high"], + opt="tier", + ) + + def init_rc(self): + return self._add_combo_box( + label="Rate Control", + tooltip="Override the preset rate-control", + widget_name="rc", + options=[ + "default", + "vbr", + "cbr", + "vbr_minqp", + "ll_2pass_quality", + "ll_2pass_size", + "vbr_2pass", + "cbr_ld_hq", + "cbr_hq", + "vbr_hq", + ], + opt="rc", + ) + + def init_spatial_aq(self): + return self._add_combo_box( + label="Spatial AQ", + tooltip="", + widget_name="spatial_aq", + options=["off", "on"], + opt="spatial_aq", + ) + + def init_rc_lookahead(self): + return self._add_text_box( + label="RC Lookahead", + tooltip="", + widget_name="rc_lookahead", + opt="rc_lookahead", + validator="int", + default="0", + width=30, + ) + + def init_level(self): + layout = self._add_combo_box( + label="Level", + tooltip="Set the encoding level restriction", + widget_name="level", + options=["auto", "1.0", "2.0", "2.1", "3.0", "3.1", "4.0", "4.1", "5.0", "5.1", "5.2", "6.0", "6.1", "6.2"], + opt="level", + ) + self.widgets.level.setMinimumWidth(60) + return layout + + def init_gpu(self): + layout = self._add_combo_box( + label="GPU", + tooltip="Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on", + widget_name="gpu", + opt="gpu", + options=["any"] + [str(x) for x in range(8)], + ) + self.widgets.gpu.setMinimumWidth(50) + return layout + + def init_b_ref_mode(self): + layout = self._add_combo_box( + label="B Ref Mode", + tooltip="Use B frames as references", + widget_name="b_ref_mode", + opt="b_ref_mode", + options=["disabled", "each", "middle"], + ) + self.widgets.gpu.setMinimumWidth(50) + return layout + def init_modes(self): - return self._add_modes(recommended_bitrates, recommended_crfs, qp_name="cqp") + layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="qp", add_qp=False) + self.qp_radio.setChecked(False) + self.bitrate_radio.setChecked(True) + self.qp_radio.setDisabled(True) + return layout def mode_update(self): - self.widgets.custom_crf.setDisabled(self.widgets.crf.currentText() != "Custom") + self.widgets.custom_qp.setDisabled(self.widgets.qp.currentText() != "Custom") self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom") self.main.build_commands() @@ -166,17 +264,23 @@ def setting_change(self, update=True): def update_video_encoder_settings(self): tune = self.widgets.tune.currentText() - settings = NVENCSettings( + settings = FFmpegNVENCSettings( preset=self.widgets.preset.currentText().split("-")[0].strip(), max_muxing_queue_size=self.widgets.max_mux.currentText(), profile=self.widgets.profile.currentText(), pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(), extra=self.ffmpeg_extras, - tune=tune.split("-")[0].strip() if tune.lower() != "default" else None, + tune=tune.split("-")[0].strip(), extra_both_passes=self.widgets.extra_both_passes.isChecked(), + rc=self.widgets.rc.currentText() if self.widgets.rc.currentIndex() != 0 else None, + spatial_aq=self.widgets.spatial_aq.currentIndex(), + rc_lookahead=int(self.widgets.rc_lookahead.text()), + level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, + gpu=int(self.widgets.gpu.currentText()) if self.widgets.gpu.currentIndex() != 0 else -1, + b_ref_mode=self.widgets.b_ref_mode.currentText(), ) encode_type, q_value = self.get_mode_settings() - settings.cqp = q_value if encode_type == "qp" else None + settings.qp = q_value if encode_type == "qp" else None settings.bitrate = q_value if encode_type == "bitrate" else None self.app.fastflix.current_video.video_settings.video_encoder_settings = settings diff --git a/fastflix/encoders/hevc_nvenc/command_builder.py b/fastflix/encoders/hevc_nvenc/command_builder.py deleted file mode 100644 index d622e33e..00000000 --- a/fastflix/encoders/hevc_nvenc/command_builder.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -import re -import secrets - -from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null -from fastflix.models.encode import NVENCSettings -from fastflix.models.fastflix import FastFlix - - -def build(fastflix: FastFlix): - settings: NVENCSettings = fastflix.current_video.video_settings.video_encoder_settings - - beginning, ending = generate_all(fastflix, "hevc_nvenc") - - beginning += f'{f"-tune {settings.tune}" if settings.tune else ""} ' f"{generate_color_details(fastflix)} " - - if settings.profile and settings.profile != "default": - beginning += f"-profile:v {settings.profile} " - - pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}" - - if settings.bitrate: - command_1 = ( - f"{beginning} -pass 1 " - f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}' - ) - command_2 = ( - f'{beginning} -pass 2 -passlogfile "{pass_log_file}" ' - f"-b:v {settings.bitrate} -preset {settings.preset} {settings.extra} " - ) + ending - return [ - Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"), - Command(command=re.sub("[ ]+", " ", command_2), name="Second pass bitrate", exe="ffmpeg"), - ] - - elif settings.crf: - command = f"{beginning} -crf {settings.crf} " f"-preset {settings.preset} {settings.extra} {ending}" - return [Command(command=re.sub("[ ]+", " ", command), name="Single pass CQP", exe="ffmpeg")] - - else: - return [] diff --git a/fastflix/encoders/hevc_x265/command_builder.py b/fastflix/encoders/hevc_x265/command_builder.py index ab6238a4..fbafb656 100644 --- a/fastflix/encoders/hevc_x265/command_builder.py +++ b/fastflix/encoders/hevc_x265/command_builder.py @@ -83,7 +83,7 @@ def build(fastflix: FastFlix): beginning, ending = generate_all(fastflix, "libx265") if settings.tune and settings.tune != "default": - beginning += f"-tune {settings.tune} " + beginning += f"-tune:v {settings.tune} " if settings.profile and settings.profile != "default": beginning += f"-profile:v {settings.profile} " @@ -170,12 +170,12 @@ def get_x265_params(params=()): if settings.bitrate: command_1 = ( f'{beginning} {get_x265_params(["pass=1", "no-slow-firstpass=1"])} ' - f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset {settings.preset} {settings.extra if settings.extra_both_passes else ""} ' + f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} ' f" -an -sn -dn -f mp4 {null}" ) command_2 = ( f'{beginning} {get_x265_params(["pass=2"])} -passlogfile "{pass_log_file}" ' - f"-b:v {settings.bitrate} -preset {settings.preset} {settings.extra} {ending}" + f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} {ending}" ) return [ Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"), @@ -184,8 +184,8 @@ def get_x265_params(params=()): elif settings.crf: command = ( - f"{beginning} {get_x265_params()} -crf {settings.crf} " - f"-preset {settings.preset} {settings.extra} {ending}" + f"{beginning} {get_x265_params()} -crf:v {settings.crf} " + f"-preset:v {settings.preset} {settings.extra} {ending}" ) return [Command(command=re.sub("[ ]+", " ", command), name="Single pass CRF", exe="ffmpeg")] diff --git a/fastflix/models/config.py b/fastflix/models/config.py index e2401ac3..a06a06f6 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -14,7 +14,7 @@ AOMAV1Settings, CopySettings, GIFSettings, - NVENCSettings, + FFmpegNVENCSettings, SVTAV1Settings, VP9Settings, WebPSettings, @@ -41,7 +41,7 @@ "gif": GIFSettings, "webp": WebPSettings, "copy_settings": CopySettings, - "hevc_nvenc": NVENCSettings, + "ffmpeg_hevc_nvenc": FFmpegNVENCSettings, } outdated_settings = ("copy",) @@ -79,7 +79,7 @@ class Profile(BaseModel): gif: Optional[GIFSettings] = None webp: Optional[WebPSettings] = None copy_settings: Optional[CopySettings] = None - hevc_nvenc: Optional[NVENCSettings] = None + ffmpeg_hevc_nvenc: Optional[FFmpegNVENCSettings] = None empty_profile = Profile(x265=x265Settings()) diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index 6242b8fb..1eaf1bbd 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -73,14 +73,22 @@ class x264Settings(EncoderSettings): bitrate: Optional[str] = None -class NVENCSettings(EncoderSettings): - name = "HEVC (nvenc)" - preset: str = "p7" - profile: str = "default" - tune: Optional[str] = None - pix_fmt: str = "yuv420p" - cqp: Optional[Union[int, float]] = None +class FFmpegNVENCSettings(EncoderSettings): + name = "HEVC (NVENC)" + preset: str = "slow" + profile: str = "main" + tune: str = "hq" + pix_fmt: str = "p010le" bitrate: Optional[str] = "6000k" + qp: Optional[str] = None + cq: int = 0 + spatial_aq: int = 0 + rc_lookahead: int = 0 + rc: Optional[str] = None + tier: str = "main" + level: Optional[str] = None + gpu: int = -1 + b_ref_mode: str = "disabled" class rav1eSettings(EncoderSettings): diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 2e3018ff..2e5f0efe 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -12,7 +12,7 @@ AudioTrack, CopySettings, GIFSettings, - NVENCSettings, + FFmpegNVENCSettings, SubtitleTrack, SVTAV1Settings, VP9Settings, @@ -64,7 +64,7 @@ class VideoSettings(BaseModel): GIFSettings, WebPSettings, CopySettings, - NVENCSettings, + FFmpegNVENCSettings, ] = None audio_tracks: List[AudioTrack] = Field(default_factory=list) subtitle_tracks: List[SubtitleTrack] = Field(default_factory=list) diff --git a/fastflix/shared.py b/fastflix/shared.py index 5f39cd38..3e0a41d2 100644 --- a/fastflix/shared.py +++ b/fastflix/shared.py @@ -30,9 +30,11 @@ NIM_MODIFY, WNDCLASS, CreateWindow, + DestroyWindow, LoadImage, RegisterClass, Shell_NotifyIcon, + UnregisterClass, UpdateWindow, ) @@ -282,19 +284,45 @@ def timedelta_to_str(delta): return output_string +tool_window = None +tool_icon = None + + def show_windows_notification(title, msg, icon_path): + global tool_window, tool_icon + wc = WNDCLASS() hinst = wc.hInstance = GetModuleHandle(None) wc.lpszClassName = "FastFlix" - tool_window = CreateWindow( - RegisterClass(wc), "Taskbar", WS_OVERLAPPED | WS_SYSMENU, 0, 0, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, hinst, None + if not tool_window: + tool_window = CreateWindow( + RegisterClass(wc), + "Taskbar", + WS_OVERLAPPED | WS_SYSMENU, + 0, + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + 0, + 0, + hinst, + None, + ) + UpdateWindow(tool_window) + + icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE + tool_icon = LoadImage(hinst, icon_path, IMAGE_ICON, 0, 0, icon_flags) + + flags = NIF_ICON | NIF_MESSAGE | NIF_TIP + nid = (tool_window, 0, flags, WM_USER + 20, tool_icon, "FastFlix Notifications") + Shell_NotifyIcon(NIM_ADD, nid) + + Shell_NotifyIcon( + NIM_MODIFY, (tool_window, 0, NIF_INFO, WM_USER + 20, tool_icon, "Balloon Tooltip", msg, 200, title, 4) ) - UpdateWindow(tool_window) - icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE - icon = LoadImage(hinst, icon_path, IMAGE_ICON, 0, 0, icon_flags) - flags = NIF_ICON | NIF_MESSAGE | NIF_TIP - nid = (tool_window, 0, flags, WM_USER + 20, icon, "Tooltip") - Shell_NotifyIcon(NIM_ADD, nid) - Shell_NotifyIcon(NIM_MODIFY, (tool_window, 0, NIF_INFO, WM_USER + 20, icon, "Balloon Tooltip", msg, 200, title, 4)) +def cleanup_windows_notification(): + if tool_window: + DestroyWindow(tool_window) + UnregisterClass("FastFlix", None) diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 862f4c9b..935cbe11 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -17,7 +17,7 @@ from fastflix.models.fastflix_app import FastFlixApp from fastflix.program_downloads import latest_ffmpeg from fastflix.resources import main_icon -from fastflix.shared import clean_logs, error_message, latest_fastflix, message +from fastflix.shared import clean_logs, error_message, latest_fastflix, message, cleanup_windows_notification from fastflix.widgets.about import About from fastflix.widgets.changes import Changes from fastflix.widgets.logs import Logs @@ -79,6 +79,7 @@ def closeEvent(self, a0: QtGui.QCloseEvent) -> None: shutil.rmtree(item, ignore_errors=True) if item.name.lower().endswith((".jpg", ".jpeg", ".png", ".gif")): item.unlink() + cleanup_windows_notification() self.main.close(from_container=True) super(Container, self).closeEvent(a0) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 27ba747e..159891e3 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -467,7 +467,7 @@ def transpose_to_rotation(self, transpose): def change_output_types(self): self.widgets.convert_to.clear() - self.widgets.convert_to.addItems([f" {x}" for x in self.app.fastflix.encoders.keys()]) + self.widgets.convert_to.addItems([f" {x}" for x in self.app.fastflix.encoders.keys()]) for i, plugin in enumerate(self.app.fastflix.encoders.values()): if getattr(plugin, "icon", False): self.widgets.convert_to.setItemIcon(i, QtGui.QIcon(plugin.icon)) @@ -1566,7 +1566,10 @@ def conversion_complete(self, return_code): else: self.video_options.show_queue() if reusables.win_based: - show_windows_notification("FastFlix", t("All queue items have completed"), icon_path=main_icon) + try: + show_windows_notification("FastFlix", t("All queue items have completed"), icon_path=main_icon) + except Exception: + message(t("All queue items have completed"), title=t("Success")) else: message(t("All queue items have completed"), title=t("Success")) diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py index 30eb8787..ff88b0a3 100644 --- a/fastflix/widgets/video_options.py +++ b/fastflix/widgets/video_options.py @@ -139,7 +139,17 @@ def update_profile(self): self.main.container.profile.update_settings() def reload(self): - self.current_settings.reload() + self.change_conversion(self.app.fastflix.current_video.video_settings.video_encoder_settings.name) + self.main.widgets.convert_to.setCurrentIndex( + list(self.app.fastflix.encoders.keys()).index( + self.app.fastflix.current_video.video_settings.video_encoder_settings.name + ) + ) + try: + self.current_settings.reload() + except Exception: + logger.exception("Should not have happened, could not reload from queue") + return if self.app.fastflix.current_video: streams = copy.deepcopy(self.app.fastflix.current_video.streams) settings = copy.deepcopy(self.app.fastflix.current_video.video_settings) From f3651a8013364dbc67a6fba58e0d981732c239c8 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 27 Jan 2021 20:41:51 -0600 Subject: [PATCH 13/50] * Adding NVEenC initial trial for HEVC --- CHANGES | 1 + fastflix/application.py | 4 + fastflix/encoders/common/setting_panel.py | 31 +- .../ffmpeg_hevc_nvenc/settings_panel.py | 17 +- fastflix/encoders/hevc_x265/settings_panel.py | 27 -- fastflix/encoders/nvencc_hevc/__init__.py | 0 .../encoders/nvencc_hevc/command_builder.py | 97 ++++++ fastflix/encoders/nvencc_hevc/main.py | 104 ++++++ .../encoders/nvencc_hevc/settings_panel.py | 311 ++++++++++++++++++ fastflix/models/config.py | 8 +- fastflix/models/encode.py | 14 + fastflix/models/video.py | 2 + 12 files changed, 579 insertions(+), 37 deletions(-) create mode 100644 fastflix/encoders/nvencc_hevc/__init__.py create mode 100644 fastflix/encoders/nvencc_hevc/command_builder.py create mode 100644 fastflix/encoders/nvencc_hevc/main.py create mode 100644 fastflix/encoders/nvencc_hevc/settings_panel.py diff --git a/CHANGES b/CHANGES index bafed885..2ea5a483 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,7 @@ ## Version 4.2.0 * Adding #109 NVENC HEVC support based on FFmpeg +* Adding NVEenC initial trial for HEVC * Adding ability to extract HDR10+ metadata if hdr10plus_parser is detected on path * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Adding Windows 10 notification for queue complete success diff --git a/fastflix/application.py b/fastflix/application.py index 7d43da1a..e00f4105 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -55,6 +55,7 @@ def init_encoders(app: FastFlixApp, **_): from fastflix.encoders.svt_av1 import main as svt_av1_plugin from fastflix.encoders.vp9 import main as vp9_plugin from fastflix.encoders.webp import main as webp_plugin + from fastflix.encoders.nvencc_hevc import main as nvencc_plugin encoders = [ hevc_plugin, @@ -69,6 +70,9 @@ def init_encoders(app: FastFlixApp, **_): copy_plugin, ] + if app.fastflix.config.nvencc: + encoders.insert(len(encoders) - 1, nvencc_plugin) + app.fastflix.encoders = { encoder.name: encoder for encoder in encoders diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index f0cc1b93..cdb2117a 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- import logging from typing import List, Tuple, Union +from pathlib import Path from box import Box -from qtpy import QtGui, QtWidgets +from qtpy import QtGui, QtWidgets, QtCore from fastflix.exceptions import FastFlixInternalException from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp +from fastflix.widgets.background_tasks import ExtractHDR10 logger = logging.getLogger("fastflix") @@ -214,6 +216,33 @@ def _add_file_select(self, label, widget_name, button_action, connect="default", layout.addWidget(button) return layout + def extract_hdr10plus(self): + self.extract_button.hide() + self.extract_label.show() + self.movie.start() + # self.extracting_hdr10 = True + self.extract_thrad = ExtractHDR10(self.app, self.main, signal=self.hdr10plus_signal) + self.extract_thrad.start() + + def done_hdr10plus_extract(self, metadata: str): + self.extract_button.show() + self.extract_label.hide() + self.movie.stop() + if Path(metadata).exists(): + self.widgets.hdr10plus_metadata.setText(metadata) + + def dhdr10_update(self): + dirname = Path(self.widgets.hdr10plus_metadata.text()).parent + if not dirname.exists(): + dirname = Path() + filename = QtWidgets.QFileDialog.getOpenFileName( + self, caption="hdr10_metadata", directory=str(dirname), filter="HDR10+ Metadata (*.json)" + ) + if not filename or not filename[0]: + return + self.widgets.hdr10plus_metadata.setText(filename[0]) + self.main.page_update() + def _add_modes( self, recommended_bitrates, diff --git a/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py b/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py index 1248763e..feff57b1 100644 --- a/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py +++ b/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py @@ -111,12 +111,12 @@ def __init__(self, parent, main, app: FastFlixApp): grid.setRowStretch(9, 1) - guide_label = QtWidgets.QLabel( - link("https://trac.ffmpeg.org/wiki/Encode/H.264", t("FFMPEG AVC / H.264 Encoding Guide")) - ) - guide_label.setAlignment(QtCore.Qt.AlignBottom) - guide_label.setOpenExternalLinks(True) - grid.addWidget(guide_label, 11, 0, 1, 6) + # guide_label = QtWidgets.QLabel( + # link("https://trac.ffmpeg.org/wiki/Encode/H.264", t("FFMPEG AVC / H.264 Encoding Guide")) + # ) + # guide_label.setAlignment(QtCore.Qt.AlignBottom) + # guide_label.setOpenExternalLinks(True) + # grid.addWidget(guide_label, 11, 0, 1, 6) self.setLayout(grid) self.hide() @@ -274,10 +274,11 @@ def update_video_encoder_settings(self): extra_both_passes=self.widgets.extra_both_passes.isChecked(), rc=self.widgets.rc.currentText() if self.widgets.rc.currentIndex() != 0 else None, spatial_aq=self.widgets.spatial_aq.currentIndex(), - rc_lookahead=int(self.widgets.rc_lookahead.text()), + rc_lookahead=int(self.widgets.rc_lookahead.text() or 0), level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, - gpu=int(self.widgets.gpu.currentText()) if self.widgets.gpu.currentIndex() != 0 else -1, + gpu=int(self.widgets.gpu.currentText() or -1) if self.widgets.gpu.currentIndex() != 0 else -1, b_ref_mode=self.widgets.b_ref_mode.currentText(), + tier=self.widgets.tier.currentText(), ) encode_type, q_value = self.get_mode_settings() settings.qp = q_value if encode_type == "qp" else None diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index d0c68245..dce3ee9f 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -173,21 +173,6 @@ def init_dhdr10_warning_and_opt(self): layout.addLayout(self.init_dhdr10_opt()) return layout - def extract_hdr10plus(self): - self.extract_button.hide() - self.extract_label.show() - self.movie.start() - # self.extracting_hdr10 = True - self.extract_thrad = ExtractHDR10(self.app, self.main, signal=self.hdr10plus_signal) - self.extract_thrad.start() - - def done_hdr10plus_extract(self, metadata: str): - self.extract_button.show() - self.extract_label.hide() - self.movie.stop() - if Path(metadata).exists(): - self.widgets.hdr10plus_metadata.setText(metadata) - def init_x265_row(self): layout = QtWidgets.QHBoxLayout() layout.addLayout(self.init_hdr10()) @@ -465,18 +450,6 @@ def init_x265_params(self): layout.addWidget(self.widgets.x265_params) return layout - def dhdr10_update(self): - dirname = Path(self.widgets.hdr10plus_metadata.text()).parent - if not dirname.exists(): - dirname = Path() - filename = QtWidgets.QFileDialog.getOpenFileName( - self, caption="hdr10_metadata", directory=str(dirname), filter="HDR10+ Metadata (*.json)" - ) - if not filename or not filename[0]: - return - self.widgets.hdr10plus_metadata.setText(filename[0]) - self.main.page_update() - def setting_change(self, update=True, pix_change=False): def hdr_opts(): if not self.widgets.pix_fmt.currentText().startswith( diff --git a/fastflix/encoders/nvencc_hevc/__init__.py b/fastflix/encoders/nvencc_hevc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py new file mode 100644 index 00000000..6d98e81e --- /dev/null +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +import re +import secrets + +from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null +from fastflix.models.encode import NVEncCSettings +from fastflix.models.fastflix import FastFlix +from fastflix.flix import unixy + + +def build(fastflix: FastFlix): + settings: NVEncCSettings = fastflix.current_video.video_settings.video_encoder_settings + + # beginning, ending = generate_all(fastflix, "hevc_nvenc") + + # beginning += f'{f"-tune:v {settings.tune}" if settings.tune else ""} {generate_color_details(fastflix)} -spatial_aq:v {settings.spatial_aq} -tier:v {settings.tier} -rc-lookahead:v {settings.rc_lookahead} -gpu {settings.gpu} -b_ref_mode {settings.b_ref_mode} ' + + # --profile main10 --tier main + master_display = None + if fastflix.current_video.master_display: + master_display = ( + f'--master-display "G{fastflix.current_video.master_display.green}' + f"B{fastflix.current_video.master_display.blue}" + f"R{fastflix.current_video.master_display.red}" + f"WP{fastflix.current_video.master_display.white}" + f'L{fastflix.current_video.master_display.luminance}"' + ) + + max_cll = None + if fastflix.current_video.cll: + max_cll = f'--max-cll "{fastflix.current_video.cll}"' + + dhdr = None + if settings.hdr10plus_metadata: + dhdr = f'--dhdr10-info "{settings.hdr10plus_metadata}"' + + command = [ + f'"{unixy(fastflix.config.nvencc)}"', + "-i", + f'"{fastflix.current_video.source}"', + "-c", + "hevc", + "--vbr", + settings.bitrate, + "--preset", + settings.preset, + "--profile", + settings.profile, + "--tier", + settings.tier, + f'{f"--lookahead {settings.lookahead}" if settings.lookahead else ""}', + f'{"--aq" if settings.spatial_aq else "--no-aq"}', + "--colormatrix", + (fastflix.current_video.video_settings.color_space or "auto"), + "--transfer", + (fastflix.current_video.video_settings.color_transfer or "auto"), + "--colorprim", + (fastflix.current_video.video_settings.color_primaries or "auto"), + f'{master_display if master_display else ""}', + f'{max_cll if max_cll else ""}', + f'{dhdr if dhdr else ""}', + "--output-depth", + str(fastflix.current_video.current_video_stream.bit_depth), + "-o", + f'"{unixy(fastflix.current_video.video_settings.output_path)}"', + ] + + return [Command(command=" ".join(command), name="NVEncC Encode")] + + +# -i "Beverly Hills Duck Pond - HDR10plus - Jessica Payne.mp4" -c hevc --profile main10 --tier main --output-depth 10 --vbr 6000k --preset quality --multipass 2pass-full --aq --repeat-headers --colormatrix bt2020nc --transfer smpte2084 --colorprim bt2020 --lookahead 16 -o "nvenc-6000k.mkv" + +# +# if settings.profile: +# beginning += f"-profile:v {settings.profile} " +# +# if settings.rc: +# beginning += f"-rc:v {settings.rc} " +# +# if settings.level: +# beginning += f"-level:v {settings.level} " +# +# pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}" +# +# command_1 = ( +# f"{beginning} -pass 1 " +# f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} -2pass 1 ' +# f'{settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}' +# ) +# command_2 = ( +# f'{beginning} -pass 2 -passlogfile "{pass_log_file}" -2pass 1 ' +# f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} " +# ) + ending +# return [ +# Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"), +# Command(command=re.sub("[ ]+", " ", command_2), name="Second pass bitrate", exe="ffmpeg"), +# ] diff --git a/fastflix/encoders/nvencc_hevc/main.py b/fastflix/encoders/nvencc_hevc/main.py new file mode 100644 index 00000000..0c368765 --- /dev/null +++ b/fastflix/encoders/nvencc_hevc/main.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +__author__ = "Chris Griffith" +from pathlib import Path + +import pkg_resources + +name = "HEVC (NVEncC)" + +video_extension = "mkv" +video_dimension_divisor = 1 +icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_nvencc.png")).resolve()) + +enable_subtitles = True +enable_audio = True +enable_attachments = True + +audio_formats = [ + "aac", + "aac_mf", + "libfdk_aac", + "ac3", + "ac3_fixed", + "ac3_mf", + "adpcm_adx", + "g722", + "g726", + "g726le", + "adpcm_ima_qt", + "adpcm_ima_ssi", + "adpcm_ima_wav", + "adpcm_ms", + "adpcm_swf", + "adpcm_yamaha", + "alac", + "libopencore_amrnb", + "libvo_amrwbenc", + "aptx", + "aptx_hd", + "comfortnoise", + "dca", + "eac3", + "flac", + "g723_1", + "libgsm", + "libgsm_ms", + "libilbc", + "mlp", + "mp2", + "mp2fixed", + "libtwolame", + "mp3_mf", + "libmp3lame", + "nellymoser", + "opus", + "libopus", + "pcm_alaw", + "pcm_dvd", + "pcm_f32be", + "pcm_f32le", + "pcm_f64be", + "pcm_f64le", + "pcm_mulaw", + "pcm_s16be", + "pcm_s16be_planar", + "pcm_s16le", + "pcm_s16le_planar", + "pcm_s24be", + "pcm_s24daud", + "pcm_s24le", + "pcm_s24le_planar", + "pcm_s32be", + "pcm_s32le", + "pcm_s32le_planar", + "pcm_s64be", + "pcm_s64le", + "pcm_s8", + "pcm_s8_planar", + "pcm_u16be", + "pcm_u16le", + "pcm_u24be", + "pcm_u24le", + "pcm_u32be", + "pcm_u32le", + "pcm_u8", + "pcm_vidc", + "real_144", + "roq_dpcm", + "s302m", + "sbc", + "sonic", + "sonicls", + "libspeex", + "truehd", + "tta", + "vorbis", + "libvorbis", + "wavpack", + "wmav1", + "wmav2", +] + +from fastflix.encoders.nvencc_hevc.command_builder import build +from fastflix.encoders.nvencc_hevc.settings_panel import NVENCC as settings_panel diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py new file mode 100644 index 00000000..3d382319 --- /dev/null +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +import logging + +from box import Box +from qtpy import QtCore, QtWidgets, QtGui + +from fastflix.encoders.common.setting_panel import SettingPanel +from fastflix.language import t +from fastflix.models.encode import NVEncCSettings +from fastflix.models.fastflix_app import FastFlixApp +from fastflix.shared import link +from fastflix.exceptions import FastFlixInternalException +from fastflix.resources import loading_movie + +logger = logging.getLogger("fastflix") + + +presets = ["default", "performance", "quality"] + +recommended_bitrates = [ + "800k (320x240p @ 30fps)", + "1000k (640x360p @ 30fps)", + "1500k (640x480p @ 30fps)", + "2000k (1280x720p @ 30fps)", + "5000k (1280x720p @ 60fps)", + "6000k (1080p @ 30fps)", + "9000k (1080p @ 60fps)", + "15000k (1440p @ 30fps)", + "25000k (1440p @ 60fps)", + "35000k (2160p @ 30fps)", + "50000k (2160p @ 60fps)", + "Custom", +] + +recommended_crfs = [ + "28", + "27", + "26", + "25", + "24", + "23", + "22", + "21", + "20", + "19", + "18", + "17", + "16", + "15", + "14", + "Custom", +] + +pix_fmts = ["8-bit: yuv420p", "10-bit: p010le"] + + +class NVENCC(SettingPanel): + profile_name = "nvencc_hevc" + hdr10plus_signal = QtCore.Signal(str) + + def __init__(self, parent, main, app: FastFlixApp): + super().__init__(parent, main, app) + self.main = main + self.app = app + + grid = QtWidgets.QGridLayout() + + self.widgets = Box(mode=None) + + self.mode = "CRF" + self.updating_settings = False + + grid.addLayout(self.init_modes(), 0, 2, 3, 4) + grid.addLayout(self._add_custom(), 10, 0, 1, 6) + + grid.addLayout(self.init_preset(), 0, 0, 1, 2) + # grid.addLayout(self.init_max_mux(), 1, 0, 1, 2) + # grid.addLayout(self.init_tune(), 2, 0, 1, 2) + grid.addLayout(self.init_profile(), 1, 0, 1, 2) + # grid.addLayout(self.init_pix_fmt(), 4, 0, 1, 2) + grid.addLayout(self.init_tier(), 2, 0, 1, 2) + + grid.addLayout(self.init_spatial_aq(), 3, 0, 1, 2) + grid.addLayout(self.init_lookahead(), 4, 0, 1, 2) + + grid.addLayout(self.init_dhdr10_info(), 4, 2, 1, 3) + grid.addLayout(self.init_dhdr10_warning_and_opt(), 4, 5, 1, 1) + + # a = QtWidgets.QHBoxLayout() + # a.addLayout(self.init_rc_lookahead()) + # a.addStretch(1) + # a.addLayout(self.init_level()) + # a.addStretch(1) + # a.addLayout(self.init_gpu()) + # a.addStretch(1) + # a.addLayout(self.init_b_ref_mode()) + # grid.addLayout(a, 3, 2, 1, 4) + + grid.setRowStretch(9, 1) + + # guide_label = QtWidgets.QLabel( + # link("https://trac.ffmpeg.org/wiki/Encode/H.264", t("FFMPEG AVC / H.264 Encoding Guide")) + # ) + # guide_label.setAlignment(QtCore.Qt.AlignBottom) + # guide_label.setOpenExternalLinks(True) + # grid.addWidget(guide_label, 11, 0, 1, 6) + + self.setLayout(grid) + self.hide() + self.hdr10plus_signal.connect(self.done_hdr10plus_extract) + + def init_preset(self): + return self._add_combo_box( + label="Preset", + widget_name="preset", + options=presets, + tooltip=("preset: The slower the preset, the better the compression and quality"), + connect="default", + opt="preset", + ) + + def init_tune(self): + return self._add_combo_box( + label="Tune", + widget_name="tune", + tooltip="Tune the settings for a particular type of source or situation\nhq - High Quality, ll - Low Latency, ull - Ultra Low Latency", + options=["hq", "ll", "ull", "lossless"], + opt="tune", + ) + + def init_profile(self): + return self._add_combo_box( + label="Profile_encoderopt", + widget_name="profile", + tooltip="Enforce an encode profile", + options=["main", "main10", "main444"], + opt="profile", + ) + + def init_pix_fmt(self): + return self._add_combo_box( + label="Bit Depth", + tooltip="Pixel Format (requires at least 10-bit for HDR)", + widget_name="pix_fmt", + options=pix_fmts, + opt="pix_fmt", + ) + + def init_tier(self): + return self._add_combo_box( + label="Tier", + tooltip="Set the encoding tier", + widget_name="tier", + options=["main", "high"], + opt="tier", + ) + + def init_rc(self): + return self._add_combo_box( + label="Rate Control", + tooltip="Override the preset rate-control", + widget_name="rc", + options=[ + "default", + "vbr", + "cbr", + "vbr_minqp", + "ll_2pass_quality", + "ll_2pass_size", + "vbr_2pass", + "cbr_ld_hq", + "cbr_hq", + "vbr_hq", + ], + opt="rc", + ) + + def init_spatial_aq(self): + return self._add_combo_box( + label="Spatial AQ", + tooltip="", + widget_name="spatial_aq", + options=["off", "on"], + opt="spatial_aq", + ) + + def init_lookahead(self): + return self._add_combo_box( + label="Lookahead", + tooltip="", + widget_name="lookahead", + opt="lookahead", + options=["off"] + [str(x) for x in range(1, 33)], + ) + + def init_level(self): + layout = self._add_combo_box( + label="Level", + tooltip="Set the encoding level restriction", + widget_name="level", + options=["auto", "1.0", "2.0", "2.1", "3.0", "3.1", "4.0", "4.1", "5.0", "5.1", "5.2", "6.0", "6.1", "6.2"], + opt="level", + ) + self.widgets.level.setMinimumWidth(60) + return layout + + def init_gpu(self): + layout = self._add_combo_box( + label="GPU", + tooltip="Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on", + widget_name="gpu", + opt="gpu", + options=["any"] + [str(x) for x in range(8)], + ) + self.widgets.gpu.setMinimumWidth(50) + return layout + + def init_b_ref_mode(self): + layout = self._add_combo_box( + label="B Ref Mode", + tooltip="Use B frames as references", + widget_name="b_ref_mode", + opt="b_ref_mode", + options=["disabled", "each", "middle"], + ) + self.widgets.gpu.setMinimumWidth(50) + return layout + + def init_dhdr10_info(self): + layout = self._add_file_select( + label="HDR10+ Metadata", + widget_name="hdr10plus_metadata", + button_action=lambda: self.dhdr10_update(), + tooltip="dhdr10_info: Path to HDR10+ JSON metadata file", + ) + self.labels["hdr10plus_metadata"].setFixedWidth(200) + return layout + + def init_dhdr10_warning_and_opt(self): + layout = QtWidgets.QHBoxLayout() + + self.extract_button = QtWidgets.QPushButton(t("Extract HDR10+")) + self.extract_button.hide() + self.extract_button.clicked.connect(self.extract_hdr10plus) + + self.extract_label = QtWidgets.QLabel(self) + self.extract_label.hide() + self.movie = QtGui.QMovie(loading_movie) + self.movie.setScaledSize(QtCore.QSize(25, 25)) + self.extract_label.setMovie(self.movie) + + layout.addWidget(self.extract_button) + layout.addWidget(self.extract_label) + return layout + + def init_modes(self): + layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="qp", add_qp=False) + self.qp_radio.setChecked(False) + self.bitrate_radio.setChecked(True) + self.qp_radio.setDisabled(True) + return layout + + def mode_update(self): + self.widgets.custom_qp.setDisabled(self.widgets.qp.currentText() != "Custom") + self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom") + self.main.build_commands() + + def setting_change(self, update=True): + if self.updating_settings: + return + self.updating_settings = True + + if update: + self.main.page_update() + self.updating_settings = False + + def update_video_encoder_settings(self): + + settings = NVEncCSettings( + preset=self.widgets.preset.currentText().split("-")[0].strip(), + profile=self.widgets.profile.currentText(), + tier=self.widgets.tier.currentText(), + lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None, + spatial_aq=bool(self.widgets.spatial_aq.currentIndex()), + hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"), + # pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(), + # extra=self.ffmpeg_extras, + # tune=tune.split("-")[0].strip(), + # extra_both_passes=self.widgets.extra_both_passes.isChecked(), + # rc=self.widgets.rc.currentText() if self.widgets.rc.currentIndex() != 0 else None, + # spatial_aq=self.widgets.spatial_aq.currentIndex(), + # rc_lookahead=int(self.widgets.rc_lookahead.text() or 0), + # level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, + # gpu=int(self.widgets.gpu.currentText() or -1) if self.widgets.gpu.currentIndex() != 0 else -1, + # b_ref_mode=self.widgets.b_ref_mode.currentText(), + ) + encode_type, q_value = self.get_mode_settings() + settings.qp = q_value if encode_type == "qp" else None + settings.bitrate = q_value if encode_type == "bitrate" else None + self.app.fastflix.current_video.video_settings.video_encoder_settings = settings + + def set_mode(self, x): + self.mode = x.text() + self.main.build_commands() + + def new_source(self): + super().new_source() + if self.app.fastflix.current_video.hdr10_plus: + self.extract_button.show() + else: + self.extract_button.hide() diff --git a/fastflix/models/config.py b/fastflix/models/config.py index a06a06f6..a11aa8c1 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -21,6 +21,7 @@ rav1eSettings, x264Settings, x265Settings, + NVEncCSettings, ) from fastflix.version import __version__ @@ -42,6 +43,7 @@ "webp": WebPSettings, "copy_settings": CopySettings, "ffmpeg_hevc_nvenc": FFmpegNVENCSettings, + "nvencc_hevc": NVEncCSettings, } outdated_settings = ("copy",) @@ -80,6 +82,7 @@ class Profile(BaseModel): webp: Optional[WebPSettings] = None copy_settings: Optional[CopySettings] = None ffmpeg_hevc_nvenc: Optional[FFmpegNVENCSettings] = None + nvencc_hevc: Optional[NVEncCSettings] = None empty_profile = Profile(x265=x265Settings()) @@ -129,6 +132,7 @@ class Config(BaseModel): ffprobe: Path = Field(default_factory=lambda: find_ffmpeg_file("ffprobe")) hdr10plus_parser: Optional[Path] = Field(default_factory=lambda: where("hdr10plus_parser")) mkvpropedit: Optional[Path] = Field(default_factory=lambda: where("mkvpropedit")) + nvencc: Optional[Path] = Field(default_factory=lambda: where("NVEncC")) flat_ui: bool = True language: str = "en" logging_level: int = 10 @@ -198,7 +202,7 @@ def load(self): "there may be non-recoverable errors while loading it." ) - paths = ("work_path", "ffmpeg", "ffprobe", "hdr10plus_parser", "mkvpropedit") + paths = ("work_path", "ffmpeg", "ffprobe", "hdr10plus_parser", "mkvpropedit", "nvencc") for key, value in data.items(): if key == "profiles": self.profiles = {} @@ -239,6 +243,8 @@ def load(self): self.hdr10plus_parser = where("hdr10plus_parser") if not self.mkvpropedit: self.mkvpropedit = where("mkvpropedit") + if not self.nvencc: + self.mkvpropedit = where("NVEncC") self.profiles.update(get_preset_defaults()) if self.selected_profile not in self.profiles: diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index 1eaf1bbd..cb2222d9 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -91,6 +91,20 @@ class FFmpegNVENCSettings(EncoderSettings): b_ref_mode: str = "disabled" +class NVEncCSettings(EncoderSettings): + name = "HEVC (NVEncC)" + preset: str = "quality" + profile: str = "main" + bitrate: Optional[str] = "6000k" + qp: Optional[str] = None + cq: int = 0 + spatial_aq: bool = False + lookahead: Optional[int] = None + tier: str = "main" + level: Optional[str] = None + hdr10plus_metadata: str = "" + + class rav1eSettings(EncoderSettings): name = "AV1 (rav1e)" speed: str = "-1" diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 2e5f0efe..c2bf6396 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -20,6 +20,7 @@ rav1eSettings, x264Settings, x265Settings, + NVEncCSettings, ) __all__ = ["VideoSettings", "Status", "Video"] @@ -65,6 +66,7 @@ class VideoSettings(BaseModel): WebPSettings, CopySettings, FFmpegNVENCSettings, + NVEncCSettings, ] = None audio_tracks: List[AudioTrack] = Field(default_factory=list) subtitle_tracks: List[SubtitleTrack] = Field(default_factory=list) From 64c7eb0a404da7a0740c0ba33d547d39fae5281b Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 27 Jan 2021 23:02:34 -0600 Subject: [PATCH 14/50] figuring out start of audio for nvencc --- fastflix/encoders/common/audio.py | 2 +- .../encoders/nvencc_hevc/command_builder.py | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/fastflix/encoders/common/audio.py b/fastflix/encoders/common/audio.py index 11fa46d7..a42fd661 100644 --- a/fastflix/encoders/common/audio.py +++ b/fastflix/encoders/common/audio.py @@ -7,7 +7,6 @@ def build_audio(audio_tracks, audio_file_index=0): command_list = [] for track in audio_tracks: - downmix = f"-ac:{track.outdex} {track.downmix}" if track.downmix > 0 else "" command_list.append( f"-map {audio_file_index}:{track.index} " f'-metadata:s:{track.outdex} title="{track.title}" ' @@ -18,6 +17,7 @@ def build_audio(audio_tracks, audio_file_index=0): if not track.conversion_codec or track.conversion_codec == "none": command_list.append(f"-c:{track.outdex} copy") elif track.conversion_codec: + downmix = f"-ac:{track.outdex} {track.downmix}" if track.downmix > 0 else "" bitrate = "" if track.conversion_codec not in lossless: bitrate = f"-b:{track.outdex} {track.conversion_bitrate} " diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 6d98e81e..38fe8198 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -7,6 +7,33 @@ from fastflix.models.fastflix import FastFlix from fastflix.flix import unixy +lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] + + +def build_audio(audio_tracks, audio_file_index=0): + # TODO figure out copy and downmix + # https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md#--audio-stream-intorstringstring1string2 + command_list = [] + copies = [] + + for track in audio_tracks: + command_list.append( + f'--audio-metadata {track.outdex}?title="{track.title}" ' + f'--audio-metadata {track.outdex}?handler="{track.title}" ' + ) + if track.language: + command_list.append(f"--audio-metadata {track.outdex}?language={track.language}") + if not track.conversion_codec or track.conversion_codec == "none": + copies.append(str(track.outdex)) + elif track.conversion_codec: + # downmix = f"-ac:{track.outdex} {track.downmix}" if track.downmix > 0 else "" + bitrate = "" + if track.conversion_codec not in lossless: + bitrate = f"--audio-bitrate {track.outdex}?{track.conversion_bitrate.rstrip('k')} " + command_list.append(f"--audio-codec {track.outdex}?{track.conversion_codec} {bitrate}") + + return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" + def build(fastflix: FastFlix): settings: NVEncCSettings = fastflix.current_video.video_settings.video_encoder_settings @@ -34,6 +61,11 @@ def build(fastflix: FastFlix): if settings.hdr10plus_metadata: dhdr = f'--dhdr10-info "{settings.hdr10plus_metadata}"' + # TODO output-res, crop, remove hdr, time, rotate, flip, seek + res = "" + if fastflix.current_video.video_settings.scale: + res = "--output-res " + command = [ f'"{unixy(fastflix.config.nvencc)}"', "-i", @@ -60,7 +92,16 @@ def build(fastflix: FastFlix): f'{max_cll if max_cll else ""}', f'{dhdr if dhdr else ""}', "--output-depth", - str(fastflix.current_video.current_video_stream.bit_depth), + "10" if fastflix.current_video.current_video_stream.bit_depth > 8 else "8", + build_audio(fastflix.current_video.video_settings.audio_tracks), + "--multipass", + "2pass-full", + "--mv-precision", + "Q-pel", + "--chromaloc", + "auto", + "--colorrange", + "auto", "-o", f'"{unixy(fastflix.current_video.video_settings.output_path)}"', ] From f0229d951004ef0409956c6357d1692c257ec98b Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 28 Jan 2021 14:27:51 -0600 Subject: [PATCH 15/50] internal improvements --- CHANGES | 1 + README.md | 19 ++-- fastflix/data/encoders/icon_nvenc.png | Bin 1611 -> 2879 bytes fastflix/data/encoders/icon_nvencc.png | Bin 0 -> 3484 bytes fastflix/data/languages.yaml | 18 ++++ fastflix/encoders/common/audio.py | 29 +++++- fastflix/encoders/common/helpers.py | 7 +- fastflix/encoders/common/setting_panel.py | 7 +- .../encoders/nvencc_hevc/command_builder.py | 95 +++++++++++++----- fastflix/encoders/nvencc_hevc/main.py | 2 +- .../encoders/nvencc_hevc/settings_panel.py | 80 ++++++++------- fastflix/flix.py | 2 +- fastflix/models/encode.py | 8 +- fastflix/models/fastflix.py | 1 + fastflix/models/video.py | 11 +- fastflix/widgets/main.py | 53 +++++----- fastflix/widgets/panels/audio_panel.py | 47 +++++---- 17 files changed, 253 insertions(+), 127 deletions(-) create mode 100644 fastflix/data/encoders/icon_nvencc.png diff --git a/CHANGES b/CHANGES index 2ea5a483..067e4625 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,7 @@ * Adding ability to extract HDR10+ metadata if hdr10plus_parser is detected on path * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Adding Windows 10 notification for queue complete success +* Fixing #185 need to specify channel layout when downmixing (thanks to Ugurtan) * Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) * Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz) * Fixing that returning item back from queue of a different encoder type would crash Fastflix diff --git a/README.md b/README.md index ed036e15..2a7a95af 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,16 @@ Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki FastFlix supports the following encoders when their required libraries are found in FFmpeg: -| Encoder | x265 | NVENC HEVC | x264 | rav1e | AOM AV1 | SVT AV1 | VP9 | WEBP | GIF | -| --------- | ---- | ---------- | ---- | ----- | ------- | ------- | --- | ---- | --- | -| HDR10 | ✓ | | | | | | ✓* | | | -| HDR10+ | ✓ | | | | | | | | | -| Audio | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | -| Subtitles | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | -| Covers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | -| bt.2020 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | - +| Encoder | x265 | NVENC HEVC |NVEncC HEVC | x264 | rav1e | AOM AV1 | SVT AV1 | VP9 | WEBP | GIF | +| --------- | ---- | ---------- | ----------- | ---- | ----- | ------- | ------- | --- | ---- | --- | +| HDR10 | ✓ | | ✓ | | | | | ✓* | | | +| HDR10+ | ✓ | | ✓ | | | | | | | | +| Audio | ✓ | ✓ | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| Subtitles | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | +| Covers | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | | | | +| bt.2020 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | + +`✓ - Full support | ✓* - Limited support` All of these are currently supported by [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds) which is the default FFmpeg downloaded. diff --git a/fastflix/data/encoders/icon_nvenc.png b/fastflix/data/encoders/icon_nvenc.png index c217120448bfebb15a67a596503dd940759bc599..7eb94ff962d0b9bd65db157808df5b6acc379024 100644 GIT binary patch literal 2879 zcmdUx`#;l-`^VpG*c@gpr%-k~C8sop+p(F*<{TOdL(3r|R7%{~7NHn2M3nO!Moxts zbId735#_8ohsqp3zJJE|^>|#b=i~Xq^~3es6>Vi<%*QRt4FCWi4r^$A%&z0{gn*8l z{KC@U7+?>KIR*gU#PjUCavVEbptZ39z~~oe9?OW8nXS<={~x6NB#;9GEI>dg1ZY42 z%Its#2O!D@RKtM;9stb_1aJW*{J?cE5CsKZ@P6uwsyx0*+(Xa~*^ zWBXuyHJwG6DMvK(&?>p?xs{@tN!$v>%heMBg*!(_xUxR!+sai&p;M1~`S&6yxvcxL zs!7qsa>iO8uViDbl?%%x*NY`2qiEFFg}~?Yq4-q*gSu2p47n*Y7kX<65&&kQL042s zo@`i)pQfi*+<$DwT)_S@rX)x~Vsd4UE}47p{Gll&WCm-@tE;VYu3T|H_%;=&Jv2ZQ7v>5ir$ldsThsdkp^ce`g_y%NWVQ z(Ls6M9Xr3X2+*gIW>@`J-^`^awIU@pZKFaHDC$GdJM&tR` zUb#m^uI!a8mh}1zoDsN*G)flylB>_l7%s9wde9<6Uy--c#Qm=(KQ~e0APanJfob%3 zJU4muZKUutd4g@b)po?W+{2{M2$<%uR34~C;BLiJY!vuFtG-Gq!5;bt;$>B4V|3^` zJ)&`9*KcPx^UwOoE8(U=l;FROB9!mwjZxIM8j(NOwlzTB2J)&-+ZS+A&+dQX(V5K4 z9tt86@A>8>Z*n2OW|&occ0o@=M0NS-JjH8HaH&piXNB-W$Fmy2Ck*Yq7C>;5n_bHH zMvK88mW9Cdc+6*8T7vW*5^wU+E}~h-SWHSPP=Tl^a!QP|CSoNcJA_j@WT!?-oHy!q z^xTQLCcf5mA*dG`Mj7}Ao9MrR{Ed9@g{h)^rp8g$TSpiEQLN!D?|az6odzL_6%i#D z$4k!0($SJDx4h^=bKYOd@%5p)Ry+AVh^?)j_VzS;P#Sstax|gHb^fVKqqbIHA}>|ZI|T_5*(6M&lTkE`(VQU;;+O7JkJrImosKv)ME28dGWe*J#7DX zzsolLp`cF z2|euCYu-C`qiIm9t-5yeNB;%?)jr+yRM#QSZeY2!Z?+Tp;Y3#jQp0lVdqLB{VQ1g7 zmFk9FCqKLIo%?sCQ=oE+dL2I&7q@u}A7;3_9|To9_oZsl(jV3d3KF+JCy><^4?D+R z5?4mVM0a#xT}#^_Y7Z-&_~MsgkR+*G9*Wrzbv}Yz3R8khqB6UXB_@XOgL|@BKUscY zxY{44HEeSa7m~#N;{=o_YbG83({iqXk$Iq}?xf@oQd|I0`*<7183L65e#N*5+g}Ax zCyEEP8{E`fhQ?tAnjJ~4Ou~cIJd8k_0F|+2uqI}W>d&y*76O6Bh=OLH-lvG7Y3Xcp7 zV$Ynzz(_*>1S(`&Z?b%)sj%^zVouycgkf*Ir}m716e(k1EKLNyL8@$LSv54Y53*|Lu})24dD^?jVP!eC%y zj%y*{UjC~A8t-VEVmAaql6KZ(g3?dwC9$g8UgftsATtrvkV2V!xuF2_5NdOGEOKtzyuu-k zj5mWLLektdn;L2w!IU;9D5MSM7xkghV|XPRz7CD zLmJB5RRudO?RSl8yOF5x6n-J6GQJ$tr=|LKa{SX`H`D8^EPXaH%x=qF7mf&$L2h(t z{9BgjcE<<4v3+ASO>erWNPo!Ty^-QY2-~HF;WzU5{V}32yaaLIPCrDnlyT@k!s-qX zkjUMt;sX~nBYI*|vaRG#;dt))ZKY4?9qfjx6O0&Y3+<$2qGHByN-ImxeF#fhzYIW4 z@+%CxV`4hcsaRr%A377q2zD~q< zoeo#guxCuI1OY9XnN9fAt};QIL|!|JI!ffhCVj1@=B%*~waZT{2MdY$(^3iyjHCGt zY$b0c`AAI1xeCLx5e(LqEK7n&OyBOz^lXonZ6$M4efD+9*td(qW(iTj^}DpAQ*;~E zP2q?K)H~>}y@XyuLGhcj#mwcI8QJlMHH^uY<)PYYx7Xpo`FT0C!69g&%D&4zZ1wX0 OeY!X!3qyv13+aC+*Z_k7 delta 1560 zcmYk6dpOez7{`Au32RD=O2{o7QzXZ*tiq}lW7>|(LMoMJvklo}55K6aB92>f8L3n* z6N&9dB_t&(kCkFOEKIp9kxe3pQ-7TIdEWQ)eJ_8$&$m(cg{r%k|%C6@1wjhCFVPL{`I8R;AAazzRE@`yL{zcOtj%YMJvI ztaeuZ<;1%z$~~$=zPO8?PgKU0R5f+4J~>Vp!y(sNV11Apg>6Tx{j7D9TbBX;<}wz_ z)Vu=8AA)Q8#$Pm$2PQb2(v^d=cjI(7zLuV zTMYl9JRfjbI_k>k_qsuy`#OKqzF0?j)`-}DN7wB9mur9Ey$F-Q3WCoo4tu_bajUnb zLt|gHV21br4@y<#C%f&{H$TWIp8ssy)eoy#sh(C5RP#Q~+alA9%OeZ6m^DpMuik)6 z_O_o!2Go^fL#vLZ%am*`A0TxJs-go=bl5M3GZa!DOR#_V4>D|`M53E&IV z&iy!MH62B->M1$pM|+sZUh6a!sX{kFJdSUQQ|N$F*}Bw1tMZBaT(n-Y?xHJ`-Np^? z+^+Blimv}tgd4X+DRC`05bVOEgJ~$I;(H@+%?W-Q>jv#sUq>B7cS%{=b4X%qf z4c0+r%7+1L;Nh*bz)5-C%{+AjNGBkWxm8x~@mfV^R5xt*epcXND(#b@6-Sr2F?5g% z8B6XCriUGiswYE)W-4tm_O5Wo7OXzt?Mus|CvQsmz~kFMq|ZVPK19kXskUw4o=!tS zEB)B2*Gf+KY?XDLvm)3gkB?+3wDD89jTWu)O;G6Wkb#SAs;>nM6IJi*>FsEDk#b+!#PSZzGoyC z*x}EPu9ijjvI<^`F|}mKrKQsr#fp1mdESR&?H@}o&p;x6qD4vhPNK=E8JW@n9^pRG zWusHQjlS$c0MR-=xzDmvOVQh2#a+P6G~fi5P~%gvAh(5j<7G|Y~hJh2Gs7Y|4YT3$e!OM>K&}G9ZGR_oXsUSY;1xDE$V|a zoW&=+^*Sdu7~X^y#+!KJj^WB~k=f5Un#(9yVY@2p>iMGJND9hSP&`NVoy?KP)}R#Y zS^9(%h9Q!mqE?bTj|_dt@XIf?%UBy-hvHj7=<*H^$%f_+ui35kHx$!`c*ok?=c|JY zFH&)!tir!bDEau!djOU-KJe9)Ur$LSQ?;(k>NBW;Cortva=QNV6_e+tFK)zmu zG(>nND6~}=h@h~lV5g_tZa;gKv8vli#DWBehl*3!I2a-aK zhPC3}Rm-c~x=H7f4{X8`CZ~4lwr-1uq4b4^8lJ=mdP-kT#8K&6df1##~_-bGOiP<-Uc?eUIGN#!w`3VRM@qalp^>`CYy)*rPR^NHG(6;{;mc!VkjF3j5 zGR{SSXs9xgM#s#p=vxbD=it5KEJjF!a7zJrfdDf)mWx*aFW3Ot%m5h>Knn!$pa&GP z0FoKyJt`#asDRpYg1Wu{TQGnKfh!V3@T6SA?te*mgUrn=0Brf_lBK#14C#B4V+a8; zPL{)S-FDZ%1Toke!vO#+E{0fb%dpw6wqAG3)Y$Ym*#nkl1+TH$VdfU5rvClwu{WR} zO!E1&T@Y9+>(Yn{rdlrCXDSzyjhuO7|GVMtBP+B=Vx{IhrVB&(8;yA7B}?lnf5ypx zZs__hJfV+-;bMP9_rR8#GMD-%?)4;Co(3a64Lqwn-o|*hz9k!OI6!iCb(HmISEMaZ zfC?RlZ!kvZuOQb%3#;Lj6EsYuBbKOh3YTp8V*(Y7+EOk~HVKpO(eT_vzS(+p>B{uO z)T#&Xl}x%LUYm}(>b`?13brqZec0!XX{_X9%^IC%9LgoCg+DKV!zA+Q`hGOky~O7x za%s%oN&7YjbFcbhc)1(0k%z^xz$_qvH^yg#qS`t3NN=-rcuQQWuKQ@Js;emPhh%Ad z++40JnfRcq`mOK2iNMcKL&F`0=W`Q__p1EJD9I~unw)1%;5@_h;Ntrz;~=c_SW!y( z?^4_mh>(>2Ri#jz?*Y*|X==ff(}euhO$+}BW1>B;VdhVqK9ypAK|HRYgsnKNcRDr4Hztcq zGSx$O;>e4?7$CO;iZgr6MJE_8&jp&nTPngu`N8Ih=I?vpgK(9rM*I{emVvQxKCy+| zjKbL4UWkK}UzQ_H0U?j!Tbi3`97?vFFFwPJjfve&wg zH7+P3%2NrlNUgd3oNge{qw`A*ny6|Q?1bs;#qwBE$F zX z&5O76!eHir^v{vmi}h;I#=>~d8nM0V>AO@g3T zl3``Ke*HBMw3-cD-PblD<%%-xk7pLl?2%q^TC(HIj$~|%bfVa2N&1Lhf(e|QXd+O1 zP47vh`|5Q?v##i}h_Fc?dF0O6=#-KmQbJ)QBiT#wGC$8vdQivsz9Is%V85Y5&HmKw zBQ?}cv4pG#>$tuwEA(&8*&4o%z*C*P@<10$c}j;2ZVPY+NIK1KW8?;M_`8e>wnt#s z6|^KFAhLp@Fxs&9Q_5(2S$qqt0e^7f5eblb&E?cDqWp-2zh8{{vk0uP*d7Fbet^Lc zeqBNZDsT1OnOn(+f6Zc4U2>M-mml(Mi*HyIj~yo>UOe!B`M05W!B|p4#$-be#rX7K z#w{eJ=%M5n*N?Xh$5f|l?l@{ygAI|&)BEox>wJ9lcS3Olre>hQ6rbM6wn}%gKvWRK zW(@PWNhKgGHAm54V-c$@zy?I_K4Gn_;pppr#Mj}TV5=p3_4mWI$T$=WzsvNs1cUrh zPjBJf?KGQowfbeO;;R%CkfWoV+2&0=pSt>d#)?gDv&JiW=sT=pRm)4uGL&4EGZoRS zB9sW^o#Bdm`*Kh196;O26DPc09KBcONeeclDxf*AD~rZMHWH#VHtjtXKX4h1#W(A> z>2Fv2+R!*$j$0Y*bIg2A-Ed)TQ3VcN`5m8z{+Hh`?B*L|_B`Q1&;ico@y_v$kovfg z2K^d#Sw9J2X-s!JGN(f?x5CXdM=b^W;SfSo#{T5`E;uP)2Pds;)S zIDeDQJE3*It=7oQv|f5H=*FC9Q`V~gdPGwqT2skCYVH+%$|c#rBR)!b>TI=nyq&MqiKFE&zNW&4X9I=E(XsG+fx?a*pwFjsM-{vxSNwbXMc(^qw4{_! zCNq{NQ^JkB^ZY^C;%iSo+~S_S<+dwiC>Bv-)mdaEzV92x9$@bTKb;<2Gl)6ASahK} z1pK~(ULVa>s~pR*eeQ-$f&CgG$itHe=t=i9&Sw1awcKP1P5x_7DV=2>Ib`7ezD`6c z9`9bj=jBbu1yJ%p+U0_$HiN(HVNF>c+qk&4wT}LV%7Gq)Fksovl+^j0>_)TWAE542 zF>2Zk)tVE|k1(T5ER=Im0R?%W(uKUIQY0uQ2Bk02C2DOvmv_A<`Dx?VWKM%vNfSo; z1RG3LwP+oXZ5z@+V7AlVHh6+7c#Qw(9?e4moI8Ackv`{UA98eE*MRPc6#XisSyup* zb;07Y-ofc#BxoY}2bnXaY&qO+@2L76?|4(?u^44op&JPm;#32%-OI7oW2$()=44L3 zwXF>F>RsYP2QDYa+Pp^9=_V+rN3;+9{^{XnM4fru1(ZJ!FO3Uw!spjo*5*7}uq9a% zF1vbH3z5_1LbT96Po=6e);?ZpQ~l;ouJ}w%v`;ecJ6QA+%*^J$1Po5nr;-m;j&Ot>NvrvN!z_sCD@f&f^H6;~-nt%n- za-5BXFTo9KJYplsl%uAPe_YuqVr6d>5GXC_FAvzQ{jbTiG7eHgm4>J@j>33x;u9cG z;D=@bg9E6=PZ^*N_Z=RtL4bVgJQzGR!tmu|%#MWhSJkpdn#p4Mui-94uBq<7NeeM7 z3BUH;hDo{2iK6SSAgr?lT}5nfmD=G?Dv$RbvtzCI@<7s8yMToOzehQ{^oTqj0fH1; zgXN7$oQ7mb^)pXt(5efZJzBJrPCh{`EF^Vq2^`Q>T~RkMDBXa{j3i-Y3ltD?3b`rE zvh!e9V>i1&8CPFzA0c3S^kur$SSBt92J<*=SPKUtk*A5^l&0d`&d|H{QxQv$^vV>; z&!4zBI>r=ql(I8mV8isfDpEzqG5(PCyq?rXVsWZTmrwfX zNi4v6^&5RzFVTLdppf1@B!)9ZK6mhgEr~|D?IBR_At7@w?BE)rg7~s_kvW0wn}_L+ z>agYYZnqUg`El5Ds$P^XWUM9~(D24hav~8w?3WcRRmf!RsR>EQ<=%7}Uw5Z0A@wez zYSCl&x(MbEEfU1DRT+F`Tx(9mpI#XHFz%)(ni=sN;TKGB@v}(ssRYWrbpV&G!o@Kp z=syE*-E-9r>*khRH@Sk3!&ki3vlbM{yXcvB+nXU~P408`w3Ja0azz+a)2|FpYl+qB zw)0*yWOtj#O)CqTc!BzK?5t+W=F($k>p>2znN__xQ%i1ZB+)oA|DQyB?gPy=Us)M9 z=?EjKaKhc4vJ(1uBB#Utp-R8}L=9`7@RMC8+kcR7YJYSt X^vmQcRSVIO|ER}M&kXxa$1(nYjsqPr literal 0 HcmV?d00001 diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index b712b586..4acb39b4 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2785,3 +2785,21 @@ Extract HDR10+: eng: Extract HDR10+ Detect HDR10+: eng: Detect HDR10+ +Custom NVEncC options: + eng: Custom NVEncC options +Set the encoding tier: + eng: Set the encoding tier +Spatial AQ: + eng: Spatial AQ +Lookahead: + eng: Lookahead +Motion vector accuracy: + eng: Motion vector accuracy +Q-pel is highest precision: + eng: Q-pel is highest precision +Multipass: + eng: Multipass +NVEncC Options: + eng: NVEncC Options +'NVEncC Encoder support is still experimental!': + eng: 'NVEncC Encoder support is still experimental!' diff --git a/fastflix/encoders/common/audio.py b/fastflix/encoders/common/audio.py index a42fd661..62c863e2 100644 --- a/fastflix/encoders/common/audio.py +++ b/fastflix/encoders/common/audio.py @@ -1,6 +1,29 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +channel_list = { + "mono": 1, + "stereo": 2, + "2.1": 3, + "3.0": 3, + "3.0(back)": 3, + "3.1": 4, + "4.0": 4, + "quad": 4, + "quad(side)": 4, + "5.0": 5, + "5.1": 6, + "6.0": 6, + "6.0(front)": 6, + "hexagonal": 6, + "6.1": 7, + "6.1(front)": 7, + "7.0": 7, + "7.0(front)": 7, + "7.1": 8, + "7.1(wide)": 8, +} + lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] @@ -17,7 +40,11 @@ def build_audio(audio_tracks, audio_file_index=0): if not track.conversion_codec or track.conversion_codec == "none": command_list.append(f"-c:{track.outdex} copy") elif track.conversion_codec: - downmix = f"-ac:{track.outdex} {track.downmix}" if track.downmix > 0 else "" + downmix = ( + f"-ac:{track.outdex} {channel_list[track.downmix]} -filter:{track.outdex} aformat=channel_layouts={track.downmix}" + if track.downmix + else "" + ) bitrate = "" if track.conversion_codec not in lossless: bitrate = f"-b:{track.outdex} {track.conversion_bitrate} " diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index 00dca416..fd96570f 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import uuid from pathlib import Path -from typing import List, Tuple, Union +from typing import List, Tuple, Union, Optional import reusables from pydantic import BaseModel, Field @@ -10,6 +10,7 @@ from fastflix.encoders.common.audio import build_audio from fastflix.encoders.common.subtitles import build_subtitle from fastflix.models.fastflix import FastFlix +from fastflix.models.video import Crop null = "/dev/null" if reusables.win_based: @@ -115,7 +116,7 @@ def generate_ending( def generate_filters( selected_track, source=None, - crop=None, + crop: Optional[Crop] = None, scale=None, scale_filter="lanczos", scale_width=None, @@ -141,7 +142,7 @@ def generate_filters( if deinterlace: filter_list.append(f"yadif") if crop: - filter_list.append(f"crop={crop}") + filter_list.append(f"crop={crop.width}:{crop.height}:{crop.left}:{crop.right}") if scale: filter_list.append(f"scale={scale}:flags={scale_filter}") elif scale_width: diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index cdb2117a..d6b8a993 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -40,13 +40,14 @@ def determine_default(self, widget_name, opt, items: List, raise_error: bool = F if not opt: return 6 opt = str(opt) + items = [x.split("(")[0].split("-")[0].strip() for x in items] elif widget_name == "bitrate": if not opt: return 5 + items = [x.split("(")[0].split("-")[0].strip() for x in items] elif widget_name == "gpu": if opt == -1: return 0 - items = [x.split("(")[0].split("-")[0].strip() if x != "-1" else "-1" for x in items] if isinstance(opt, str): try: return items.index(opt) @@ -165,9 +166,9 @@ def _add_check_box(self, label, widget_name, opt, connect="default", enabled=Tru return layout - def _add_custom(self, connect="default", disable_both_passes=False): + def _add_custom(self, title="Custom ffmpeg options", connect="default", disable_both_passes=False): layout = QtWidgets.QHBoxLayout() - self.labels.ffmpeg_options = QtWidgets.QLabel(t("Custom ffmpeg options")) + self.labels.ffmpeg_options = QtWidgets.QLabel(t(title)) self.labels.ffmpeg_options.setToolTip(t("Extra flags or options, cannot modify existing settings")) layout.addWidget(self.labels.ffmpeg_options) self.ffmpeg_extras_widget = QtWidgets.QLineEdit() diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 38fe8198..7ecb8d1d 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -1,41 +1,57 @@ # -*- coding: utf-8 -*- import re import secrets +from typing import List, Tuple, Union from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null from fastflix.models.encode import NVEncCSettings +from fastflix.models.video import SubtitleTrack, Video from fastflix.models.fastflix import FastFlix from fastflix.flix import unixy lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] -def build_audio(audio_tracks, audio_file_index=0): - # TODO figure out copy and downmix - # https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md#--audio-stream-intorstringstring1string2 +def build_audio(audio_tracks): command_list = [] copies = [] for track in audio_tracks: - command_list.append( - f'--audio-metadata {track.outdex}?title="{track.title}" ' - f'--audio-metadata {track.outdex}?handler="{track.title}" ' - ) if track.language: command_list.append(f"--audio-metadata {track.outdex}?language={track.language}") if not track.conversion_codec or track.conversion_codec == "none": copies.append(str(track.outdex)) elif track.conversion_codec: - # downmix = f"-ac:{track.outdex} {track.downmix}" if track.downmix > 0 else "" + downmix = f"--audio-stream {track.outdex}?:{track.downmix}" if track.downmix else "" bitrate = "" if track.conversion_codec not in lossless: bitrate = f"--audio-bitrate {track.outdex}?{track.conversion_bitrate.rstrip('k')} " - command_list.append(f"--audio-codec {track.outdex}?{track.conversion_codec} {bitrate}") + command_list.append(f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate}") + command_list.append( + f'--audio-metadata {track.outdex}?title="{track.title}" ' + f'--audio-metadata {track.outdex}?handler="{track.title}" ' + ) return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" +def build_subtitle(subtitle_tracks: List[SubtitleTrack]) -> str: + command_list = [] + copies = [] + for i, track in enumerate(subtitle_tracks, start=1): + if track.burn_in: + command_list.append(f"--vpp-subburn track={i}") + else: + copies.append(str(i)) + if track.disposition: + command_list.append(f"--sub-disposition {i}?{track.disposition}") + command_list.append(f"--sub-metadata {i}?language='{track.language}'") + + return f" --sub-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" + + def build(fastflix: FastFlix): + video: Video = fastflix.current_video settings: NVEncCSettings = fastflix.current_video.video_settings.video_encoder_settings # beginning, ending = generate_all(fastflix, "hevc_nvenc") @@ -61,15 +77,34 @@ def build(fastflix: FastFlix): if settings.hdr10plus_metadata: dhdr = f'--dhdr10-info "{settings.hdr10plus_metadata}"' - # TODO output-res, crop, remove hdr, time, rotate, flip, seek - res = "" - if fastflix.current_video.video_settings.scale: - res = "--output-res " + # TODO trim + + transform = "" + if video.video_settings.vertical_flip or video.video_settings.horizontal_flip: + transform = f"--vpp-transform flip_x={'true' if video.video_settings.horizontal_flip else 'false'},flip_y={'true' if video.video_settings.vertical_flip else 'false'}" + + remove_hdr = "" + if video.video_settings.remove_hdr: + remove_type = ( + video.video_settings.tone_map + if video.video_settings.tone_map in ("mobius", "hable", "reinhard") + else "mobius" + ) + remove_hdr = f"--vpp-colorspace hdr2sdr={remove_type}" if video.video_settings.remove_hdr else "" + + crop = "" + if video.video_settings.crop: + crop = f"--crop {video.video_settings.crop.left},{video.video_settings.crop.top},{video.video_settings.crop.right},{video.video_settings.crop.bottom}" command = [ f'"{unixy(fastflix.config.nvencc)}"', "-i", - f'"{fastflix.current_video.source}"', + f'"{unixy(video.source)}"', + (f"--seek {video.video_settings.start_time}" if video.video_settings.start_time else ""), + (f"--vpp-rotate {video.video_settings.rotate}" if video.video_settings.rotate else ""), + transform, + (f'--scale {video.video_settings.scale.replace(":", "x")}' if video.video_settings.scale else ""), + crop, "-c", "hevc", "--vbr", @@ -83,30 +118,38 @@ def build(fastflix: FastFlix): f'{f"--lookahead {settings.lookahead}" if settings.lookahead else ""}', f'{"--aq" if settings.spatial_aq else "--no-aq"}', "--colormatrix", - (fastflix.current_video.video_settings.color_space or "auto"), + (video.video_settings.color_space or "auto"), "--transfer", - (fastflix.current_video.video_settings.color_transfer or "auto"), + (video.video_settings.color_transfer or "auto"), "--colorprim", - (fastflix.current_video.video_settings.color_primaries or "auto"), - f'{master_display if master_display else ""}', - f'{max_cll if max_cll else ""}', - f'{dhdr if dhdr else ""}', + (video.video_settings.color_primaries or "auto"), + (master_display if master_display else ""), + (max_cll if max_cll else ""), + (dhdr if dhdr else ""), "--output-depth", - "10" if fastflix.current_video.current_video_stream.bit_depth > 8 else "8", - build_audio(fastflix.current_video.video_settings.audio_tracks), + ("10" if video.current_video_stream.bit_depth > 8 else "8"), "--multipass", - "2pass-full", + settings.multipass, "--mv-precision", - "Q-pel", + settings.mv_precision, "--chromaloc", "auto", "--colorrange", "auto", + f"--avsync {'cfr' if video.frame_rate == video.average_frame_rate else 'vfr'}", + f'{f"--interlace {video.interlaced}" if video.interlaced else ""}', + f'{"--vpp-yadif" if video.video_settings.deinterlace else ""}', + (f"--output-res {video.video_settings.scale}" if video.video_settings.scale else ""), + (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), + remove_hdr, + build_audio(video.video_settings.audio_tracks), + build_subtitle(video.video_settings.subtitle_tracks), + settings.extra, "-o", - f'"{unixy(fastflix.current_video.video_settings.output_path)}"', + f'"{unixy(video.video_settings.output_path)}"', ] - return [Command(command=" ".join(command), name="NVEncC Encode")] + return [Command(command=" ".join(command), name="NVEncC Encode", exe="NVEncE")] # -i "Beverly Hills Duck Pond - HDR10plus - Jessica Payne.mp4" -c hevc --profile main10 --tier main --output-depth 10 --vbr 6000k --preset quality --multipass 2pass-full --aq --repeat-headers --colormatrix bt2020nc --transfer smpte2084 --colorprim bt2020 --lookahead 16 -o "nvenc-6000k.mkv" diff --git a/fastflix/encoders/nvencc_hevc/main.py b/fastflix/encoders/nvencc_hevc/main.py index 0c368765..3bfabe9d 100644 --- a/fastflix/encoders/nvencc_hevc/main.py +++ b/fastflix/encoders/nvencc_hevc/main.py @@ -13,7 +13,7 @@ enable_subtitles = True enable_audio = True -enable_attachments = True +enable_attachments = False audio_formats = [ "aac", diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 3d382319..b8a73f89 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -10,7 +10,7 @@ from fastflix.models.fastflix_app import FastFlixApp from fastflix.shared import link from fastflix.exceptions import FastFlixInternalException -from fastflix.resources import loading_movie +from fastflix.resources import loading_movie, warning_icon logger = logging.getLogger("fastflix") @@ -71,7 +71,7 @@ def __init__(self, parent, main, app: FastFlixApp): self.updating_settings = False grid.addLayout(self.init_modes(), 0, 2, 3, 4) - grid.addLayout(self._add_custom(), 10, 0, 1, 6) + grid.addLayout(self._add_custom(title="Custom NVEncC options", disable_both_passes=True), 10, 0, 1, 6) grid.addLayout(self.init_preset(), 0, 0, 1, 2) # grid.addLayout(self.init_max_mux(), 1, 0, 1, 2) @@ -82,6 +82,8 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_spatial_aq(), 3, 0, 1, 2) grid.addLayout(self.init_lookahead(), 4, 0, 1, 2) + grid.addLayout(self.init_mv_precision(), 5, 0, 1, 2) + grid.addLayout(self.init_multipass(), 6, 0, 1, 2) grid.addLayout(self.init_dhdr10_info(), 4, 2, 1, 3) grid.addLayout(self.init_dhdr10_warning_and_opt(), 4, 5, 1, 1) @@ -98,12 +100,18 @@ def __init__(self, parent, main, app: FastFlixApp): grid.setRowStretch(9, 1) - # guide_label = QtWidgets.QLabel( - # link("https://trac.ffmpeg.org/wiki/Encode/H.264", t("FFMPEG AVC / H.264 Encoding Guide")) - # ) - # guide_label.setAlignment(QtCore.Qt.AlignBottom) - # guide_label.setOpenExternalLinks(True) - # grid.addWidget(guide_label, 11, 0, 1, 6) + guide_label = QtWidgets.QLabel( + link("https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md", t("NVEncC Options")) + ) + + warning_label = QtWidgets.QLabel() + warning_label.setPixmap(QtGui.QIcon(warning_icon).pixmap(22)) + + guide_label.setAlignment(QtCore.Qt.AlignBottom) + guide_label.setOpenExternalLinks(True) + grid.addWidget(guide_label, 11, 0, 1, 4) + grid.addWidget(warning_label, 11, 4, 1, 1, alignment=QtCore.Qt.AlignRight) + grid.addWidget(QtWidgets.QLabel(t("NVEncC Encoder support is still experimental!")), 11, 5, 1, 1) self.setLayout(grid) self.hide() @@ -137,14 +145,14 @@ def init_profile(self): opt="profile", ) - def init_pix_fmt(self): - return self._add_combo_box( - label="Bit Depth", - tooltip="Pixel Format (requires at least 10-bit for HDR)", - widget_name="pix_fmt", - options=pix_fmts, - opt="pix_fmt", - ) + # def init_pix_fmt(self): + # return self._add_combo_box( + # label="Bit Depth", + # tooltip="Pixel Format (requires at least 10-bit for HDR)", + # widget_name="pix_fmt", + # options=pix_fmts, + # opt="pix_fmt", + # ) def init_tier(self): return self._add_combo_box( @@ -155,26 +163,6 @@ def init_tier(self): opt="tier", ) - def init_rc(self): - return self._add_combo_box( - label="Rate Control", - tooltip="Override the preset rate-control", - widget_name="rc", - options=[ - "default", - "vbr", - "cbr", - "vbr_minqp", - "ll_2pass_quality", - "ll_2pass_size", - "vbr_2pass", - "cbr_ld_hq", - "cbr_hq", - "vbr_hq", - ], - opt="rc", - ) - def init_spatial_aq(self): return self._add_combo_box( label="Spatial AQ", @@ -184,6 +172,24 @@ def init_spatial_aq(self): opt="spatial_aq", ) + def init_multipass(self): + return self._add_combo_box( + label="Multipass", + tooltip="", + widget_name="multipass", + options=["none", "2pass-quarter", "2pass-full"], + opt="multipass", + ) + + def init_mv_precision(self): + return self._add_combo_box( + label="Motion vector accuracy", + tooltip="Q-pel is highest precision", + widget_name="mv_precision", + options=["auto", "Q-pel", "half-pel", "full-pel"], + opt="mv_precision", + ) + def init_lookahead(self): return self._add_combo_box( label="Lookahead", @@ -283,6 +289,8 @@ def update_video_encoder_settings(self): lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None, spatial_aq=bool(self.widgets.spatial_aq.currentIndex()), hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"), + multipass=self.widgets.multipass.currentText(), + mv_precision=self.widgets.mv_precision.currentText(), # pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(), # extra=self.ffmpeg_extras, # tune=tune.split("-")[0].strip(), diff --git a/fastflix/flix.py b/fastflix/flix.py index 99e75e82..af25d392 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -334,7 +334,7 @@ def detect_interlaced(app: FastFlixApp, config: Config, source: Path, **_): else: if int(tffs) + int(bffs) > int(progressive): app.fastflix.current_video.video_settings.deinterlace = True - app.fastflix.current_video.interlaced = True + app.fastflix.current_video.interlaced = "tff" if int(tffs) > int(bffs) else "bff" return app.fastflix.current_video.video_settings.deinterlace = False app.fastflix.current_video.interlaced = False diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index cb2222d9..62756bcf 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -9,7 +9,7 @@ class AudioTrack(BaseModel): index: int outdex: int codec: str = "" - downmix: int = 0 + downmix: Optional[str] = None title: str = "" language: str = "" conversion_bitrate: str = "" @@ -98,11 +98,13 @@ class NVEncCSettings(EncoderSettings): bitrate: Optional[str] = "6000k" qp: Optional[str] = None cq: int = 0 - spatial_aq: bool = False + spatial_aq: bool = True lookahead: Optional[int] = None - tier: str = "main" + tier: str = "high" level: Optional[str] = None hdr10plus_metadata: str = "" + multipass: str = "2pass-full" + mv_precision: str = "auto" class rav1eSettings(EncoderSettings): diff --git a/fastflix/models/fastflix.py b/fastflix/models/fastflix.py index 6bb47c65..c7b8454c 100644 --- a/fastflix/models/fastflix.py +++ b/fastflix/models/fastflix.py @@ -18,6 +18,7 @@ class FastFlix(BaseModel): config: Config = None data_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) log_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs" + queue_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml" ffmpeg_version: str = "" ffmpeg_config: List[str] = "" ffprobe_version: str = "" diff --git a/fastflix/models/video.py b/fastflix/models/video.py index c2bf6396..8d05a385 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -26,8 +26,17 @@ __all__ = ["VideoSettings", "Status", "Video"] +class Crop(BaseModel): + top: int = 0 + right: int = 0 + bottom: int = 0 + left: int = 0 + width: int = 0 + height: int = 0 + + class VideoSettings(BaseModel): - crop: Optional[str] = None + crop: Optional[Crop] = None start_time: Union[float, int] = 0 end_time: Union[float, int] = 0 fast_seek: bool = True diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 159891e3..76e36398 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -31,7 +31,7 @@ ) from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp -from fastflix.models.video import Status, Video, VideoSettings +from fastflix.models.video import Status, Video, VideoSettings, Crop from fastflix.resources import ( black_x_icon, folder_icon, @@ -479,6 +479,7 @@ def change_output_types(self): def init_encoder_drop_down(self): layout = QtWidgets.QHBoxLayout() self.widgets.convert_to = QtWidgets.QComboBox() + self.widgets.convert_to.setMinimumWidth(180) self.change_output_types() self.widgets.convert_to.currentTextChanged.connect(self.change_encoder) @@ -842,32 +843,37 @@ def get_auto_crop(self): self.loading_video = False self.widgets.crop.bottom.setText(str(b)) - def build_crop(self) -> Union[str, None]: + def build_crop(self) -> Union[Crop, None]: if not self.initialized or not self.app.fastflix.current_video: return None try: - top = int(self.widgets.crop.top.text()) - left = int(self.widgets.crop.left.text()) - right = int(self.widgets.crop.right.text()) - bottom = int(self.widgets.crop.bottom.text()) + crop = Crop( + top=int(self.widgets.crop.top.text()), + left=int(self.widgets.crop.left.text()), + right=int(self.widgets.crop.right.text()), + bottom=int(self.widgets.crop.bottom.text()), + ) except (ValueError, AttributeError): logger.error("Invalid crop") return None - width = self.app.fastflix.current_video.width - right - left - height = self.app.fastflix.current_video.height - bottom - top - if (top + left + right + bottom) == 0: - return None - try: - assert top >= 0, t("Top must be positive number") - assert left >= 0, t("Left must be positive number") - assert width > 0, t("Total video width must be greater than 0") - assert height > 0, t("Total video height must be greater than 0") - assert width <= self.app.fastflix.current_video.width, t("Width must be smaller than video width") - assert height <= self.app.fastflix.current_video.height, t("Height must be smaller than video height") - except AssertionError as err: - error_message(f"{t('Invalid Crop')}: {err}") - return - return f"{width}:{height}:{left}:{top}" + else: + crop.width = self.app.fastflix.current_video.width - crop.right - crop.left + crop.height = self.app.fastflix.current_video.height - crop.bottom - crop.top + if (crop.top + crop.left + crop.right + crop.bottom) == 0: + return None + try: + assert crop.top >= 0, t("Top must be positive number") + assert crop.left >= 0, t("Left must be positive number") + assert crop.width > 0, t("Total video width must be greater than 0") + assert crop.height > 0, t("Total video height must be greater than 0") + assert crop.width <= self.app.fastflix.current_video.width, t("Width must be smaller than video width") + assert crop.height <= self.app.fastflix.current_video.height, t( + "Height must be smaller than video height" + ) + except AssertionError as err: + error_message(f"{t('Invalid Crop')}: {err}") + return + return crop def keep_aspect_update(self) -> None: keep_aspect = self.widgets.scale.keep_aspect.isChecked() @@ -941,8 +947,9 @@ def scale_update(self): self.widgets.scale.height.setDisabled(keep_aspect) height = self.app.fastflix.current_video.height width = self.app.fastflix.current_video.width - if self.build_crop(): - width, height, *_ = (int(x) for x in self.build_crop().split(":")) + if crop := self.build_crop(): + width = crop.width + height = crop.height if keep_aspect and (not height or not width): self.scale_updating = False diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index c62a9f1f..23a794b6 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -1,18 +1,18 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from typing import List +from typing import List, Optional from box import Box from iso639 import Lang from iso639.exceptions import InvalidLanguageValue from qtpy import QtCore, QtGui, QtWidgets -from fastflix.encoders.common.audio import lossless +from fastflix.encoders.common.audio import lossless, channel_list from fastflix.language import t from fastflix.models.encode import AudioTrack from fastflix.models.fastflix_app import FastFlixApp from fastflix.resources import black_x_icon, copy_icon, down_arrow_icon, up_arrow_icon -from fastflix.shared import no_border +from fastflix.shared import no_border, error_message from fastflix.widgets.panels.abstract_list import FlixList language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower()) @@ -78,17 +78,6 @@ def __init__( if all_info: self.widgets.audio_info.setToolTip(all_info.to_yaml()) - downmix_options = [ - "mono", - "stereo", - "2.1 / 3.0", - "3.1 / 4.0", - "4.1 / 5.0", - "5.1 / 6.0", - "6.1 / 7.0", - "7.1 / 8.0", - ] - self.widgets.language.addItems(["No Language Set"] + language_list) self.widgets.language.setMaximumWidth(110) if language: @@ -105,7 +94,7 @@ def __init__( self.widgets.title.textChanged.connect(self.page_update) self.widgets.audio_info.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - self.widgets.downmix.addItems([t("No Downmix")] + downmix_options[: channels - 2]) + self.widgets.downmix.addItems([t("No Downmix")] + [k for k, v in channel_list.items() if v <= channels]) self.widgets.downmix.currentIndexChanged.connect(self.update_downmix) self.widgets.downmix.setCurrentIndex(0) self.widgets.downmix.setDisabled(True) @@ -219,15 +208,19 @@ def update_enable(self): self.parent.reorder(update=True) def update_downmix(self): - channels = self.widgets.downmix.currentIndex() + channels = ( + channel_list[self.widgets.downmix.currentText()] + if self.widgets.downmix.currentIndex() > 0 + else self.channels + ) self.widgets.convert_bitrate.clear() if channels > 0: self.widgets.convert_bitrate.addItems( - [f"{x}k" for x in range(32 * channels, (256 * channels) + 1, 32 * channels)] + [f"{x}k" for x in range(16 * channels, (256 * channels) + 1, 16 * channels)] ) else: self.widgets.convert_bitrate.addItems( - [f"{x}k" for x in range(32 * self.channels, (256 * self.channels) + 1, 32 * self.channels)] + [f"{x}k" for x in range(16 * self.channels, (256 * self.channels) + 1, 16 * self.channels)] ) self.widgets.convert_bitrate.setCurrentIndex(3) self.page_update() @@ -278,8 +271,8 @@ def conversion(self): return {"codec": self.widgets.convert_to.currentText(), "bitrate": self.widgets.convert_bitrate.currentText()} @property - def downmix(self) -> int: - return self.widgets.downmix.currentIndex() + def downmix(self) -> Optional[str]: + return self.widgets.downmix.currentText() if self.widgets.downmix.currentIndex() > 0 else None @property def language(self) -> str: @@ -397,6 +390,20 @@ def new_source(self, codecs): self.update_audio_settings() def allowed_formats(self, allowed_formats=None): + disable_dups = "nvencc" in self.main.convert_to.lower() + tracks_need_removed = False + for track in self.tracks: + track.widgets.dup_button.setDisabled(disable_dups) + if not track.original: + if disable_dups: + tracks_need_removed = True + else: + if disable_dups: + track.widgets.dup_button.hide() + else: + track.widgets.dup_button.show() + if tracks_need_removed: + error_message(t("This encoder does not support duplicating audio tracks, please remove copied tracks!")) if not allowed_formats: return for track in self.tracks: From 8c36fe7ce144d526e5a0611f05eb670c37b09e27 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 28 Jan 2021 18:12:19 -0600 Subject: [PATCH 16/50] Fixing imports that are windows only --- README.md | 14 +------ fastflix/shared.py | 71 +---------------------------------- fastflix/widgets/container.py | 3 +- fastflix/widgets/main.py | 3 +- fastflix/windows_tools.py | 70 ++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 84 deletions(-) create mode 100644 fastflix/windows_tools.py diff --git a/README.md b/README.md index 2a7a95af..b55715f0 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,15 @@ FastFlix keeps HDR10 metadata for x265, which will be expanded to AV1 libraries It needs `FFmpeg` (version 4.3 or greater) under the hood for the heavy lifting, and can work with a variety of encoders. -Please also grab [hdr10plus_parser](https://github.com/quietvoid/hdr10plus_parser/releases) and [mkvpropedit](https://mkvtoolnix.download/downloads.html) and make sure they are on the system path for full feature set. - **NEW**: Join us on [discord](https://discord.gg/GUBFP6f) or [reddit](https://www.reddit.com/r/FastFlix/)! Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki) for help or more details, and please report bugs or ideas in the [github issue tracker](https://github.com/cdgriffith/FastFlix/issues)! # Encoders - FastFlix supports the following encoders when their required libraries are found in FFmpeg: + FastFlix supports the following encoders if available: -| Encoder | x265 | NVENC HEVC |NVEncC HEVC | x264 | rav1e | AOM AV1 | SVT AV1 | VP9 | WEBP | GIF | +| Encoder | x265 | NVENC HEVC | [NVEncC HEVC](https://github.com/rigaya/NVEnc/releases) | x264 | rav1e | AOM AV1 | SVT AV1 | VP9 | WEBP | GIF | | --------- | ---- | ---------- | ----------- | ---- | ----- | ------- | ------- | --- | ---- | --- | | HDR10 | ✓ | | ✓ | | | | | ✓* | | | | HDR10+ | ✓ | | ✓ | | | | | | | | @@ -31,14 +29,6 @@ Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki `✓ - Full support | ✓* - Limited support` -All of these are currently supported by [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds) which is the default FFmpeg downloaded. - -Most other builds do not have all these encoders available by default and may require custom compiling FFmpeg for a specific encoder. - -* [Windows FFmpeg (and more) auto builder](https://github.com/m-ab-s/media-autobuild_suite) -* [Windows cross compile FFmpeg (build on linux)](https://github.com/rdp/ffmpeg-windows-build-helpers) -* [FFmpeg compilation guide](https://trac.ffmpeg.org/wiki/CompilationGuide) - # Releases View the [releases](https://github.com/cdgriffith/FastFlix/releases) for binaries for Windows, MacOS or Linux diff --git a/fastflix/shared.py b/fastflix/shared.py index 3e0a41d2..dbbec3cf 100644 --- a/fastflix/shared.py +++ b/fastflix/shared.py @@ -11,32 +11,7 @@ import pkg_resources import requests import reusables -from win32api import GetModuleHandle -from win32con import ( - CW_USEDEFAULT, - IMAGE_ICON, - LR_DEFAULTSIZE, - LR_LOADFROMFILE, - WM_USER, - WS_OVERLAPPED, - WS_SYSMENU, -) -from win32gui import ( - NIF_ICON, - NIF_INFO, - NIF_MESSAGE, - NIF_TIP, - NIM_ADD, - NIM_MODIFY, - WNDCLASS, - CreateWindow, - DestroyWindow, - LoadImage, - RegisterClass, - Shell_NotifyIcon, - UnregisterClass, - UpdateWindow, -) + try: # PyInstaller creates a temp folder and stores path in _MEIPASS @@ -282,47 +257,3 @@ def timedelta_to_str(delta): output_string = output_string.split(".")[0] # Remove .XXX microseconds return output_string - - -tool_window = None -tool_icon = None - - -def show_windows_notification(title, msg, icon_path): - global tool_window, tool_icon - - wc = WNDCLASS() - hinst = wc.hInstance = GetModuleHandle(None) - wc.lpszClassName = "FastFlix" - if not tool_window: - tool_window = CreateWindow( - RegisterClass(wc), - "Taskbar", - WS_OVERLAPPED | WS_SYSMENU, - 0, - 0, - CW_USEDEFAULT, - CW_USEDEFAULT, - 0, - 0, - hinst, - None, - ) - UpdateWindow(tool_window) - - icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE - tool_icon = LoadImage(hinst, icon_path, IMAGE_ICON, 0, 0, icon_flags) - - flags = NIF_ICON | NIF_MESSAGE | NIF_TIP - nid = (tool_window, 0, flags, WM_USER + 20, tool_icon, "FastFlix Notifications") - Shell_NotifyIcon(NIM_ADD, nid) - - Shell_NotifyIcon( - NIM_MODIFY, (tool_window, 0, NIF_INFO, WM_USER + 20, tool_icon, "Balloon Tooltip", msg, 200, title, 4) - ) - - -def cleanup_windows_notification(): - if tool_window: - DestroyWindow(tool_window) - UnregisterClass("FastFlix", None) diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 935cbe11..891e69e6 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -17,7 +17,8 @@ from fastflix.models.fastflix_app import FastFlixApp from fastflix.program_downloads import latest_ffmpeg from fastflix.resources import main_icon -from fastflix.shared import clean_logs, error_message, latest_fastflix, message, cleanup_windows_notification +from fastflix.shared import clean_logs, error_message, latest_fastflix, message +from fastflix.windows_tools import cleanup_windows_notification from fastflix.widgets.about import About from fastflix.widgets.changes import Changes from fastflix.widgets.logs import Logs diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 76e36398..7e5923d3 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -42,7 +42,8 @@ video_add_icon, video_playlist_icon, ) -from fastflix.shared import error_message, message, time_to_number, yes_no_message, show_windows_notification +from fastflix.shared import error_message, message, time_to_number, yes_no_message +from fastflix.windows_tools import show_windows_notification from fastflix.widgets.background_tasks import SubtitleFix, ThumbnailCreator from fastflix.widgets.progress_bar import ProgressBar, Task from fastflix.widgets.video_options import VideoOptions diff --git a/fastflix/windows_tools.py b/fastflix/windows_tools.py new file mode 100644 index 00000000..290cdf3a --- /dev/null +++ b/fastflix/windows_tools.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from win32api import GetModuleHandle +from win32con import ( + CW_USEDEFAULT, + IMAGE_ICON, + LR_DEFAULTSIZE, + LR_LOADFROMFILE, + WM_USER, + WS_OVERLAPPED, + WS_SYSMENU, +) +from win32gui import ( + NIF_ICON, + NIF_INFO, + NIF_MESSAGE, + NIF_TIP, + NIM_ADD, + NIM_MODIFY, + WNDCLASS, + CreateWindow, + DestroyWindow, + LoadImage, + RegisterClass, + Shell_NotifyIcon, + UnregisterClass, + UpdateWindow, +) + +tool_window = None +tool_icon = None + + +def show_windows_notification(title, msg, icon_path): + global tool_window, tool_icon + + wc = WNDCLASS() + hinst = wc.hInstance = GetModuleHandle(None) + wc.lpszClassName = "FastFlix" + if not tool_window: + tool_window = CreateWindow( + RegisterClass(wc), + "Taskbar", + WS_OVERLAPPED | WS_SYSMENU, + 0, + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + 0, + 0, + hinst, + None, + ) + UpdateWindow(tool_window) + + icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE + tool_icon = LoadImage(hinst, icon_path, IMAGE_ICON, 0, 0, icon_flags) + + flags = NIF_ICON | NIF_MESSAGE | NIF_TIP + nid = (tool_window, 0, flags, WM_USER + 20, tool_icon, "FastFlix Notifications") + Shell_NotifyIcon(NIM_ADD, nid) + + Shell_NotifyIcon( + NIM_MODIFY, (tool_window, 0, NIF_INFO, WM_USER + 20, tool_icon, "Balloon Tooltip", msg, 200, title, 4) + ) + + +def cleanup_windows_notification(): + if tool_window: + DestroyWindow(tool_window) + UnregisterClass("FastFlix", None) From 3b14a7879ba2667971e92bd238ab855d68dd645c Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 28 Jan 2021 18:20:09 -0600 Subject: [PATCH 17/50] Fixing imports that are windows only --- fastflix/windows_tools.py | 53 ++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/fastflix/windows_tools.py b/fastflix/windows_tools.py index 290cdf3a..a3d40af5 100644 --- a/fastflix/windows_tools.py +++ b/fastflix/windows_tools.py @@ -1,30 +1,5 @@ # -*- coding: utf-8 -*- -from win32api import GetModuleHandle -from win32con import ( - CW_USEDEFAULT, - IMAGE_ICON, - LR_DEFAULTSIZE, - LR_LOADFROMFILE, - WM_USER, - WS_OVERLAPPED, - WS_SYSMENU, -) -from win32gui import ( - NIF_ICON, - NIF_INFO, - NIF_MESSAGE, - NIF_TIP, - NIM_ADD, - NIM_MODIFY, - WNDCLASS, - CreateWindow, - DestroyWindow, - LoadImage, - RegisterClass, - Shell_NotifyIcon, - UnregisterClass, - UpdateWindow, -) + tool_window = None tool_icon = None @@ -32,6 +7,30 @@ def show_windows_notification(title, msg, icon_path): global tool_window, tool_icon + from win32api import GetModuleHandle + from win32con import ( + CW_USEDEFAULT, + IMAGE_ICON, + LR_DEFAULTSIZE, + LR_LOADFROMFILE, + WM_USER, + WS_OVERLAPPED, + WS_SYSMENU, + ) + from win32gui import ( + NIF_ICON, + NIF_INFO, + NIF_MESSAGE, + NIF_TIP, + NIM_ADD, + NIM_MODIFY, + WNDCLASS, + CreateWindow, + LoadImage, + RegisterClass, + Shell_NotifyIcon, + UpdateWindow, + ) wc = WNDCLASS() hinst = wc.hInstance = GetModuleHandle(None) @@ -65,6 +64,8 @@ def show_windows_notification(title, msg, icon_path): def cleanup_windows_notification(): + from win32gui import DestroyWindow, UnregisterClass + if tool_window: DestroyWindow(tool_window) UnregisterClass("FastFlix", None) From c410f66b4d2f726069133b4dafd1f1e08b8a8739 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Fri, 29 Jan 2021 18:19:08 -0600 Subject: [PATCH 18/50] * Adding #194 fast two pass encoding (thanks to Ugurtan) Lots of nvencc fixes --- CHANGES | 1 + fastflix/conversion_worker.py | 12 +- fastflix/data/languages.yaml | 6 + fastflix/encoders/common/helpers.py | 8 +- fastflix/encoders/common/setting_panel.py | 23 +++- .../encoders/nvencc_hevc/command_builder.py | 53 +++++++-- .../encoders/nvencc_hevc/settings_panel.py | 111 +++++++++++++----- fastflix/encoders/vp9/command_builder.py | 12 +- fastflix/encoders/vp9/settings_panel.py | 45 ++++++- fastflix/models/encode.py | 8 +- fastflix/models/fastflix.py | 1 + fastflix/models/queue.py | 51 ++++++++ fastflix/widgets/main.py | 23 ++-- fastflix/widgets/panels/audio_panel.py | 2 +- requirements-build.txt | 3 +- requirements.txt | 3 +- 16 files changed, 285 insertions(+), 77 deletions(-) create mode 100644 fastflix/models/queue.py diff --git a/CHANGES b/CHANGES index 067e4625..0cee93a4 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,7 @@ * Adding ability to extract HDR10+ metadata if hdr10plus_parser is detected on path * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Adding Windows 10 notification for queue complete success +* Adding #194 fast two pass encoding (thanks to Ugurtan) * Fixing #185 need to specify channel layout when downmixing (thanks to Ugurtan) * Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) * Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index ae6dc624..a818eece 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -5,13 +5,23 @@ import reusables from appdirs import user_data_dir +from filelock import FileLock +from box import Box from fastflix.command_runner import BackgroundRunner from fastflix.language import t from fastflix.shared import file_date +from fastflix.models.queue import STATUS, REQUEST, QueueItem, Queue logger = logging.getLogger("fastflix-core") +log_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs" +after_done_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "after_done_logs" + +queue_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml" +queue_lock_file = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.lock" + + CONTINUOUS = 0x80000000 SYSTEM_REQUIRED = 0x00000001 @@ -51,8 +61,6 @@ def queue_worker(gui_proc, worker_queue, status_queue, log_queue): gui_died = False currently_encoding = False paused = False - log_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs" - after_done_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "after_done_logs" def start_command(): nonlocal currently_encoding diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 4acb39b4..61a6895e 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2803,3 +2803,9 @@ NVEncC Options: eng: NVEncC Options 'NVEncC Encoder support is still experimental!': eng: 'NVEncC Encoder support is still experimental!' +Init Q: + eng: Init Q +Min Q: + eng: Min Q +Max Q: + eng: Max Q diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index fd96570f..5078f978 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -119,8 +119,6 @@ def generate_filters( crop: Optional[Crop] = None, scale=None, scale_filter="lanczos", - scale_width=None, - scale_height=None, remove_hdr=False, rotate=0, vertical_flip=None, @@ -142,13 +140,9 @@ def generate_filters( if deinterlace: filter_list.append(f"yadif") if crop: - filter_list.append(f"crop={crop.width}:{crop.height}:{crop.left}:{crop.right}") + filter_list.append(f"crop={crop['width']}:{crop['height']}:{crop['left']}:{crop['top']}") if scale: filter_list.append(f"scale={scale}:flags={scale_filter}") - elif scale_width: - filter_list.append(f"scale={scale_width}:-8:flags={scale_filter}") - elif scale_height: - filter_list.append(f"scale=-8:{scale_height}:flags={scale_filter}") if rotate: if rotate < 3: filter_list.append(f"transpose={rotate}") diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index d6b8a993..18dfc7ca 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -62,15 +62,27 @@ def determine_default(self, widget_name, opt, items: List, raise_error: bool = F return opt def _add_combo_box( - self, label, options, widget_name, opt=None, connect="default", enabled=True, default=0, tooltip="" + self, + options, + widget_name, + label=None, + opt=None, + connect="default", + enabled=True, + default=0, + tooltip="", + min_width=None, ): layout = QtWidgets.QHBoxLayout() - self.labels[widget_name] = QtWidgets.QLabel(t(label)) - if tooltip: - self.labels[widget_name].setToolTip(self.translate_tip(tooltip)) + if label: + self.labels[widget_name] = QtWidgets.QLabel(t(label)) + if tooltip: + self.labels[widget_name].setToolTip(self.translate_tip(tooltip)) self.widgets[widget_name] = QtWidgets.QComboBox() self.widgets[widget_name].addItems(options) + if min_width: + self.widgets[widget_name].setMinimumWidth(min_width) if opt: default = self.determine_default( @@ -91,6 +103,9 @@ def _add_combo_box( else: self.widgets[widget_name].currentIndexChanged.connect(connect) + if not label: + return self.widgets[widget_name] + layout.addWidget(self.labels[widget_name]) layout.addWidget(self.widgets[widget_name]) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 7ecb8d1d..302f985d 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -2,6 +2,7 @@ import re import secrets from typing import List, Tuple, Union +import logging from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null from fastflix.models.encode import NVEncCSettings @@ -11,6 +12,8 @@ lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] +logger = logging.getLogger("fastflix") + def build_audio(audio_tracks): command_list = [] @@ -59,6 +62,12 @@ def build(fastflix: FastFlix): # beginning += f'{f"-tune:v {settings.tune}" if settings.tune else ""} {generate_color_details(fastflix)} -spatial_aq:v {settings.spatial_aq} -tier:v {settings.tier} -rc-lookahead:v {settings.rc_lookahead} -gpu {settings.gpu} -b_ref_mode {settings.b_ref_mode} ' # --profile main10 --tier main + + video_track = 0 + for i, track in enumerate(video.streams.video): + if int(track.index) == video.video_settings.selected_track: + video_track = i + master_display = None if fastflix.current_video.master_display: master_display = ( @@ -77,7 +86,25 @@ def build(fastflix: FastFlix): if settings.hdr10plus_metadata: dhdr = f'--dhdr10-info "{settings.hdr10plus_metadata}"' - # TODO trim + trim = "" + try: + rate = video.average_frame_rate or video.frame_rate + if "/" in rate: + over, under = [int(x) for x in rate.split("/")] + rate = over / under + else: + rate = float(rate) + except Exception: + logger.exception("Could not get framerate of this movie!") + else: + if video.video_settings.end_time: + end_frame = int(video.video_settings.end_time * rate) + start_frame = 0 + if video.video_settings.start_time: + start_frame = int(video.video_settings.start_time * rate) + trim = f"--trim {start_frame}:{end_frame}" + elif video.video_settings.start_time: + trim = f"--seek {video.video_settings.start_time}" transform = "" if video.video_settings.vertical_flip or video.video_settings.horizontal_flip: @@ -100,23 +127,29 @@ def build(fastflix: FastFlix): f'"{unixy(fastflix.config.nvencc)}"', "-i", f'"{unixy(video.source)}"', - (f"--seek {video.video_settings.start_time}" if video.video_settings.start_time else ""), + f"--video-streamid {video_track}", + trim, (f"--vpp-rotate {video.video_settings.rotate}" if video.video_settings.rotate else ""), transform, - (f'--scale {video.video_settings.scale.replace(":", "x")}' if video.video_settings.scale else ""), + (f'--output-res {video.video_settings.scale.replace(":", "x")}' if video.video_settings.scale else ""), crop, + (f"--video-metadata 1?clear" if video.video_settings.remove_metadata else "--video-metadata 1?copy"), + (f'--video-metadata 1?title="{video.video_settings.video_title}"' if video.video_settings.video_title else ""), + ("--chapter-copy" if video.video_settings.copy_chapters else ""), "-c", "hevc", - "--vbr", - settings.bitrate, + (f"--vbr {settings.bitrate}" if settings.bitrate else f"--cqp {settings.cqp}"), + (f"--qp-init {settings.init_q}" if settings.init_q else ""), + (f"--qp-min {settings.min_q}" if settings.min_q else ""), + (f"--qp-max {settings.max_q}" if settings.max_q else ""), "--preset", settings.preset, "--profile", settings.profile, "--tier", settings.tier, - f'{f"--lookahead {settings.lookahead}" if settings.lookahead else ""}', - f'{"--aq" if settings.spatial_aq else "--no-aq"}', + (f"--lookahead {settings.lookahead}" if settings.lookahead else ""), + ("--aq" if settings.spatial_aq else "--no-aq"), "--colormatrix", (video.video_settings.color_space or "auto"), "--transfer", @@ -127,7 +160,7 @@ def build(fastflix: FastFlix): (max_cll if max_cll else ""), (dhdr if dhdr else ""), "--output-depth", - ("10" if video.current_video_stream.bit_depth > 8 else "8"), + ("10" if video.current_video_stream.bit_depth > 8 and not video.video_settings.remove_hdr else "8"), "--multipass", settings.multipass, "--mv-precision", @@ -137,8 +170,8 @@ def build(fastflix: FastFlix): "--colorrange", "auto", f"--avsync {'cfr' if video.frame_rate == video.average_frame_rate else 'vfr'}", - f'{f"--interlace {video.interlaced}" if video.interlaced else ""}', - f'{"--vpp-yadif" if video.video_settings.deinterlace else ""}', + (f"--interlace {video.interlaced}" if video.interlaced else ""), + ("--vpp-yadif" if video.video_settings.deinterlace else ""), (f"--output-res {video.video_settings.scale}" if video.video_settings.scale else ""), (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index b8a73f89..7d18073a 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import logging +from typing import List, Optional from box import Box from qtpy import QtCore, QtWidgets, QtGui @@ -18,17 +19,17 @@ presets = ["default", "performance", "quality"] recommended_bitrates = [ - "800k (320x240p @ 30fps)", - "1000k (640x360p @ 30fps)", - "1500k (640x480p @ 30fps)", - "2000k (1280x720p @ 30fps)", - "5000k (1280x720p @ 60fps)", - "6000k (1080p @ 30fps)", - "9000k (1080p @ 60fps)", - "15000k (1440p @ 30fps)", - "25000k (1440p @ 60fps)", - "35000k (2160p @ 30fps)", - "50000k (2160p @ 60fps)", + "200k (320x240p @ 30fps)", + "300k (640x360p @ 30fps)", + "1000k (640x480p @ 30fps)", + "1750k (1280x720p @ 30fps)", + "2500k (1280x720p @ 60fps)", + "4000k (1920x1080p @ 30fps)", + "5000k (1920x1080p @ 60fps)", + "7000k (2560x1440p @ 30fps)", + "10000k (2560x1440p @ 60fps)", + "15000k (3840x2160p @ 30fps)", + "20000k (3840x2160p @ 60fps)", "Custom", ] @@ -51,8 +52,6 @@ "Custom", ] -pix_fmts = ["8-bit: yuv420p", "10-bit: p010le"] - class NVENCC(SettingPanel): profile_name = "nvencc_hevc" @@ -67,10 +66,10 @@ def __init__(self, parent, main, app: FastFlixApp): self.widgets = Box(mode=None) - self.mode = "CRF" + self.mode = "Bitrate" self.updating_settings = False - grid.addLayout(self.init_modes(), 0, 2, 3, 4) + grid.addLayout(self.init_modes(), 0, 2, 4, 4) grid.addLayout(self._add_custom(title="Custom NVEncC options", disable_both_passes=True), 10, 0, 1, 6) grid.addLayout(self.init_preset(), 0, 0, 1, 2) @@ -85,18 +84,25 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_mv_precision(), 5, 0, 1, 2) grid.addLayout(self.init_multipass(), 6, 0, 1, 2) - grid.addLayout(self.init_dhdr10_info(), 4, 2, 1, 3) - grid.addLayout(self.init_dhdr10_warning_and_opt(), 4, 5, 1, 1) + qp_line = QtWidgets.QHBoxLayout() + qp_line.addLayout(self.init_init_q()) + qp_line.addStretch(1) + qp_line.addLayout(self.init_min_q()) + qp_line.addStretch(1) + qp_line.addLayout(self.init_max_q()) - # a = QtWidgets.QHBoxLayout() - # a.addLayout(self.init_rc_lookahead()) - # a.addStretch(1) - # a.addLayout(self.init_level()) - # a.addStretch(1) + grid.addLayout(qp_line, 4, 2, 1, 4) + + advanced = QtWidgets.QHBoxLayout() + advanced.addLayout(self.init_level()) + advanced.addStretch(1) # a.addLayout(self.init_gpu()) # a.addStretch(1) # a.addLayout(self.init_b_ref_mode()) - # grid.addLayout(a, 3, 2, 1, 4) + grid.addLayout(advanced, 5, 2, 1, 4) + + grid.addLayout(self.init_dhdr10_info(), 6, 2, 1, 3) + grid.addLayout(self.init_dhdr10_warning_and_opt(), 6, 5, 1, 1) grid.setRowStretch(9, 1) @@ -232,6 +238,34 @@ def init_b_ref_mode(self): self.widgets.gpu.setMinimumWidth(50) return layout + @staticmethod + def _qp_range(): + return [str(x) for x in range(0, 52)] + + def init_min_q(self): + layout = QtWidgets.QHBoxLayout() + layout.addWidget(QtWidgets.QLabel(t("Min Q"))) + layout.addWidget(self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=40)) + return layout + + def init_init_q(self): + layout = QtWidgets.QHBoxLayout() + layout.addWidget(QtWidgets.QLabel(t("Init Q"))) + layout.addWidget(self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=40)) + return layout + + def init_max_q(self): + layout = QtWidgets.QHBoxLayout() + layout.addWidget(QtWidgets.QLabel(t("Max Q"))) + layout.addWidget(self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=40)) + return layout + def init_dhdr10_info(self): layout = self._add_file_select( label="HDR10+ Metadata", @@ -260,14 +294,14 @@ def init_dhdr10_warning_and_opt(self): return layout def init_modes(self): - layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="qp", add_qp=False) - self.qp_radio.setChecked(False) - self.bitrate_radio.setChecked(True) - self.qp_radio.setDisabled(True) + layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="cqp") + # self.qp_radio.setChecked(False) + # self.bitrate_radio.setChecked(True) + # self.qp_radio.setDisabled(True) return layout def mode_update(self): - self.widgets.custom_qp.setDisabled(self.widgets.qp.currentText() != "Custom") + self.widgets.custom_cqp.setDisabled(self.widgets.cqp.currentText() != "Custom") self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom") self.main.build_commands() @@ -280,6 +314,14 @@ def setting_change(self, update=True): self.main.page_update() self.updating_settings = False + def gather_q(self, group) -> Optional[str]: + if self.mode.lower() != "bitrate": + return None + if self.widgets[f"{group}_q_i"].currentIndex() > 0: + if self.widgets[f"{group}_q_p"].currentIndex() > 0 and self.widgets[f"{group}_q_b"].currentIndex() > 0: + return f'{self.widgets[f"{group}_q_i"].currentText()}:{self.widgets[f"{group}_q_p"].currentText()}:{self.widgets[f"{group}_q_b"].currentText()}' + return self.widgets[f"{group}_q_i"].currentText() + def update_video_encoder_settings(self): settings = NVEncCSettings( @@ -291,24 +333,31 @@ def update_video_encoder_settings(self): hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"), multipass=self.widgets.multipass.currentText(), mv_precision=self.widgets.mv_precision.currentText(), + init_q=self.gather_q("init"), + min_q=self.gather_q("min"), + max_q=self.gather_q("max"), # pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(), - # extra=self.ffmpeg_extras, + extra=self.ffmpeg_extras, # tune=tune.split("-")[0].strip(), # extra_both_passes=self.widgets.extra_both_passes.isChecked(), # rc=self.widgets.rc.currentText() if self.widgets.rc.currentIndex() != 0 else None, # spatial_aq=self.widgets.spatial_aq.currentIndex(), # rc_lookahead=int(self.widgets.rc_lookahead.text() or 0), - # level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, + level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, # gpu=int(self.widgets.gpu.currentText() or -1) if self.widgets.gpu.currentIndex() != 0 else -1, # b_ref_mode=self.widgets.b_ref_mode.currentText(), ) encode_type, q_value = self.get_mode_settings() - settings.qp = q_value if encode_type == "qp" else None + settings.cqp = q_value if encode_type == "qp" else None settings.bitrate = q_value if encode_type == "bitrate" else None self.app.fastflix.current_video.video_settings.video_encoder_settings = settings def set_mode(self, x): self.mode = x.text() + for group in ("init", "max", "min"): + for frame_type in ("i", "p", "b"): + self.widgets[f"{group}_q_{frame_type}"].setEnabled(self.mode.lower() == "bitrate") + self.main.build_commands() def new_source(self): diff --git a/fastflix/encoders/vp9/command_builder.py b/fastflix/encoders/vp9/command_builder.py index 5f6783cd..6d86c8d1 100644 --- a/fastflix/encoders/vp9/command_builder.py +++ b/fastflix/encoders/vp9/command_builder.py @@ -24,16 +24,18 @@ def build(fastflix: FastFlix): beginning = re.sub("[ ]+", " ", beginning) - details = f"-quality {settings.quality} -speed {settings.speed} -profile:v {settings.profile}" + details = f"-quality:v {settings.quality} -profile:v {settings.profile} -tile-columns:v {settings.tile_columns} -tile-rows:v {settings.tile_rows} " if settings.bitrate: - command_1 = f"{beginning} -b:v {settings.bitrate} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}" - command_2 = f"{beginning} -b:v {settings.bitrate} {details} -pass 2 {settings.extra} {ending}" + command_1 = f"{beginning} -speed:v {'4' if settings.fast_first_pass else settings.speed} -b:v {settings.bitrate} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}" + command_2 = ( + f"{beginning} -speed:v {settings.speed} -b:v {settings.bitrate} {details} -pass 2 {settings.extra} {ending}" + ) elif settings.crf: - command_1 = f"{beginning} -b:v 0 -crf {settings.crf} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}" + command_1 = f"{beginning} -b:v 0 -crf:v {settings.crf} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}" command_2 = ( - f"{beginning} -b:v 0 -crf {settings.crf} {details} " + f"{beginning} -b:v 0 -crf:v {settings.crf} {details} " f'{"-pass 2" if not settings.single_pass else ""} {settings.extra} {ending}' ) diff --git a/fastflix/encoders/vp9/settings_panel.py b/fastflix/encoders/vp9/settings_panel.py index 6c426468..8f72e494 100644 --- a/fastflix/encoders/vp9/settings_panel.py +++ b/fastflix/encoders/vp9/settings_panel.py @@ -61,9 +61,19 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_max_mux(), 3, 0, 1, 2) grid.addLayout(self.init_profile(), 4, 0, 1, 2) + grid.addLayout(self.init_tile_columns(), 5, 0, 1, 2) + grid.addLayout(self.init_tile_rows(), 6, 0, 1, 2) + grid.addLayout(self.init_modes(), 0, 2, 5, 4) - grid.addLayout(self.init_single_pass(), 5, 2, 1, 2) - grid.addLayout(self.init_row_mt(), 5, 4, 1, 2) + + checkboxes = QtWidgets.QHBoxLayout() + checkboxes.addLayout(self.init_single_pass()) + checkboxes.addStretch(1) + checkboxes.addLayout(self.init_row_mt()) + checkboxes.addStretch(1) + checkboxes.addLayout(self.init_fast_first_pass()) + + grid.addLayout(checkboxes, 5, 2, 1, 4) # grid.addWidget(QtWidgets.QWidget(), 8, 0) grid.setRowStretch(8, 1) @@ -138,6 +148,32 @@ def init_row_mt(self): opt="row_mt", ) + def init_tile_columns(self): + return self._add_combo_box( + label="Tile Columns", + tooltip="Log2 of number of tile columns to encode faster (lesser quality)", + widget_name="tile_columns", + options=[str(x) for x in range(-1, 7)], + opt="tile_columns", + ) + + def init_tile_rows(self): + return self._add_combo_box( + label="Tile Rows", + tooltip="Log2 of number of tile rows to encode faster (lesser quality)", + widget_name="tile_rows", + options=[str(x) for x in range(-1, 3)], + opt="tile_rows", + ) + + def init_fast_first_pass(self): + return self._add_check_box( + label="Fast first pass", + tooltip="Set speed to 4 for first pass", + widget_name="fast_first_pass", + opt="fast_first_pass", + ) + def init_single_pass(self): return self._add_check_box(label="Single Pass (CRF)", tooltip="", widget_name="single_pass", opt="single_pass") @@ -160,6 +196,11 @@ def update_video_encoder_settings(self): profile=self.widgets.profile.currentIndex(), extra=self.ffmpeg_extras, extra_both_passes=self.widgets.extra_both_passes.isChecked(), + fast_first_pass=self.widgets.fast_first_pass.isChecked(), + tile_columns=self.widgets.tile_columns.currentText() + if self.widgets.tile_columns.currentIndex() > 0 + else "-1", + tile_rows=self.widgets.tile_rows.currentText() if self.widgets.tile_rows.currentIndex() > 0 else "-1", ) encode_type, q_value = self.get_mode_settings() settings.crf = q_value if encode_type == "qp" else None diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index 62756bcf..e94c04ef 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -96,7 +96,7 @@ class NVEncCSettings(EncoderSettings): preset: str = "quality" profile: str = "main" bitrate: Optional[str] = "6000k" - qp: Optional[str] = None + cqp: Optional[str] = None cq: int = 0 spatial_aq: bool = True lookahead: Optional[int] = None @@ -105,6 +105,9 @@ class NVEncCSettings(EncoderSettings): hdr10plus_metadata: str = "" multipass: str = "2pass-full" mv_precision: str = "auto" + init_q: Optional[str] = None + min_q: Optional[str] = None + max_q: Optional[str] = None class rav1eSettings(EncoderSettings): @@ -139,6 +142,9 @@ class VP9Settings(EncoderSettings): single_pass: bool = False crf: Optional[Union[int, float]] = 31 bitrate: Optional[str] = None + fast_first_pass: Optional[bool] = True + tile_columns: str = "-1" + tile_rows: str = "-1" class AOMAV1Settings(EncoderSettings): diff --git a/fastflix/models/fastflix.py b/fastflix/models/fastflix.py index c7b8454c..8e3960a4 100644 --- a/fastflix/models/fastflix.py +++ b/fastflix/models/fastflix.py @@ -19,6 +19,7 @@ class FastFlix(BaseModel): data_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) log_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs" queue_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml" + queue_lock_file: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.lock" ffmpeg_version: str = "" ffmpeg_config: List[str] = "" ffprobe_version: str = "" diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py new file mode 100644 index 00000000..d750ff52 --- /dev/null +++ b/fastflix/models/queue.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from typing import List, Optional, Union + +from box import Box +from pydantic import BaseModel, Field + + +class STATUS: + CONVERTED = "converted" + CANCELLED = "cancelled" + ERRORED = "errored" + IN_PROGRESS = "in_progress" + READY = "ready" + + +class REQUEST: + CANCEL = "cancel" + PAUSE_ENCODE = "pause_encode" + RESUME_ENCODE = "resume_encode" + PAUSE_QUEUE = "pause_queue" + RESUME_QUEUE = "resume_queue" + + +class QueueItem(BaseModel): + video_uuid: str + command_uuid: str + command: str + work_dir: str + filename: str + status: Union[STATUS.CONVERTED, STATUS.CANCELLED, STATUS.IN_PROGRESS, STATUS.ERRORED, STATUS.READY] + + +class Queue(BaseModel): + revision: int = 0 + request: Optional[ + Union[REQUEST.CANCEL, REQUEST.PAUSE_QUEUE, REQUEST.RESUME_QUEUE, REQUEST.PAUSE_ENCODE, REQUEST.RESUME_ENCODE] + ] = None + queue: List[QueueItem] = Field(default_factory=list) + after_done_command: Optional[str] = None + + +def get_queue(queue_file): + loaded = Box.from_yaml(filename=queue_file) + queue = Queue(revision=loaded.revision, request=loaded.request, after_done_command=loaded.after_done_command) + queue.request = [QueueItem(**item) for item in loaded.queue] + return queue + + +def save_queue(queue, queue_file): + queue.revision += 1 + Box(queue.dict()).to_yaml(filename=queue_file) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 7e5923d3..0273f705 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1074,19 +1074,18 @@ def reload_video_from_queue(self, video: Video): self.widgets.video_track.clear() self.widgets.video_track.addItems(text_video_tracks) - if self.app.fastflix.current_video.video_settings.crop: - width, height, left, top = self.app.fastflix.current_video.video_settings.crop.split(":") - right = str(self.app.fastflix.current_video.width - (int(width) + int(left))) - bottom = str(self.app.fastflix.current_video.height - (int(height) + int(top))) - else: - top, left, right, bottom = "0", "0", "0", "0" - end_time = self.app.fastflix.current_video.video_settings.end_time or video.duration - self.widgets.crop.top.setText(top) - self.widgets.crop.left.setText(left) - self.widgets.crop.right.setText(right) - self.widgets.crop.bottom.setText(bottom) + if self.app.fastflix.current_video.video_settings.crop: + self.widgets.crop.top.setText(str(self.app.fastflix.current_video.video_settings.crop.top)) + self.widgets.crop.left.setText(str(self.app.fastflix.current_video.video_settings.crop.left)) + self.widgets.crop.right.setText(str(self.app.fastflix.current_video.video_settings.crop.right)) + self.widgets.crop.bottom.setText(str(self.app.fastflix.current_video.video_settings.crop.bottom)) + else: + self.widgets.crop.top.setText("0") + self.widgets.crop.left.setText("0") + self.widgets.crop.right.setText("0") + self.widgets.crop.bottom.setText("0") self.widgets.start_time.setText(self.number_to_time(video.video_settings.start_time)) self.widgets.end_time.setText(self.number_to_time(end_time)) self.widgets.video_title.setText(self.app.fastflix.current_video.video_settings.video_title) @@ -1298,7 +1297,7 @@ def thumbnail_generated(self, success=False): self.widgets.preview.setText(t("Error Updating Thumbnail")) return pixmap = QtGui.QPixmap(str(self.thumb_file)) - pixmap = pixmap.scaled(320, 213, QtCore.Qt.KeepAspectRatio) + pixmap = pixmap.scaled(320, 190, QtCore.Qt.KeepAspectRatio) self.widgets.preview.setPixmap(pixmap) def build_scale(self): diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index 23a794b6..1222c616 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -435,7 +435,7 @@ def reload(self, original_tracks, audio_formats): track.widgets.enable_check.setChecked(enabled) if enabled: existing_track = [x for x in original_tracks if x.index == track.index][0] - track.widgets.downmix.setCurrentIndex(existing_track.downmix) + track.widgets.downmix.setCurrentText(existing_track.downmix) track.widgets.convert_to.setCurrentText(existing_track.conversion_codec) track.widgets.convert_bitrate.setCurrentText(existing_track.conversion_bitrate) track.widgets.title.setText(existing_track.title) diff --git a/requirements-build.txt b/requirements-build.txt index bef8b02c..ac8d25e2 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1,11 +1,12 @@ appdirs colorama coloredlogs +filelock iso639-lang mistune psutil pydantic -pyinstaller==4.1 +pyinstaller==4.2 pyqt5 python-box qtpy diff --git a/requirements.txt b/requirements.txt index 339c8a90..d6e1273f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,12 @@ appdirs colorama coloredlogs +filelock iso639-lang mistune psutil pydantic -pyside2 +pyqt5 python-box qtpy requests From 2d3b8eaa2a1f91216e368a476bf88c7f6945863f Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Fri, 29 Jan 2021 22:01:16 -0600 Subject: [PATCH 19/50] even more options for nvencc --- fastflix/data/languages.yaml | 20 ++++ .../encoders/nvencc_hevc/command_builder.py | 12 ++- .../encoders/nvencc_hevc/settings_panel.py | 96 +++++++++++++++---- fastflix/models/encode.py | 5 + 4 files changed, 115 insertions(+), 18 deletions(-) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 61a6895e..cd61ca7e 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2809,3 +2809,23 @@ Min Q: eng: Min Q Max Q: eng: Max Q +Crop Detect Points: + eng: Crop Detect Points +VBR Target: + eng: VBR Target +Level: + eng: Level +Set the encoding level restriction: + eng: Set the encoding level restriction +B Frames: + eng: B Frames +Ref Frames: + eng: Ref Frames +B Ref Mode: + eng: B Ref Mode +Use B frames as references: + eng: Use B frames as references +Metrics: + eng: Metrics +Calculate PSNR and SSIM and show in the encoder output: + eng: Calculate PSNR and SSIM and show in the encoder output diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 302f985d..f25de3b0 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -123,6 +123,10 @@ def build(fastflix: FastFlix): if video.video_settings.crop: crop = f"--crop {video.video_settings.crop.left},{video.video_settings.crop.top},{video.video_settings.crop.right},{video.video_settings.crop.bottom}" + vbv = "" + if video.video_settings.maxrate: + vbv = f"--max-bitrate {video.video_settings.maxrate} --vbv-bufsize {video.video_settings.bufsize}" + command = [ f'"{unixy(fastflix.config.nvencc)}"', "-i", @@ -138,10 +142,15 @@ def build(fastflix: FastFlix): ("--chapter-copy" if video.video_settings.copy_chapters else ""), "-c", "hevc", - (f"--vbr {settings.bitrate}" if settings.bitrate else f"--cqp {settings.cqp}"), + (f"--vbr {settings.bitrate.rstrip('k')}" if settings.bitrate else f"--cqp {settings.cqp}"), + vbv, + (f"--vbr-target {settings.vbr_target}" if settings.vbr_target is not None else ""), (f"--qp-init {settings.init_q}" if settings.init_q else ""), (f"--qp-min {settings.min_q}" if settings.min_q else ""), (f"--qp-max {settings.max_q}" if settings.max_q else ""), + (f"--bframes {settings.b_frames}" if settings.b_frames else ""), + (f"--ref {settings.ref}" if settings.ref else ""), + f"--bref-mode {settings.b_ref_mode}", "--preset", settings.preset, "--profile", @@ -175,6 +184,7 @@ def build(fastflix: FastFlix): (f"--output-res {video.video_settings.scale}" if video.video_settings.scale else ""), (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, + "--psnr --ssim" if settings.metrics else "", build_audio(video.video_settings.audio_tracks), build_subtitle(video.video_settings.subtitle_tracks), settings.extra, diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 7d18073a..972a1946 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -85,6 +85,8 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_multipass(), 6, 0, 1, 2) qp_line = QtWidgets.QHBoxLayout() + qp_line.addLayout(self.init_vbr_target()) + qp_line.addStretch(1) qp_line.addLayout(self.init_init_q()) qp_line.addStretch(1) qp_line.addLayout(self.init_min_q()) @@ -94,11 +96,16 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(qp_line, 4, 2, 1, 4) advanced = QtWidgets.QHBoxLayout() - advanced.addLayout(self.init_level()) + advanced.addLayout(self.init_ref()) + advanced.addStretch(1) + advanced.addLayout(self.init_b_frames()) advanced.addStretch(1) + advanced.addLayout(self.init_level()) # a.addLayout(self.init_gpu()) - # a.addStretch(1) - # a.addLayout(self.init_b_ref_mode()) + advanced.addStretch(1) + advanced.addLayout(self.init_b_ref_mode()) + advanced.addStretch(1) + advanced.addLayout(self.init_metrics()) grid.addLayout(advanced, 5, 2, 1, 4) grid.addLayout(self.init_dhdr10_info(), 6, 2, 1, 3) @@ -183,7 +190,7 @@ def init_multipass(self): label="Multipass", tooltip="", widget_name="multipass", - options=["none", "2pass-quarter", "2pass-full"], + options=["None", "2pass-quarter", "2pass-full"], opt="multipass", ) @@ -192,7 +199,7 @@ def init_mv_precision(self): label="Motion vector accuracy", tooltip="Q-pel is highest precision", widget_name="mv_precision", - options=["auto", "Q-pel", "half-pel", "full-pel"], + options=[t("Auto"), "Q-pel", "half-pel", "full-pel"], opt="mv_precision", ) @@ -210,7 +217,22 @@ def init_level(self): label="Level", tooltip="Set the encoding level restriction", widget_name="level", - options=["auto", "1.0", "2.0", "2.1", "3.0", "3.1", "4.0", "4.1", "5.0", "5.1", "5.2", "6.0", "6.1", "6.2"], + options=[ + t("Auto"), + "1.0", + "2.0", + "2.1", + "3.0", + "3.1", + "4.0", + "4.1", + "5.0", + "5.1", + "5.2", + "6.0", + "6.1", + "6.2", + ], opt="level", ) self.widgets.level.setMinimumWidth(60) @@ -234,8 +256,8 @@ def init_b_ref_mode(self): widget_name="b_ref_mode", opt="b_ref_mode", options=["disabled", "each", "middle"], + min_width=60, ) - self.widgets.gpu.setMinimumWidth(50) return layout @staticmethod @@ -245,27 +267,62 @@ def _qp_range(): def init_min_q(self): layout = QtWidgets.QHBoxLayout() layout.addWidget(QtWidgets.QLabel(t("Min Q"))) - layout.addWidget(self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=40)) - layout.addWidget(self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=40)) - layout.addWidget(self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=45)) + layout.addWidget(self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=45)) + layout.addWidget(self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=45)) return layout def init_init_q(self): layout = QtWidgets.QHBoxLayout() layout.addWidget(QtWidgets.QLabel(t("Init Q"))) - layout.addWidget(self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=40)) - layout.addWidget(self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=40)) - layout.addWidget(self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=45)) + layout.addWidget(self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=45)) + layout.addWidget(self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=45)) return layout def init_max_q(self): layout = QtWidgets.QHBoxLayout() layout.addWidget(QtWidgets.QLabel(t("Max Q"))) - layout.addWidget(self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=40)) - layout.addWidget(self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=40)) - layout.addWidget(self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=40)) + layout.addWidget(self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=45)) + layout.addWidget(self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=45)) + layout.addWidget(self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=45)) return layout + def init_vbr_target(self): + return self._add_combo_box( + widget_name="vbr_target", + label="VBR Target", + options=[t("Auto")] + self._qp_range(), + opt="vbr_target", + min_width=60, + ) + + def init_b_frames(self): + return self._add_combo_box( + widget_name="b_frames", + label="B Frames", + options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"], + opt="b_frames", + min_width=60, + ) + + def init_ref(self): + return self._add_combo_box( + widget_name="ref", + label="Ref Frames", + options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"], + opt="ref", + min_width=60, + ) + + def init_metrics(self): + return self._add_check_box( + widget_name="metrics", + opt="metrics", + label="Metrics", + tooltip="Calculate PSNR and SSIM and show in the encoder output", + ) + def init_dhdr10_info(self): layout = self._add_file_select( label="HDR10+ Metadata", @@ -343,7 +400,12 @@ def update_video_encoder_settings(self): # rc=self.widgets.rc.currentText() if self.widgets.rc.currentIndex() != 0 else None, # spatial_aq=self.widgets.spatial_aq.currentIndex(), # rc_lookahead=int(self.widgets.rc_lookahead.text() or 0), + metrics=self.widgets.metrics.isChecked(), level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, + b_frames=self.widgets.b_frames.currentText() if self.widgets.b_frames.currentIndex() != 0 else None, + ref=self.widgets.ref.currentText() if self.widgets.ref.currentIndex() != 0 else None, + vbr_target=self.widgets.vbr_target.currentText() if self.widgets.vbr_target.currentIndex() > 0 else None, + b_ref_mode=self.widgets.b_ref_mode.currentText(), # gpu=int(self.widgets.gpu.currentText() or -1) if self.widgets.gpu.currentIndex() != 0 else -1, # b_ref_mode=self.widgets.b_ref_mode.currentText(), ) @@ -357,7 +419,7 @@ def set_mode(self, x): for group in ("init", "max", "min"): for frame_type in ("i", "p", "b"): self.widgets[f"{group}_q_{frame_type}"].setEnabled(self.mode.lower() == "bitrate") - + self.widgets.vbr_target.setEnabled(self.mode.lower() == "bitrate") self.main.build_commands() def new_source(self): diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index e94c04ef..b78805f0 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -108,6 +108,11 @@ class NVEncCSettings(EncoderSettings): init_q: Optional[str] = None min_q: Optional[str] = None max_q: Optional[str] = None + vbr_target: Optional[str] = None + b_frames: Optional[str] = None + b_ref_mode: str = "disabled" + ref: Optional[str] = None + metrics: bool = True class rav1eSettings(EncoderSettings): From 5f654b031fcebedb7931816d3a284519a14bf64a Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Fri, 29 Jan 2021 22:11:03 -0600 Subject: [PATCH 20/50] adding warning for nvencc encode settings --- fastflix/encoders/nvencc_hevc/command_builder.py | 11 +++++++---- fastflix/widgets/main.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index f25de3b0..402690f9 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -88,12 +88,11 @@ def build(fastflix: FastFlix): trim = "" try: - rate = video.average_frame_rate or video.frame_rate - if "/" in rate: - over, under = [int(x) for x in rate.split("/")] + if "/" in video.frame_rate: + over, under = [int(x) for x in video.frame_rate.split("/")] rate = over / under else: - rate = float(rate) + rate = float(video.frame_rate) except Exception: logger.exception("Could not get framerate of this movie!") else: @@ -106,6 +105,10 @@ def build(fastflix: FastFlix): elif video.video_settings.start_time: trim = f"--seek {video.video_settings.start_time}" + if (video.frame_rate != video.average_frame_rate) and trim: + logger.warning("Cannot use 'trim' when working with variable frame rate videos") + trim = "" + transform = "" if video.video_settings.vertical_flip or video.video_settings.horizontal_flip: transform = f"--vpp-transform flip_x={'true' if video.video_settings.horizontal_flip else 'false'},flip_y={'true' if video.video_settings.vertical_flip else 'false'}" diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 0273f705..6324ba19 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1315,7 +1315,7 @@ def get_all_settings(self): end_time = self.end_time if self.end_time == float(self.app.fastflix.current_video.format.get("duration", 0)): end_time = 0 - if self.end_time and self.end_time - 0.1 <= self.app.fastflix.current_video.duration <= self.end_time + 0.1: + if self.end_time and (self.end_time - 0.1 <= self.app.fastflix.current_video.duration <= self.end_time + 0.1): end_time = 0 scale = self.build_scale() From d8cf08ab308bcc84db32017c0dd3444ab0781f52 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 30 Jan 2021 13:54:20 -0600 Subject: [PATCH 21/50] queue ideas --- .../encoders/nvencc_hevc/command_builder.py | 6 ++- fastflix/models/queue.py | 46 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 402690f9..2f544d5f 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -18,8 +18,12 @@ def build_audio(audio_tracks): command_list = [] copies = [] + track_ids = set() for track in audio_tracks: + if track.index in track_ids: + logger.warning("NVEncC does not support copy and duplicate of audio tracks!") + track_ids.add(track.index) if track.language: command_list.append(f"--audio-metadata {track.outdex}?language={track.language}") if not track.conversion_codec or track.conversion_codec == "none": @@ -147,7 +151,7 @@ def build(fastflix: FastFlix): "hevc", (f"--vbr {settings.bitrate.rstrip('k')}" if settings.bitrate else f"--cqp {settings.cqp}"), vbv, - (f"--vbr-target {settings.vbr_target}" if settings.vbr_target is not None else ""), + (f"--vbr-quality {settings.vbr_target}" if settings.vbr_target is not None else ""), (f"--qp-init {settings.init_q}" if settings.init_q else ""), (f"--qp-min {settings.min_q}" if settings.min_q else ""), (f"--qp-max {settings.max_q}" if settings.max_q else ""), diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index d750ff52..722095b9 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- from typing import List, Optional, Union +import os +from pathlib import Path from box import Box from pydantic import BaseModel, Field +from fastflix.models.video import Video + class STATUS: CONVERTED = "converted" @@ -21,31 +25,43 @@ class REQUEST: RESUME_QUEUE = "resume_queue" -class QueueItem(BaseModel): - video_uuid: str - command_uuid: str - command: str - work_dir: str - filename: str - status: Union[STATUS.CONVERTED, STATUS.CANCELLED, STATUS.IN_PROGRESS, STATUS.ERRORED, STATUS.READY] +# class QueueItem(BaseModel): +# video_uuid: str +# command_uuid: str +# command: str +# work_dir: str +# filename: str +# status: Union[STATUS.CONVERTED, STATUS.CANCELLED, STATUS.IN_PROGRESS, STATUS.ERRORED, STATUS.READY] class Queue(BaseModel): - revision: int = 0 - request: Optional[ - Union[REQUEST.CANCEL, REQUEST.PAUSE_QUEUE, REQUEST.RESUME_QUEUE, REQUEST.PAUSE_ENCODE, REQUEST.RESUME_ENCODE] - ] = None - queue: List[QueueItem] = Field(default_factory=list) + # request: Optional[ + # Union[REQUEST.CANCEL, REQUEST.PAUSE_QUEUE, REQUEST.RESUME_QUEUE, REQUEST.PAUSE_ENCODE, REQUEST.RESUME_ENCODE] + # ] = None + queue: List[Video] = Field(default_factory=list) after_done_command: Optional[str] = None def get_queue(queue_file): loaded = Box.from_yaml(filename=queue_file) - queue = Queue(revision=loaded.revision, request=loaded.request, after_done_command=loaded.after_done_command) - queue.request = [QueueItem(**item) for item in loaded.queue] + for video in loaded["video"]: + video["source"] = Path(video["source"]) + video["work_path"] = Path(video["work_path"]) + video["video_settings"]["output_path"] = Path(video["video_settings"]["output_path"]) + + queue = Queue(after_done_command=loaded.after_done_command) + queue.queue = [Video(**video) for video in loaded["video"]] + + # queue.request = [QueueItem(**item) for item in loaded.queue] return queue def save_queue(queue, queue_file): queue.revision += 1 - Box(queue.dict()).to_yaml(filename=queue_file) + dict_queue = queue.dict() + for video in dict_queue["video"]: + video["source"] = os.fspath(video["source"]) + video["work_path"] = os.fspath(video["work_path"]) + video["video_settings"]["output_path"] = os.fspath(video["video_settings"]["output_path"]) + + Box().to_yaml(filename=queue_file) From 6dedb742d92bd442d213c8386b4fe4aacec69608 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 30 Jan 2021 18:42:43 -0600 Subject: [PATCH 22/50] some fixes, some queue stuff --- fastflix/conversion_worker.py | 2 +- fastflix/encoders/common/helpers.py | 13 ---- .../encoders/nvencc_hevc/settings_panel.py | 2 +- fastflix/encoders/rav1e/settings_panel.py | 6 +- fastflix/encoders/webp/settings_panel.py | 13 ++-- fastflix/models/config.py | 14 +--- fastflix/models/encode.py | 17 ++++- fastflix/models/queue.py | 74 +++++++------------ fastflix/widgets/main.py | 5 +- pyproject.toml | 33 --------- tests/test_queue.py | 13 ++++ tests/test_version_check.py | 6 +- 12 files changed, 77 insertions(+), 121 deletions(-) create mode 100644 tests/test_queue.py diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index a818eece..1214dd4e 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -11,7 +11,7 @@ from fastflix.command_runner import BackgroundRunner from fastflix.language import t from fastflix.shared import file_date -from fastflix.models.queue import STATUS, REQUEST, QueueItem, Queue +from fastflix.models.queue import STATUS, REQUEST, Queue logger = logging.getLogger("fastflix-core") diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index 5078f978..2932834c 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -17,23 +17,10 @@ null = "NUL" -class Loop: - item = "loop" - - def __init__(self, condition, commands, dirs=(), files=(), name="", ensure_paths=()): - self.name = name - self.condition = condition - self.commands = commands - self.ensure_paths = ensure_paths - self.dirs = dirs - self.files = files - - class Command(BaseModel): command: str item = "command" name: str = "" - ensure_paths: List = () exe: str = None shell: bool = False uuid: str = Field(default_factory=lambda: str(uuid.uuid4())) diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 972a1946..bab30693 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -199,7 +199,7 @@ def init_mv_precision(self): label="Motion vector accuracy", tooltip="Q-pel is highest precision", widget_name="mv_precision", - options=[t("Auto"), "Q-pel", "half-pel", "full-pel"], + options=["Auto", "Q-pel", "half-pel", "full-pel"], opt="mv_precision", ) diff --git a/fastflix/encoders/rav1e/settings_panel.py b/fastflix/encoders/rav1e/settings_panel.py index 6b74e8e7..73867b60 100644 --- a/fastflix/encoders/rav1e/settings_panel.py +++ b/fastflix/encoders/rav1e/settings_panel.py @@ -106,10 +106,12 @@ def init_tile_columns(self): ) def init_tiles(self): - return self._add_combo_box("Tiles", [str(x) for x in range(-1, 17)], "tiles", opt="tiles") + return self._add_combo_box( + label="Tiles", options=[str(x) for x in range(-1, 17)], widget_name="tiles", opt="tiles" + ) def init_single_pass(self): - return self._add_check_box("Single Pass (Bitrate)", "single_pass", opt="single_pass") + return self._add_check_box(label="Single Pass (Bitrate)", widget_name="single_pass", opt="single_pass") def init_pix_fmt(self): return self._add_combo_box( diff --git a/fastflix/encoders/webp/settings_panel.py b/fastflix/encoders/webp/settings_panel.py index 1945962e..e2296b6c 100644 --- a/fastflix/encoders/webp/settings_panel.py +++ b/fastflix/encoders/webp/settings_panel.py @@ -31,20 +31,23 @@ def __init__(self, parent, main, app: FastFlixApp): self.setLayout(grid) def init_lossless(self): - return self._add_combo_box("lossless", ["yes", "no"], "lossless", default=1) + return self._add_combo_box(label="lossless", options=["yes", "no"], widget_name="lossless", default=1) def init_compression(self): return self._add_combo_box( - "compression level", - ["0", "1", "2", "3", "4", "5", "6"], - "compression", + label="compression level", + options=["0", "1", "2", "3", "4", "5", "6"], + widget_name="compression", tooltip="For lossy, this is a quality/speed tradeoff.\nFor lossless, this is a size/speed tradeoff.", default=4, ) def init_preset(self): return self._add_combo_box( - "preset", ["none", "default", "picture", "photo", "drawing", "icon", "text"], "preset", default=1 + label="preset", + options=["none", "default", "picture", "photo", "drawing", "icon", "text"], + widget_name="preset", + default=1, ) def init_modes(self): diff --git a/fastflix/models/config.py b/fastflix/models/config.py index a11aa8c1..2d648de5 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -22,6 +22,7 @@ x264Settings, x265Settings, NVEncCSettings, + setting_types, ) from fastflix.version import __version__ @@ -32,19 +33,6 @@ NO_OPT = object() -setting_types = { - "x265": x265Settings, - "x264": x264Settings, - "rav1e": rav1eSettings, - "svt_av1": SVTAV1Settings, - "vp9": VP9Settings, - "aom_av1": AOMAV1Settings, - "gif": GIFSettings, - "webp": WebPSettings, - "copy_settings": CopySettings, - "ffmpeg_hevc_nvenc": FFmpegNVENCSettings, - "nvencc_hevc": NVEncCSettings, -} outdated_settings = ("copy",) diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index b78805f0..cbb9a713 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -104,7 +104,7 @@ class NVEncCSettings(EncoderSettings): level: Optional[str] = None hdr10plus_metadata: str = "" multipass: str = "2pass-full" - mv_precision: str = "auto" + mv_precision: str = "Auto" init_q: Optional[str] = None min_q: Optional[str] = None max_q: Optional[str] = None @@ -179,3 +179,18 @@ class GIFSettings(EncoderSettings): class CopySettings(EncoderSettings): name = "Copy" + + +setting_types = { + "x265": x265Settings, + "x264": x264Settings, + "rav1e": rav1eSettings, + "svt_av1": SVTAV1Settings, + "vp9": VP9Settings, + "aom_av1": AOMAV1Settings, + "gif": GIFSettings, + "webp": WebPSettings, + "copy_settings": CopySettings, + "ffmpeg_hevc_nvenc": FFmpegNVENCSettings, + "nvencc_hevc": NVEncCSettings, +} diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index 722095b9..d457a3d6 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -6,62 +6,44 @@ from box import Box from pydantic import BaseModel, Field -from fastflix.models.video import Video - - -class STATUS: - CONVERTED = "converted" - CANCELLED = "cancelled" - ERRORED = "errored" - IN_PROGRESS = "in_progress" - READY = "ready" - - -class REQUEST: - CANCEL = "cancel" - PAUSE_ENCODE = "pause_encode" - RESUME_ENCODE = "resume_encode" - PAUSE_QUEUE = "pause_queue" - RESUME_QUEUE = "resume_queue" - - -# class QueueItem(BaseModel): -# video_uuid: str -# command_uuid: str -# command: str -# work_dir: str -# filename: str -# status: Union[STATUS.CONVERTED, STATUS.CANCELLED, STATUS.IN_PROGRESS, STATUS.ERRORED, STATUS.READY] - - -class Queue(BaseModel): - # request: Optional[ - # Union[REQUEST.CANCEL, REQUEST.PAUSE_QUEUE, REQUEST.RESUME_QUEUE, REQUEST.PAUSE_ENCODE, REQUEST.RESUME_ENCODE] - # ] = None - queue: List[Video] = Field(default_factory=list) - after_done_command: Optional[str] = None +from fastflix.models.video import Video, VideoSettings, AudioTrack, SubtitleTrack, AttachmentTrack +from fastflix.models.encode import setting_types def get_queue(queue_file): loaded = Box.from_yaml(filename=queue_file) - for video in loaded["video"]: + queue = [] + for video in loaded["queue"]: video["source"] = Path(video["source"]) video["work_path"] = Path(video["work_path"]) video["video_settings"]["output_path"] = Path(video["video_settings"]["output_path"]) - - queue = Queue(after_done_command=loaded.after_done_command) - queue.queue = [Video(**video) for video in loaded["video"]] - - # queue.request = [QueueItem(**item) for item in loaded.queue] + encoder_settings = video["video_settings"]["video_encoder_settings"] + ves = [x(**encoder_settings) for x in setting_types.values() if x().name == encoder_settings["name"]][0] + audio = [AudioTrack(**x) for x in video["video_settings"]["audio_tracks"]] + subtitles = [SubtitleTrack(**x) for x in video["video_settings"]["subtitle_tracks"]] + attachments = [AttachmentTrack(**x) for x in video["video_settings"]["attachment_tracks"]] + del video["video_settings"]["audio_tracks"] + del video["video_settings"]["subtitle_tracks"] + del video["video_settings"]["attachment_tracks"] + del video["video_settings"]["video_encoder_settings"] + vs = VideoSettings( + **video["video_settings"], + audio_tracks=audio, + subtitle_tracks=subtitles, + attachment_tracks=attachments, + video_encoder_settings=ves, + ) + del video["video_settings"] + queue.append(Video(**video, video_settings=vs)) return queue -def save_queue(queue, queue_file): - queue.revision += 1 - dict_queue = queue.dict() - for video in dict_queue["video"]: +def save_queue(queue: List[Video], queue_file): + items = [] + for video in queue: + video = video.dict() video["source"] = os.fspath(video["source"]) video["work_path"] = os.fspath(video["work_path"]) video["video_settings"]["output_path"] = os.fspath(video["video_settings"]["output_path"]) - - Box().to_yaml(filename=queue_file) + items.append(video) + Box(queue=items).to_yaml(filename=queue_file) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 6324ba19..b148eb42 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1477,6 +1477,7 @@ def set_convert_button(self, convert=True): @reusables.log_exception("fastflix", show_traceback=False) def encode_video(self): + # from fastflix.models.queue import save_queue if self.converting: logger.debug(t("Canceling current encode")) @@ -1493,7 +1494,7 @@ def encode_video(self): if add_current: if not self.add_to_queue(): return - + # save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) # Command looks like (video_uuid, command_uuid, command, work_dir, filename) # Request looks like (queue command, log_dir, (commands)) requests = ["add_items", str(self.app.fastflix.log_path)] @@ -1560,6 +1561,8 @@ def add_to_queue(self): self.app.fastflix.worker_queue.put(tuple(requests)) self.clear_current_video() + # from fastflix.models.queue import save_queue + # save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) return True @reusables.log_exception("fastflix", show_traceback=False) diff --git a/pyproject.toml b/pyproject.toml index 00d21ab6..9414fb76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,36 +1,3 @@ -[tool.poetry] -name = "fastflix" -version = "4.0.0" -description = "Easy to use video encoder GUI" -authors = ["Chris Griffith "] -license = "MIT" -include = ["fastflix/data/rotations/*.png", "fastflix/data/encoders/*.png", "fastflix/data/icon.ico", "fastflix/CHANGES"] -readme = "README.md" - -[tool.poetry.dependencies] -python = ">=3.6,<3.10" -appdirs = "^1.4.4" -qtpy = "^1.9.0" -python-box = {version = "^5.1.1", extras = ["all"]} -requests = "^2.24.0" -reusables = "^0.9.5" -"ruamel.yaml" = "^0.16.10" -mistune = "^0.8.4" -coloredlogs = "^14.0" -psutil = "^5.7.2" -PySide2 = "^5.15.1" -colorama = "^0.4.4" -iso639-lang = "^0.0.8" - -[tool.poetry.dev-dependencies] - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" - -[tool.poetry.scripts] -fastflix = 'fastflix.__main__:start_fastflix' - [tool.black] line-length = 120 target-version = ['py36', 'py37', 'py38'] diff --git a/tests/test_queue.py b/tests/test_queue.py new file mode 100644 index 00000000..35eaaeb0 --- /dev/null +++ b/tests/test_queue.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from pathlib import Path + +from box import Box + +from fastflix.models.queue import get_queue + +here = Path(__file__).parent + + +def test_queue_load(): + get_queue(here / "media" / "queue.yaml") diff --git a/tests/test_version_check.py b/tests/test_version_check.py index 825b863f..0ad54442 100644 --- a/tests/test_version_check.py +++ b/tests/test_version_check.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +from subprocess import run, PIPE import re from distutils.version import StrictVersion @@ -11,10 +11,6 @@ def test_version(): with open("fastflix/version.py") as version_file: code_version = StrictVersion(re.search(r"__version__ *= *['\"](.+)['\"]", version_file.read()).group(1)) - pyproject_version = StrictVersion(Box.from_toml(filename="pyproject.toml").tool.poetry.version) - - assert code_version == pyproject_version, f"Code Version {code_version} vs PyProject Version {pyproject_version}" - url = "https://api.github.com/repos/cdgriffith/FastFlix/releases/latest" data = requests.get(url).json() assert ( From 868ede46de8a636819a2556002ee33777704060b Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 30 Jan 2021 23:03:00 -0600 Subject: [PATCH 23/50] some fixes, some queue stuff --- fastflix/conversion_worker.py | 29 ++++++++++- .../encoders/nvencc_hevc/settings_panel.py | 48 +++++++++++-------- fastflix/flix.py | 6 ++- fastflix/models/queue.py | 26 +++++++--- fastflix/models/video.py | 6 ++- 5 files changed, 83 insertions(+), 32 deletions(-) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 1214dd4e..af594067 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -5,13 +5,12 @@ import reusables from appdirs import user_data_dir -from filelock import FileLock from box import Box from fastflix.command_runner import BackgroundRunner from fastflix.language import t from fastflix.shared import file_date -from fastflix.models.queue import STATUS, REQUEST, Queue +from fastflix.models.queue import get_queue, save_queue logger = logging.getLogger("fastflix-core") @@ -51,6 +50,32 @@ def allow_sleep_mode(): logger.debug("System has been allowed to enter sleep mode again") +def get_next_command_to_run(): + queue = get_queue() + for video in queue: + if video.status.ready: + return video + + +def set_status(current_video, completed=True, success=False, cancelled=False, errored=False): + queue = get_queue() + for video in queue: + if video.uuid == current_video.uuid: + if completed: + video.status.complete = True + if cancelled: + video.status.cancelled = True + if errored: + video.status.error = True + if success: + video.status.success = True + break + else: + logger.error(f"Could not find video in queue to update status of!\n {current_video}") + return + save_queue(queue) + + @reusables.log_exception(log="fastflix-core") def queue_worker(gui_proc, worker_queue, status_queue, log_queue): runner = BackgroundRunner(log_queue=log_queue) diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index bab30693..b0044364 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -15,7 +15,6 @@ logger = logging.getLogger("fastflix") - presets = ["default", "performance", "quality"] recommended_bitrates = [ @@ -267,25 +266,43 @@ def _qp_range(): def init_min_q(self): layout = QtWidgets.QHBoxLayout() layout.addWidget(QtWidgets.QLabel(t("Min Q"))) - layout.addWidget(self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=45)) - layout.addWidget(self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=45)) - layout.addWidget(self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=45)) + layout.addWidget( + self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=45, opt="min_q_i") + ) + layout.addWidget( + self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=45, opt="min_q_p") + ) + layout.addWidget( + self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=45, opt="min_q_b") + ) return layout def init_init_q(self): layout = QtWidgets.QHBoxLayout() layout.addWidget(QtWidgets.QLabel(t("Init Q"))) - layout.addWidget(self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=45)) - layout.addWidget(self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=45)) - layout.addWidget(self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=45)) + layout.addWidget( + self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=45, opt="init_q_i") + ) + layout.addWidget( + self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=45, opt="init_q_p") + ) + layout.addWidget( + self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=45, opt="init_q_b") + ) return layout def init_max_q(self): layout = QtWidgets.QHBoxLayout() layout.addWidget(QtWidgets.QLabel(t("Max Q"))) - layout.addWidget(self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=45)) - layout.addWidget(self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=45)) - layout.addWidget(self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=45)) + layout.addWidget( + self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=45, opt="max_q_i") + ) + layout.addWidget( + self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=45, opt="max_q_p") + ) + layout.addWidget( + self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=45, opt="max_q_b") + ) return layout def init_vbr_target(self): @@ -352,9 +369,6 @@ def init_dhdr10_warning_and_opt(self): def init_modes(self): layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="cqp") - # self.qp_radio.setChecked(False) - # self.bitrate_radio.setChecked(True) - # self.qp_radio.setDisabled(True) return layout def mode_update(self): @@ -393,21 +407,13 @@ def update_video_encoder_settings(self): init_q=self.gather_q("init"), min_q=self.gather_q("min"), max_q=self.gather_q("max"), - # pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(), extra=self.ffmpeg_extras, - # tune=tune.split("-")[0].strip(), - # extra_both_passes=self.widgets.extra_both_passes.isChecked(), - # rc=self.widgets.rc.currentText() if self.widgets.rc.currentIndex() != 0 else None, - # spatial_aq=self.widgets.spatial_aq.currentIndex(), - # rc_lookahead=int(self.widgets.rc_lookahead.text() or 0), metrics=self.widgets.metrics.isChecked(), level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, b_frames=self.widgets.b_frames.currentText() if self.widgets.b_frames.currentIndex() != 0 else None, ref=self.widgets.ref.currentText() if self.widgets.ref.currentIndex() != 0 else None, vbr_target=self.widgets.vbr_target.currentText() if self.widgets.vbr_target.currentIndex() > 0 else None, b_ref_mode=self.widgets.b_ref_mode.currentText(), - # gpu=int(self.widgets.gpu.currentText() or -1) if self.widgets.gpu.currentIndex() != 0 else -1, - # b_ref_mode=self.widgets.b_ref_mode.currentText(), ) encode_type, q_value = self.get_mode_settings() settings.cqp = q_value if encode_type == "qp" else None diff --git a/fastflix/flix.py b/fastflix/flix.py index af25d392..3a8fce70 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -446,13 +446,15 @@ def parse_hdr_details(app: FastFlixApp, **_): def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): + # TODO run this on video stream change if ( not app.fastflix.current_video.master_display or not config.hdr10plus_parser or not config.hdr10plus_parser.exists() ): - return + return + logger.debug("checking for hdr10+") process = Popen( [ config.ffmpeg, @@ -460,7 +462,7 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): "-i", unixy(app.fastflix.current_video.source), "-map", - f"0:v", + f"0:v:0", "-loglevel", "panic", "-c:v", diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index d457a3d6..2c7249ea 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -5,13 +5,21 @@ from box import Box from pydantic import BaseModel, Field +from filelock import FileLock +from appdirs import user_data_dir -from fastflix.models.video import Video, VideoSettings, AudioTrack, SubtitleTrack, AttachmentTrack +from fastflix.models.video import Video, VideoSettings, Status, Crop +from fastflix.models.encode import AudioTrack, SubtitleTrack, AttachmentTrack from fastflix.models.encode import setting_types +queue_file = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml" +lock_file = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.lock" + + +def get_queue() -> List[Video]: + with FileLock(lock_file): + loaded = Box.from_yaml(filename=queue_file) -def get_queue(queue_file): - loaded = Box.from_yaml(filename=queue_file) queue = [] for video in loaded["queue"]: video["source"] = Path(video["source"]) @@ -22,23 +30,28 @@ def get_queue(queue_file): audio = [AudioTrack(**x) for x in video["video_settings"]["audio_tracks"]] subtitles = [SubtitleTrack(**x) for x in video["video_settings"]["subtitle_tracks"]] attachments = [AttachmentTrack(**x) for x in video["video_settings"]["attachment_tracks"]] + status = Status(**video["status"]) + crop = Crop(**video["crop"]) del video["video_settings"]["audio_tracks"] del video["video_settings"]["subtitle_tracks"] del video["video_settings"]["attachment_tracks"] del video["video_settings"]["video_encoder_settings"] + del video["status"] + del video["crop"] vs = VideoSettings( **video["video_settings"], audio_tracks=audio, subtitle_tracks=subtitles, attachment_tracks=attachments, video_encoder_settings=ves, + crop=crop, ) del video["video_settings"] - queue.append(Video(**video, video_settings=vs)) + queue.append(Video(**video, video_settings=vs, status=status)) return queue -def save_queue(queue: List[Video], queue_file): +def save_queue(queue: List[Video]): items = [] for video in queue: video = video.dict() @@ -46,4 +59,5 @@ def save_queue(queue: List[Video], queue_file): video["work_path"] = os.fspath(video["work_path"]) video["video_settings"]["output_path"] = os.fspath(video["video_settings"]["output_path"]) items.append(video) - Box(queue=items).to_yaml(filename=queue_file) + with FileLock(lock_file): + Box(queue=items).to_yaml(filename=queue_file) diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 8d05a385..775a5da6 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -23,7 +23,7 @@ NVEncCSettings, ) -__all__ = ["VideoSettings", "Status", "Video"] +__all__ = ["VideoSettings", "Status", "Video", "Crop", "Status"] class Crop(BaseModel): @@ -91,6 +91,10 @@ class Status(BaseModel): cancelled: bool = False current_command: int = 0 + @property + def ready(self) -> bool: + return not self.success and not self.error and not self.complete and not self.running and not self.cancelled + class Video(BaseModel): source: Path From 3ff65e53ebb0002b4e99c4f26f4e7965a0520a42 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 30 Jan 2021 23:45:06 -0600 Subject: [PATCH 24/50] fixing hdr10 extraction with multiple video tracks (does not work for videos with multiple hdr10+) --- fastflix/data/languages.yaml | 2 + fastflix/encoders/hevc_x265/settings_panel.py | 2 +- .../encoders/nvencc_hevc/settings_panel.py | 2 +- fastflix/flix.py | 77 ++++++++++--------- fastflix/models/video.py | 2 +- fastflix/widgets/background_tasks.py | 4 +- 6 files changed, 46 insertions(+), 43 deletions(-) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index cd61ca7e..fefc21b8 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2829,3 +2829,5 @@ Metrics: eng: Metrics Calculate PSNR and SSIM and show in the encoder output: eng: Calculate PSNR and SSIM and show in the encoder output +Extracting HDR10+ metadata: + eng: Extracting HDR10+ metadata diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index dce3ee9f..38b303ea 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -517,7 +517,7 @@ def hdr_opts(): def new_source(self): super().new_source() self.setting_change() - if self.app.fastflix.current_video.hdr10_plus: + if self.app.fastflix.current_video.hdr10_plus is not None: self.extract_button.show() else: self.extract_button.hide() diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index b0044364..440f8b34 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -430,7 +430,7 @@ def set_mode(self, x): def new_source(self): super().new_source() - if self.app.fastflix.current_video.hdr10_plus: + if self.app.fastflix.current_video.hdr10_plus is not None: self.extract_button.show() else: self.extract_button.hide() diff --git a/fastflix/flix.py b/fastflix/flix.py index 3a8fce70..b7354475 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -446,7 +446,6 @@ def parse_hdr_details(app: FastFlixApp, **_): def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): - # TODO run this on video stream change if ( not app.fastflix.current_video.master_display or not config.hdr10plus_parser @@ -454,42 +453,44 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): ): return - logger.debug("checking for hdr10+") - process = Popen( - [ - config.ffmpeg, - "-y", - "-i", - unixy(app.fastflix.current_video.source), - "-map", - f"0:v:0", - "-loglevel", - "panic", - "-c:v", - "copy", - "-vbsf", - "hevc_mp4toannexb", - "-f", - "hevc", - "-", - ], - stdout=PIPE, - stderr=PIPE, - stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc - ) - process_two = Popen( - [config.hdr10plus_parser, "--verify", "-"], - stdout=PIPE, - stderr=PIPE, - stdin=process.stdout, - encoding="utf-8", - ) + for stream in app.fastflix.current_video.streams.video: + logger.debug(f"Checking for hdr10+ in stream {stream.index}") + process = Popen( + [ + config.ffmpeg, + "-y", + "-i", + unixy(app.fastflix.current_video.source), + "-map", + f"0:{stream.index}", + "-loglevel", + "panic", + "-c:v", + "copy", + "-vbsf", + "hevc_mp4toannexb", + "-f", + "hevc", + "-", + ], + stdout=PIPE, + stderr=PIPE, + stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc + ) - try: - stdout, stderr = process_two.communicate() - except Exception: - logger.exception("Unexpected error while trying to detect HDR10+ metdata") - else: - if "Dynamic HDR10+ metadata detected." in stdout: - app.fastflix.current_video.hdr10_plus = True + process_two = Popen( + [config.hdr10plus_parser, "--verify", "-"], + stdout=PIPE, + stderr=PIPE, + stdin=process.stdout, + encoding="utf-8", + ) + + try: + stdout, stderr = process_two.communicate() + except Exception: + logger.exception(f"Unexpected error while trying to detect HDR10+ metadata in stream {stream.index}") + else: + if "Dynamic HDR10+ metadata detected." in stdout: + app.fastflix.current_video.hdr10_plus = stream.index diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 775a5da6..e737bfa6 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -110,7 +110,7 @@ class Video(BaseModel): # HDR10 Details master_display: Box = None cll: str = "" - hdr10_plus: bool = False + hdr10_plus: Optional[int] = None video_settings: VideoSettings = Field(default_factory=VideoSettings) status: Status = Field(default_factory=Status) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 44082d45..d6b6ef29 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -128,7 +128,7 @@ def run(self): "-i", str(self.app.fastflix.current_video.source).replace("\\", "/"), "-map", - f"0:{self.app.fastflix.current_video.video_settings.selected_track}", + f"0:{self.app.fastflix.current_video.hdr10_plus}", "-loglevel", "panic", "-c:v", @@ -145,7 +145,7 @@ def run(self): ) process_two = Popen( - ["hdr10plus_parser", "-o", str(output).replace("\\", "/"), "-"], + [self.app.fastflix.config.hdr10plus_parser, "-o", str(output).replace("\\", "/"), "-"], stdout=PIPE, stderr=PIPE, stdin=process.stdout, From 1d7877f66a48b2bfc513f7ad836a896b05d0a06c Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Mon, 1 Feb 2021 19:27:19 -0600 Subject: [PATCH 25/50] showing progress for hdr10 extraction. Making sure UPX is not used. Some NVENCC tweaks --- FastFlix_Nix_OneFile.spec | 2 +- FastFlix_Windows_Installer.spec | 2 +- FastFlix_Windows_OneFile.spec | 2 +- fastflix/encoders/common/setting_panel.py | 5 ++- fastflix/encoders/hevc_x265/settings_panel.py | 10 +++-- .../encoders/nvencc_hevc/command_builder.py | 18 +++++++-- .../encoders/nvencc_hevc/settings_panel.py | 40 +++++++++++-------- fastflix/models/encode.py | 12 ++++-- fastflix/widgets/background_tasks.py | 19 ++++++--- fastflix/widgets/panels/audio_panel.py | 2 +- 10 files changed, 76 insertions(+), 36 deletions(-) diff --git a/FastFlix_Nix_OneFile.spec b/FastFlix_Nix_OneFile.spec index 3a748e70..5a2f1886 100644 --- a/FastFlix_Nix_OneFile.spec +++ b/FastFlix_Nix_OneFile.spec @@ -41,7 +41,7 @@ exe = EXE(pyz, debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, + upx=False, upx_exclude=[], runtime_tmpdir=None, console=True , icon='fastflix/data/icon.ico') diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index dd4ae782..e7ea30b1 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -47,6 +47,6 @@ coll = COLLECT(exe, a.zipfiles, a.datas, strip=False, - upx=True, + upx=False, upx_exclude=[], name='FastFlix') diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 88ce8eed..b501fc41 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -41,7 +41,7 @@ exe = EXE(pyz, debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, + upx=False, upx_exclude=[], runtime_tmpdir=None, console=True , icon='fastflix\\data\\icon.ico') diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index 18dfc7ca..6a964130 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -237,7 +237,9 @@ def extract_hdr10plus(self): self.extract_label.show() self.movie.start() # self.extracting_hdr10 = True - self.extract_thrad = ExtractHDR10(self.app, self.main, signal=self.hdr10plus_signal) + self.extract_thrad = ExtractHDR10( + self.app, self.main, signal=self.hdr10plus_signal, ffmpeg_signal=self.hdr10plus_ffmpeg_signal + ) self.extract_thrad.start() def done_hdr10plus_extract(self, metadata: str): @@ -246,6 +248,7 @@ def done_hdr10plus_extract(self, metadata: str): self.movie.stop() if Path(metadata).exists(): self.widgets.hdr10plus_metadata.setText(metadata) + self.ffmpeg_level.setText("") def dhdr10_update(self): dirname = Path(self.widgets.hdr10plus_metadata.text()).parent diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index 38b303ea..38f3b29d 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -65,6 +65,7 @@ def get_breaker(): class HEVC(SettingPanel): profile_name = "x265" hdr10plus_signal = QtCore.Signal(str) + hdr10plus_ffmpeg_signal = QtCore.Signal(str) def __init__(self, parent, main, app: FastFlixApp): super().__init__(parent, main, app) @@ -106,10 +107,12 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_dhdr10_info(), 9, 2, 1, 3) grid.addLayout(self.init_dhdr10_warning_and_opt(), 9, 5, 1, 1) + self.ffmpeg_level = QtWidgets.QLabel() + grid.addWidget(self.ffmpeg_level, 10, 2, 1, 4) - grid.setRowStretch(10, True) + grid.setRowStretch(11, True) - grid.addLayout(self._add_custom(), 11, 0, 1, 6) + grid.addLayout(self._add_custom(), 12, 0, 1, 6) link_1 = link( "https://trac.ffmpeg.org/wiki/Encode/H.265", @@ -128,9 +131,10 @@ def __init__(self, parent, main, app: FastFlixApp): guide_label.setAlignment(QtCore.Qt.AlignBottom) guide_label.setOpenExternalLinks(True) - grid.addWidget(guide_label, 12, 0, 1, 6) + grid.addWidget(guide_label, 13, 0, 1, 6) self.hdr10plus_signal.connect(self.done_hdr10plus_extract) + self.hdr10plus_ffmpeg_signal.connect(lambda x: self.ffmpeg_level.setText(x)) self.setLayout(grid) self.hide() diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 2f544d5f..7b9d20aa 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -134,6 +134,18 @@ def build(fastflix: FastFlix): if video.video_settings.maxrate: vbv = f"--max-bitrate {video.video_settings.maxrate} --vbv-bufsize {video.video_settings.bufsize}" + init_q = settings.init_q_i + if settings.init_q_i and settings.init_q_p and settings.init_q_b: + init_q = f"{settings.init_q_i}:{settings.init_q_p}:{settings.init_q_b}" + + min_q = settings.min_q_i + if settings.min_q_i and settings.min_q_p and settings.min_q_b: + min_q = f"{settings.min_q_i}:{settings.min_q_p}:{settings.min_q_b}" + + max_q = settings.max_q_i + if settings.max_q_i and settings.max_q_p and settings.max_q_b: + max_q = f"{settings.max_q_i}:{settings.max_q_p}:{settings.max_q_b}" + command = [ f'"{unixy(fastflix.config.nvencc)}"', "-i", @@ -152,9 +164,9 @@ def build(fastflix: FastFlix): (f"--vbr {settings.bitrate.rstrip('k')}" if settings.bitrate else f"--cqp {settings.cqp}"), vbv, (f"--vbr-quality {settings.vbr_target}" if settings.vbr_target is not None else ""), - (f"--qp-init {settings.init_q}" if settings.init_q else ""), - (f"--qp-min {settings.min_q}" if settings.min_q else ""), - (f"--qp-max {settings.max_q}" if settings.max_q else ""), + (f"--qp-init {init_q}" if init_q else ""), + (f"--qp-min {min_q}" if min_q else ""), + (f"--qp-max {max_q}" if max_q else ""), (f"--bframes {settings.b_frames}" if settings.b_frames else ""), (f"--ref {settings.ref}" if settings.ref else ""), f"--bref-mode {settings.b_ref_mode}", diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 440f8b34..d921a3bf 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -55,6 +55,7 @@ class NVENCC(SettingPanel): profile_name = "nvencc_hevc" hdr10plus_signal = QtCore.Signal(str) + hdr10plus_ffmpeg_signal = QtCore.Signal(str) def __init__(self, parent, main, app: FastFlixApp): super().__init__(parent, main, app) @@ -107,8 +108,10 @@ def __init__(self, parent, main, app: FastFlixApp): advanced.addLayout(self.init_metrics()) grid.addLayout(advanced, 5, 2, 1, 4) - grid.addLayout(self.init_dhdr10_info(), 6, 2, 1, 3) - grid.addLayout(self.init_dhdr10_warning_and_opt(), 6, 5, 1, 1) + grid.addLayout(self.init_dhdr10_info(), 6, 2, 1, 4) + + self.ffmpeg_level = QtWidgets.QLabel() + grid.addWidget(self.ffmpeg_level, 7, 2, 1, 4) grid.setRowStretch(9, 1) @@ -128,6 +131,7 @@ def __init__(self, parent, main, app: FastFlixApp): self.setLayout(grid) self.hide() self.hdr10plus_signal.connect(self.done_hdr10plus_extract) + self.hdr10plus_ffmpeg_signal.connect(lambda x: self.ffmpeg_level.setText(x)) def init_preset(self): return self._add_combo_box( @@ -348,11 +352,6 @@ def init_dhdr10_info(self): tooltip="dhdr10_info: Path to HDR10+ JSON metadata file", ) self.labels["hdr10plus_metadata"].setFixedWidth(200) - return layout - - def init_dhdr10_warning_and_opt(self): - layout = QtWidgets.QHBoxLayout() - self.extract_button = QtWidgets.QPushButton(t("Extract HDR10+")) self.extract_button.hide() self.extract_button.clicked.connect(self.extract_hdr10plus) @@ -365,6 +364,7 @@ def init_dhdr10_warning_and_opt(self): layout.addWidget(self.extract_button) layout.addWidget(self.extract_label) + return layout def init_modes(self): @@ -385,13 +385,13 @@ def setting_change(self, update=True): self.main.page_update() self.updating_settings = False - def gather_q(self, group) -> Optional[str]: - if self.mode.lower() != "bitrate": - return None - if self.widgets[f"{group}_q_i"].currentIndex() > 0: - if self.widgets[f"{group}_q_p"].currentIndex() > 0 and self.widgets[f"{group}_q_b"].currentIndex() > 0: - return f'{self.widgets[f"{group}_q_i"].currentText()}:{self.widgets[f"{group}_q_p"].currentText()}:{self.widgets[f"{group}_q_b"].currentText()}' - return self.widgets[f"{group}_q_i"].currentText() + # def gather_q(self, group) -> Optional[str]: + # if self.mode.lower() != "bitrate": + # return None + # if self.widgets[f"{group}_q_i"].currentIndex() > 0: + # if self.widgets[f"{group}_q_p"].currentIndex() > 0 and self.widgets[f"{group}_q_b"].currentIndex() > 0: + # return f'{self.widgets[f"{group}_q_i"].currentText()}:{self.widgets[f"{group}_q_p"].currentText()}:{self.widgets[f"{group}_q_b"].currentText()}' + # return self.widgets[f"{group}_q_i"].currentText() def update_video_encoder_settings(self): @@ -404,9 +404,15 @@ def update_video_encoder_settings(self): hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"), multipass=self.widgets.multipass.currentText(), mv_precision=self.widgets.mv_precision.currentText(), - init_q=self.gather_q("init"), - min_q=self.gather_q("min"), - max_q=self.gather_q("max"), + init_q_i=self.widgets.init_q_i.currentText() if self.widgets.init_q_i.currentIndex() != 0 else None, + init_q_p=self.widgets.init_q_p.currentText() if self.widgets.init_q_p.currentIndex() != 0 else None, + init_q_b=self.widgets.init_q_b.currentText() if self.widgets.init_q_b.currentIndex() != 0 else None, + max_q_i=self.widgets.max_q_i.currentText() if self.widgets.max_q_i.currentIndex() != 0 else None, + max_q_p=self.widgets.max_q_p.currentText() if self.widgets.max_q_p.currentIndex() != 0 else None, + max_q_b=self.widgets.max_q_b.currentText() if self.widgets.max_q_b.currentIndex() != 0 else None, + min_q_i=self.widgets.min_q_i.currentText() if self.widgets.min_q_i.currentIndex() != 0 else None, + min_q_p=self.widgets.min_q_p.currentText() if self.widgets.min_q_p.currentIndex() != 0 else None, + min_q_b=self.widgets.min_q_b.currentText() if self.widgets.min_q_b.currentIndex() != 0 else None, extra=self.ffmpeg_extras, metrics=self.widgets.metrics.isChecked(), level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index cbb9a713..afed3243 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -105,9 +105,15 @@ class NVEncCSettings(EncoderSettings): hdr10plus_metadata: str = "" multipass: str = "2pass-full" mv_precision: str = "Auto" - init_q: Optional[str] = None - min_q: Optional[str] = None - max_q: Optional[str] = None + init_q_i: Optional[str] = None + init_q_p: Optional[str] = None + init_q_b: Optional[str] = None + min_q_i: Optional[str] = None + min_q_p: Optional[str] = None + min_q_b: Optional[str] = None + max_q_i: Optional[str] = None + max_q_p: Optional[str] = None + max_q_b: Optional[str] = None vbr_target: Optional[str] = None b_frames: Optional[str] = None b_ref_mode: str = "disabled" diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index d6b6ef29..8d42893b 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -109,11 +109,12 @@ def run(self): class ExtractHDR10(QtCore.QThread): - def __init__(self, app: FastFlixApp, main, signal): + def __init__(self, app: FastFlixApp, main, signal, ffmpeg_signal): super().__init__(main) self.main = main self.app = app self.signal = signal + self.ffmpeg_signal = ffmpeg_signal def run(self): @@ -121,6 +122,8 @@ def run(self): self.main.thread_logging_signal.emit(f'INFO:{t("Extracting HDR10+ metadata")} to {output}') + self.ffmpeg_signal.emit("Extracting HDR10+ metadata") + process = Popen( [ self.app.fastflix.config.ffmpeg, @@ -129,8 +132,6 @@ def run(self): str(self.app.fastflix.current_video.source).replace("\\", "/"), "-map", f"0:{self.app.fastflix.current_video.hdr10_plus}", - "-loglevel", - "panic", "-c:v", "copy", "-vbsf", @@ -140,8 +141,8 @@ def run(self): "-", ], stdout=PIPE, - stderr=PIPE, - stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc + stderr=open(self.app.fastflix.current_video.work_path / "out.txt", "wb"), + # stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc ) process_two = Popen( @@ -153,6 +154,14 @@ def run(self): cwd=str(self.app.fastflix.current_video.work_path), ) + with open(self.app.fastflix.current_video.work_path / "out.txt", "r", encoding="utf-8") as f: + while True: + if process.poll() is not None or process_two.poll() is not None: + break + if line := f.readline().rstrip(): + if line.startswith("frame"): + self.ffmpeg_signal.emit(line) + stdout, stderr = process_two.communicate() self.main.thread_logging_signal.emit(f"DEBUG: HDR10+ Extract: {stdout}") self.signal.emit(str(output)) diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index 1222c616..f5d5621b 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -164,7 +164,7 @@ def init_conversion(self): self.widgets.convert_bitrate.setFixedWidth(70) self.widgets.convert_bitrate.addItems( - [f"{x}k" for x in range(32 * self.channels, (256 * self.channels) + 1, 32 * self.channels)] + [f"{x}k" for x in range(16 * self.channels, (256 * self.channels) + 1, 16 * self.channels)] if self.channels else [ "32k", From 8909bd45b8596ee01d9c299554e35c4c041feea6 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Mon, 1 Feb 2021 22:22:27 -0600 Subject: [PATCH 26/50] first actual almost working queue as a file --- fastflix/conversion_worker.py | 148 ++++++++++++++++-------- fastflix/flix.py | 7 +- fastflix/models/queue.py | 21 +++- fastflix/models/video.py | 7 +- fastflix/widgets/background_tasks.py | 17 ++- fastflix/widgets/main.py | 116 +++++++++++-------- fastflix/widgets/panels/queue_panel.py | 44 +++---- fastflix/widgets/panels/status_panel.py | 33 +++--- fastflix/widgets/video_options.py | 2 +- 9 files changed, 241 insertions(+), 154 deletions(-) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index af594067..9b6f7235 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -2,6 +2,7 @@ import logging from pathlib import Path from queue import Empty +from typing import Optional import reusables from appdirs import user_data_dir @@ -11,6 +12,7 @@ from fastflix.language import t from fastflix.shared import file_date from fastflix.models.queue import get_queue, save_queue +from fastflix.models.video import Video logger = logging.getLogger("fastflix-core") @@ -50,25 +52,50 @@ def allow_sleep_mode(): logger.debug("System has been allowed to enter sleep mode again") -def get_next_command_to_run(): +def get_next_video() -> Optional[Video]: queue = get_queue() for video in queue: - if video.status.ready: + if ( + not video.status.complete + and not video.status.success + and not video.status.cancelled + and not video.status.error + and not video.status.running + ): return video -def set_status(current_video, completed=True, success=False, cancelled=False, errored=False): +def set_status( + current_video, + completed=None, + success=None, + cancelled=None, + errored=None, + running=None, + next_command=False, + reset_commands=False, +): queue = get_queue() for video in queue: if video.uuid == current_video.uuid: - if completed: - video.status.complete = True - if cancelled: - video.status.cancelled = True - if errored: - video.status.error = True - if success: - video.status.success = True + if completed is not None: + video.status.complete = completed + if cancelled is not None: + video.status.cancelled = cancelled + if errored is not None: + video.status.error = errored + if success is not None: + video.status.success = success + if running is not None: + video.status.running = running + + if completed or cancelled or errored or success: + video.status.running = False + + if next_command: + video.status.current_command += 1 + if reset_commands: + video.status.current_command = 0 break else: logger.error(f"Could not find video in queue to update status of!\n {current_video}") @@ -82,17 +109,23 @@ def queue_worker(gui_proc, worker_queue, status_queue, log_queue): # Command looks like (video_uuid, command_uuid, command, work_dir) after_done_command = "" - commands_to_run = [] gui_died = False currently_encoding = False paused = False + video: Optional[Video] = None + + def current_command(): + nonlocal video def start_command(): nonlocal currently_encoding - log_queue.put(f"CLEAR_WINDOW:{commands_to_run[0][0]}:{commands_to_run[0][1]}") + log_queue.put( + f"CLEAR_WINDOW:{video.uuid}:{video.video_settings.conversion_commands[video.status.current_command]['uuid']}" + ) reusables.remove_file_handlers(logger) new_file_handler = reusables.get_file_handler( - log_path / f"flix_conversion_{commands_to_run[0][4]}_{file_date()}.log", + log_path + / f"flix_conversion_{video.video_settings.video_title or video.video_settings.output_path.stem}_{file_date()}.log", level=logging.DEBUG, log_format="%(asctime)s - %(message)s", encoding="utf-8", @@ -101,11 +134,13 @@ def start_command(): prevent_sleep_mode() currently_encoding = True runner.start_exec( - commands_to_run[0][2], - work_dir=commands_to_run[0][3], + video.video_settings.conversion_commands[video.status.current_command]["command"], + work_dir=str(video.work_path), ) + set_status(video, running=True) + status_queue.put(("queue",)) - status_queue.put(("running", commands_to_run[0][0], commands_to_run[0][1], runner.started_at.isoformat())) + # status_queue.put(("running", commands_to_run[0][0], commands_to_run[0][1], runner.started_at.isoformat())) while True: if currently_encoding and not runner.is_alive(): @@ -115,7 +150,8 @@ def start_command(): # Stop working! currently_encoding = False - status_queue.put(("error", commands_to_run[0][0], commands_to_run[0][1])) + set_status(video, errored=True) + status_queue.put(("error",)) commands_to_run = [] allow_sleep_mode() if gui_died: @@ -124,25 +160,32 @@ def start_command(): # Successfully encoded, do next one if it exists # First check if the current video has more commands - logger.info(t("Command has completed")) - status_queue.put(("converted", commands_to_run[0][0], commands_to_run[0][1])) - commands_to_run.pop(0) - if commands_to_run: - if not paused: - logger.info(t("starting next command")) - start_command() - else: - currently_encoding = False - allow_sleep_mode() - logger.debug(t("Queue has been paused")) + video.status.current_command += 1 + + if len(video.video_settings.conversion_commands) > video.status.current_command: + logger.debug("About to run next command for this video") + set_status(video, next_command=True) + status_queue.put(("queue",)) + start_command() continue else: - logger.info(t("all conversions complete")) - # Finished the queue - # fastflix.current_encoding = None + set_status(video, next_command=True, completed=True) + status_queue.put(("queue",)) + video = None + + if paused: currently_encoding = False - status_queue.put(("complete",)) allow_sleep_mode() + logger.debug(t("Queue has been paused")) + continue + + if video := get_next_video(): + start_command() + continue + else: + currently_encoding = False + allow_sleep_mode() + logger.info(t("all conversions complete")) if after_done_command: logger.info(f"{t('Running after done command:')} {after_done_command}") try: @@ -175,36 +218,47 @@ def start_command(): # Request looks like (queue command, log_dir, (commands)) log_path = Path(request[1]) - for command in request[2]: - if command not in commands_to_run: - logger.debug(t(f"Adding command to the queue for {command[4]} - {command[2]}")) - commands_to_run.append(command) - # else: - # logger.debug(t(f"Command already in queue: {command[1]}")) - if not runner.is_alive() and not paused: - logger.debug(t("No encoding is currently in process, starting encode")) - start_command() + if not currently_encoding and not paused: + video = get_next_video() + if video: + start_command() + + # for command in request[2]: + # if command not in commands_to_run: + # logger.debug(t(f"Adding command to the queue for {command[4]} - {command[2]}")) + # commands_to_run.append(command) + # # else: + # # logger.debug(t(f"Command already in queue: {command[1]}")) + # if not runner.is_alive() and not paused: + # logger.debug(t("No encoding is currently in process, starting encode")) + # start_command() if request[0] == "cancel": logger.debug(t("Cancel has been requested, killing encoding")) runner.kill() + set_status(video, reset_commands=True, cancelled=True) currently_encoding = False allow_sleep_mode() - status_queue.put(("cancelled", commands_to_run[0][0], commands_to_run[0][1])) - commands_to_run = [] + status_queue.put(("cancelled", video.uuid)) + if request[0] == "pause queue": logger.debug(t("Command worker received request to pause encoding after the current item completes")) paused = True + if request[0] == "resume queue": paused = False logger.debug(t("Command worker received request to resume encoding")) - if commands_to_run and not runner.is_alive(): + if not currently_encoding: + if not video: + video = get_next_video() start_command() + if request[0] == "set after done": after_done_command = request[1] if after_done_command: logger.debug(f'{t("Setting after done command to:")} {after_done_command}') else: logger.debug(t("Removing after done command")) + if request[0] == "pause encode": logger.debug(t("Command worker received request to pause current encode")) try: @@ -212,7 +266,7 @@ def start_command(): except Exception: logger.exception("Could not pause command") else: - status_queue.put(("paused encode", commands_to_run[0][0], commands_to_run[0][1])) + status_queue.put(("paused encode",)) if request[0] == "resume encode": logger.debug(t("Command worker received request to resume paused encode")) try: @@ -220,4 +274,4 @@ def start_command(): except Exception: logger.exception("Could not resume command") else: - status_queue.put(("resumed encode", commands_to_run[0][0], commands_to_run[0][1])) + status_queue.put(("resumed encode",)) diff --git a/fastflix/flix.py b/fastflix/flix.py index b7354475..f564c8b4 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -454,6 +454,8 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): return + hdr10plus_streams = [] + for stream in app.fastflix.current_video.streams.video: logger.debug(f"Checking for hdr10+ in stream {stream.index}") process = Popen( @@ -493,4 +495,7 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): logger.exception(f"Unexpected error while trying to detect HDR10+ metadata in stream {stream.index}") else: if "Dynamic HDR10+ metadata detected." in stdout: - app.fastflix.current_video.hdr10_plus = stream.index + hdr10plus_streams.append(stream.index) + + if hdr10plus_streams: + app.fastflix.current_video.hdr10_plus = hdr10plus_streams diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index 2c7249ea..f0612cdd 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -16,9 +16,13 @@ lock_file = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.lock" -def get_queue() -> List[Video]: - with FileLock(lock_file): +def get_queue(lockless=False) -> List[Video]: + + if lockless: loaded = Box.from_yaml(filename=queue_file) + else: + with FileLock(lock_file): + loaded = Box.from_yaml(filename=queue_file) queue = [] for video in loaded["queue"]: @@ -31,13 +35,15 @@ def get_queue() -> List[Video]: subtitles = [SubtitleTrack(**x) for x in video["video_settings"]["subtitle_tracks"]] attachments = [AttachmentTrack(**x) for x in video["video_settings"]["attachment_tracks"]] status = Status(**video["status"]) - crop = Crop(**video["crop"]) + crop = None + if video["video_settings"]["crop"]: + crop = Crop(**video["video_settings"]["crop"]) del video["video_settings"]["audio_tracks"] del video["video_settings"]["subtitle_tracks"] del video["video_settings"]["attachment_tracks"] del video["video_settings"]["video_encoder_settings"] del video["status"] - del video["crop"] + del video["video_settings"]["crop"] vs = VideoSettings( **video["video_settings"], audio_tracks=audio, @@ -51,7 +57,7 @@ def get_queue() -> List[Video]: return queue -def save_queue(queue: List[Video]): +def save_queue(queue: List[Video], lockless=False): items = [] for video in queue: video = video.dict() @@ -59,5 +65,8 @@ def save_queue(queue: List[Video]): video["work_path"] = os.fspath(video["work_path"]) video["video_settings"]["output_path"] = os.fspath(video["video_settings"]["output_path"]) items.append(video) - with FileLock(lock_file): + if lockless: Box(queue=items).to_yaml(filename=queue_file) + else: + with FileLock(lock_file): + Box(queue=items).to_yaml(filename=queue_file) diff --git a/fastflix/models/video.py b/fastflix/models/video.py index e737bfa6..071c4b5c 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -89,6 +89,7 @@ class Status(BaseModel): complete: bool = False running: bool = False cancelled: bool = False + subtitle_fixed: bool = False current_command: int = 0 @property @@ -108,9 +109,9 @@ class Video(BaseModel): interlaced: bool = True # HDR10 Details - master_display: Box = None - cll: str = "" - hdr10_plus: Optional[int] = None + master_display: Optional[Box] = None + cll: Optional[str] = None + hdr10_plus: Optional[List[int]] = None video_settings: VideoSettings = Field(default_factory=VideoSettings) status: Status = Field(default_factory=Status) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 8d42893b..68a93c3e 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -117,9 +117,20 @@ def __init__(self, app: FastFlixApp, main, signal, ffmpeg_signal): self.ffmpeg_signal = ffmpeg_signal def run(self): + if not self.app.fastflix.current_video.hdr10_plus: + self.main.thread_logging_signal.emit("ERROR:No tracks have HDR10+ data to extract") + return output = self.app.fastflix.current_video.work_path / "metadata.json" + if ( + self.app.fastflix.current_video.video_settings.selected_track + not in self.app.fastflix.current_video.hdr10_plus + ): + self.main.thread_logging_signal.emit( + "WARNING:Selected video track not detected to have HDR10+ data, trying anyways" + ) + self.main.thread_logging_signal.emit(f'INFO:{t("Extracting HDR10+ metadata")} to {output}') self.ffmpeg_signal.emit("Extracting HDR10+ metadata") @@ -131,7 +142,7 @@ def run(self): "-i", str(self.app.fastflix.current_video.source).replace("\\", "/"), "-map", - f"0:{self.app.fastflix.current_video.hdr10_plus}", + f"0:{self.app.fastflix.current_video.video_settings.selected_track}", "-c:v", "copy", "-vbsf", @@ -141,7 +152,7 @@ def run(self): "-", ], stdout=PIPE, - stderr=open(self.app.fastflix.current_video.work_path / "out.txt", "wb"), + stderr=open(self.app.fastflix.current_video.work_path / "hdr10extract_out.txt", "wb"), # stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc ) @@ -154,7 +165,7 @@ def run(self): cwd=str(self.app.fastflix.current_video.work_path), ) - with open(self.app.fastflix.current_video.work_path / "out.txt", "r", encoding="utf-8") as f: + with open(self.app.fastflix.current_video.work_path / "hdr10extract_out.txt", "r", encoding="utf-8") as f: while True: if process.poll() is not None or process_two.poll() is not None: break diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index b148eb42..8137684b 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -32,6 +32,7 @@ from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.video import Status, Video, VideoSettings, Crop +from fastflix.models.queue import get_queue from fastflix.resources import ( black_x_icon, folder_icon, @@ -114,7 +115,7 @@ class Main(QtWidgets.QWidget): thumbnail_complete = QtCore.Signal(int) cancelled = QtCore.Signal(str) close_event = QtCore.Signal() - status_update_signal = QtCore.Signal(str) + status_update_signal = QtCore.Signal() thread_logging_signal = QtCore.Signal(str) def __init__(self, parent, app: FastFlixApp): @@ -1552,7 +1553,7 @@ def add_to_queue(self): video = self.app.fastflix.current_video self.app.fastflix.queue.append(copy.deepcopy(video)) - self.video_options.update_queue(currently_encoding=self.converting) + self.video_options.update_queue() self.video_options.show_queue() if self.converting: @@ -1571,6 +1572,8 @@ def conversion_complete(self, return_code): self.paused = False self.set_convert_button() + self.video_options.update_queue() + if return_code: error_message(t("There was an error during conversion and the queue has stopped"), title=t("Error")) else: @@ -1589,12 +1592,16 @@ def conversion_cancelled(self, data): self.paused = False self.set_convert_button() + self.app.fastflix.queue = get_queue() + try: - video_uuid, command_uuid, *_ = data.split("|") + video_uuid, *_ = data.split("|") cancelled_video = self.find_video(video_uuid) except Exception: return + self.video_options.update_queue() + if self.video_options.queue.paused: self.video_options.queue.pause_resume_queue() @@ -1647,53 +1654,64 @@ def dragEnterEvent(self, event): def dragMoveEvent(self, event): event.accept() if event.mimeData().hasUrls else event.ignoreAF() - def status_update(self, status): - logger.debug(f"Updating status from command worker: {status}") - try: - command, video_uuid, command_uuid, *_ = status.split("|") - except ValueError: - logger.exception(f"Could not process status update from the command worker: {status}") - return - - try: - video = self.find_video(video_uuid) - command_index = self.find_command(video, command_uuid) - except FlixError as err: - logger.error(f"Could not update queue status due to not found video/command - {err}") - return + def status_update(self): + logger.debug(f"Updating queue from command worker") - if command == "converted": - if command_index == len(video.video_settings.conversion_commands): - video.status.complete = True - video.status.success = True - video.status.running = False + self.app.fastflix.queue = get_queue() + for video in self.app.fastflix.queue: + if video.status.complete and not video.status.subtitle_fixed: if video.video_settings.subtitle_tracks and not video.video_settings.subtitle_tracks[0].disposition: if mkv_prop_edit := shutil.which("mkvpropedit"): worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) worker.start() - self.video_options.update_queue(currently_encoding=self.converting) - else: - logger.error(f"This should not happen? {status} - {video}") - - elif command == "running": - video.status.current_command = command_index - video.status.running = True - self.video_options.update_queue(currently_encoding=self.converting) - - elif command == "error": - video.status.error = True - video.status.running = False - self.video_options.update_queue(currently_encoding=self.converting) - elif command == "cancelled": - video.status.cancelled = True - video.status.running = False - self.video_options.update_queue(currently_encoding=self.converting) - - elif command in ("paused encode", "resumed encode"): - pass - else: - logger.warning(f"status worker received unknown command: {command}") + self.video_options.update_queue() + + # try: + # command, video_uuid, command_uuid, *_ = status.split("|") + # except ValueError: + # logger.exception(f"Could not process status update from the command worker: {status}") + # return + # # + # try: + # video = self.find_video(video_uuid) + # command_index = self.find_command(video, command_uuid) + # except FlixError as err: + # logger.error(f"Could not update queue status due to not found video/command - {err}") + # return + # + # if command == "converted": + # if command_index == len(video.video_settings.conversion_commands): + # video.status.complete = True + # video.status.success = True + # video.status.running = False + # if video.video_settings.subtitle_tracks and not video.video_settings.subtitle_tracks[0].disposition: + # if mkv_prop_edit := shutil.which("mkvpropedit"): + # worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) + # worker.start() + # self.video_options.update_queue(currently_encoding=self.converting) + # else: + # logger.error(f"This should not happen? {status} - {video}") + # + # elif command == "running": + # video.status.current_command = command_index + # video.status.running = True + # self.video_options.update_queue(currently_encoding=self.converting) + # + # elif command == "error": + # video.status.error = True + # video.status.running = False + # self.video_options.update_queue(currently_encoding=self.converting) + # + # elif command == "cancelled": + # video.status.cancelled = True + # video.status.running = False + # self.video_options.update_queue(currently_encoding=self.converting) + # + # elif command in ("paused encode", "resumed encode"): + # pass + # else: + # logger.warning(f"status worker received unknown command: {command}") def find_video(self, uuid) -> Video: for video in self.app.fastflix.queue: @@ -1722,19 +1740,21 @@ def run(self): while True: # Message looks like (command, video_uuid, command_uuid) status = self.status_queue.get() + if status[0] == "queue": + self.main.status_update_signal.emit() if status[0] == "complete": self.main.completed.emit(0) elif status[0] == "error": - self.main.status_update_signal.emit("|".join(status)) + # self.main.status_update_signal.emit("|".join(status)) self.main.completed.emit(1) elif status[0] == "cancelled": self.main.cancelled.emit("|".join(status[1:])) - self.main.status_update_signal.emit("|".join(status)) + # self.main.status_update_signal.emit("|".join(status)) elif status[0] == "exit": try: self.terminate() finally: self.main.close_event.emit() return - else: - self.main.status_update_signal.emit("|".join(status)) + # else: + # self.main.status_update_signal.emit("|".join(status)) diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index 7f72377c..f324e531 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -11,6 +11,7 @@ from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.video import Video +from fastflix.models.queue import get_queue, save_queue from fastflix.resources import black_x_icon, down_arrow_icon, edit_box_icon, folder_icon, play_icon, up_arrow_icon from fastflix.shared import no_border, open_folder from fastflix.widgets.panels.abstract_list import FlixList @@ -48,13 +49,13 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= up_button=QtWidgets.QPushButton(QtGui.QIcon(up_arrow_icon), ""), down_button=QtWidgets.QPushButton(QtGui.QIcon(down_arrow_icon), ""), cancel_button=QtWidgets.QPushButton(QtGui.QIcon(black_x_icon), ""), - reload_buttom=QtWidgets.QPushButton(QtGui.QIcon(edit_box_icon), ""), + reload_button=QtWidgets.QPushButton(QtGui.QIcon(edit_box_icon), ""), ) for widget in self.widgets.values(): widget.setStyleSheet(no_border) - if self.currently_encoding: - widget.setDisabled(True) + # if self.currently_encoding: + # widget.setDisabled(True) title = QtWidgets.QLabel( video.video_settings.video_title @@ -94,17 +95,21 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= status = f"{t('Encoding complete')}" elif video.status.running: status = ( - f"{t('Encoding command')} {video.status.current_command} {t('of')} " + f"{t('Encoding command')} {video.status.current_command + 1} {t('of')} " f"{len(video.video_settings.conversion_commands)}" ) elif video.status.cancelled: - status = t("Cancelled - Ready to try again") + status = t("Cancelled") + # TODO add retry button - if not self.currently_encoding: + if not self.video.status.running: self.widgets.cancel_button.clicked.connect(lambda: self.parent.remove_item(self.video)) - self.widgets.reload_buttom.clicked.connect(lambda: self.parent.reload_from_queue(self.video)) - self.widgets.cancel_button.setFixedWidth(25) - self.widgets.reload_buttom.setFixedWidth(25) + self.widgets.reload_button.clicked.connect(lambda: self.parent.reload_from_queue(self.video)) + self.widgets.cancel_button.setFixedWidth(25) + self.widgets.reload_button.setFixedWidth(25) + else: + self.widgets.cancel_button.hide() + self.widgets.reload_button.hide() grid = QtWidgets.QGridLayout() grid.addLayout(self.init_move_buttons(), 0, 0) @@ -119,7 +124,7 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= grid.addWidget(open_button, 0, 9) right_buttons = QtWidgets.QHBoxLayout() - right_buttons.addWidget(self.widgets.reload_buttom) + right_buttons.addWidget(self.widgets.reload_button) right_buttons.addWidget(self.widgets.cancel_button) grid.addLayout(right_buttons, 0, 10, alignment=QtCore.Qt.AlignRight) @@ -136,10 +141,8 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= def init_move_buttons(self): layout = QtWidgets.QVBoxLayout() layout.setSpacing(0) - self.widgets.up_button.setDisabled(True if self.currently_encoding else self.first) self.widgets.up_button.setFixedWidth(20) self.widgets.up_button.clicked.connect(lambda: self.parent.move_up(self)) - self.widgets.down_button.setDisabled(True if self.currently_encoding else self.last) self.widgets.down_button.setFixedWidth(20) self.widgets.down_button.clicked.connect(lambda: self.parent.move_down(self)) layout.addWidget(self.widgets.up_button) @@ -176,7 +179,6 @@ def __init__(self, parent, app: FastFlixApp): self.paused = False self.encode_paused = False self.encoding = False - self.main.status_update_signal.connect(self.update_status) top_layout = QtWidgets.QHBoxLayout() top_layout.addWidget(QtWidgets.QLabel(t("Queue"))) @@ -235,6 +237,7 @@ def __init__(self, parent, app: FastFlixApp): def reorder(self, update=True): super().reorder(update=update) self.app.fastflix.queue = [track.video for track in self.tracks] + save_queue(self.app.fastflix.queue) def new_source(self): for track in self.tracks: @@ -250,10 +253,13 @@ def clear_complete(self): self.remove_item(queued_item.video) def remove_item(self, video): + # TODO Make sure this one not currently running self.app.fastflix.queue.remove(video) + save_queue(self.app.fastflix.queue) self.new_source() def reload_from_queue(self, video): + # TODO Make sure this one not currently running self.main.reload_video_from_queue(video) self.app.fastflix.queue.remove(video) self.new_source() @@ -299,15 +305,3 @@ def set_after_done(self): command = done_actions["linux"][option] self.app.fastflix.worker_queue.put(["set after done", command]) - - def update_status(self, status: str): - command, *_ = status.split("|") - self.encoding = command not in ("complete", "error", "cancelled", "converted") - for track in self.tracks: - for widget in track.widgets.values(): - widget.setDisabled(self.encoding) - if self.tracks: - self.tracks[0].set_first(True) - self.tracks[-1].set_last(True) - if self.encoding and self.paused: - self.pause_resume_queue() diff --git a/fastflix/widgets/panels/status_panel.py b/fastflix/widgets/panels/status_panel.py index 06284795..2d7d1cb2 100644 --- a/fastflix/widgets/panels/status_panel.py +++ b/fastflix/widgets/panels/status_panel.py @@ -125,16 +125,6 @@ def update_bitrate(self, bitrate): def update_title_bar(self): pass - def set_started_at(self, msg): - try: - started_at = datetime.datetime.fromisoformat(msg.split("|")[-1]) - except Exception: - logger.exception("Unable to parse start time, assuming it was now") - self.started_at = datetime.datetime.now(datetime.timezone.utc) - return - - self.started_at = started_at - def update_time_elapsed(self): now = datetime.datetime.now(datetime.timezone.utc) @@ -150,11 +140,9 @@ def update_time_elapsed(self): self.time_elapsed_label.setText(f"{t('Time Elapsed')}: {timedelta_to_str(time_elapsed)}") - def on_status_update(self, msg): - update_type = msg.split("|")[0] - - if update_type == "running": - self.set_started_at(msg) + def on_status_update(self): + # If there was a status change, we need to restart ticker no matter what + self.started_at = datetime.datetime.now(datetime.timezone.utc) def close(self): self.ticker_thread.terminate() @@ -240,13 +228,18 @@ def run(self): logger.debug("Ticker thread stopped") - def on_status_update(self, msg): - update_type = msg.split("|")[0] + def on_status_update(self): + # update_type = msg.split("|")[0] + # + # if update_type in ("complete", "error", "cancelled", "converted"): + # self.send_tick_signal = False + # else: + # self.send_tick_signal = True - if update_type in ("complete", "error", "cancelled", "converted"): - self.send_tick_signal = False - else: + if self.parent.main.converting: self.send_tick_signal = True + else: + self.send_tick_signal = False def on_stop(self): self.stop_received = True diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py index ff88b0a3..2c0c95ee 100644 --- a/fastflix/widgets/video_options.py +++ b/fastflix/widgets/video_options.py @@ -175,7 +175,7 @@ def clear_tracks(self): self.info.reset() self.debug.reset() - def update_queue(self, currently_encoding=False): + def update_queue(self): self.queue.new_source() def show_queue(self): From 87bb1d9ca525ad2e4ac12766d8d0a5e89c909cd3 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 08:50:04 -0600 Subject: [PATCH 27/50] adding retry button --- fastflix/conversion_worker.py | 7 +++++-- fastflix/data/icons/undo-arrow.png | Bin 0 -> 5829 bytes fastflix/resources.py | 1 + fastflix/widgets/main.py | 21 +++++++++---------- fastflix/widgets/panels/queue_panel.py | 27 +++++++++++++++++++++++-- 5 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 fastflix/data/icons/undo-arrow.png diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 9b6f7235..38fb9d96 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -75,6 +75,8 @@ def set_status( next_command=False, reset_commands=False, ): + if not current_video: + return queue = get_queue() for video in queue: if video.uuid == current_video.uuid: @@ -235,10 +237,11 @@ def start_command(): if request[0] == "cancel": logger.debug(t("Cancel has been requested, killing encoding")) runner.kill() - set_status(video, reset_commands=True, cancelled=True) + if video: + set_status(video, reset_commands=True, cancelled=True) currently_encoding = False allow_sleep_mode() - status_queue.put(("cancelled", video.uuid)) + status_queue.put(("cancelled", video.uuid if video else "")) if request[0] == "pause queue": logger.debug(t("Command worker received request to pause encoding after the current item completes")) diff --git a/fastflix/data/icons/undo-arrow.png b/fastflix/data/icons/undo-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..d4df03e7339a313917d1c2c204a53b239bed077c GIT binary patch literal 5829 zcmdUz+%?#y}8IkQPK3-7Q@M1ZfHB?(w6W z4cNYq&+}hA_nYf;u5&);+%N7|ca*-aIxQ6!6$k{P)p()$5(FY5fIwgn1@S*))Ja(X zhYEddqv!wl|7c$I8Rq|6SKjBPt|7?v(=Ws|De=V##}^ie%xg63?@>Q8_q5Z0Y zR36Z@(D;FDQODQJ2AkNjzn(2yuVWi-`?2NSilPg>;|*vrU{37%xM-FvMXTh^zVxc&*-jO7`AMTD3a4 zc!kh;-@3LQG75mGx!BgE9n>!=i<}J$)*v;c;5uT4A0*fNLr8Y}cz7vKUyqWcWE*Dv z_abUN#-E|@HRX7t2P(aPau(>2?Dmzo*qOsmu`7 z_XvQU1sV~heDN!eZu)y>- zd5!TMZ|rY$_la(t6jsPdjV>{MLraB{j=A8rs35qd) zlc6Vtvk0phQT(nslE{HVyAtiyYuK(;VxOgA>x{R~m-(P%M^JGDF{Hjk8g;|)lX zyNl>8fgoWF#d}KaM!+mB?1#qaHCdma_$hOtjTypv$x z^YzTC*dz1-IB^%ci3A=$;}&F0HA4J)<|kB0{+>d+2wlEFFPD(l%NyR|aCLagq2cI0 zFS0}R3VOl?l$Xdj=-Gnt!>l#w@(&$!SN8GK!O>7g6B2V~e{X@$# z5Q*$T<8`ewC}n~e-dbq@RUb}-;Em_GR%ox~gJGc8fBU|+PJ9awB!xc{gbsESE(t$u zM0|;tR&JaF;Pa@{`&;`rsMFG|;Y%pSG?pIf1wSuaA^%gE#g2_VEiEEh_Jp(I<$z9H zavsm>{Xdlj@0r_IZld2mKH|if8aL2_e%P2>kcOO=x=?oDSTDM8$!uQmWb=m@(=_Jy z@MPorKWrS8FbH~>e)%;&M2B9zLEhb0WxoE`pZ@*f962-Gc}(KpUpqp70Q#$Ei`4uL zZe0M!>O-nvQIBbZ9rZtmv0qJ4H9t(*-nj8SC+`=BTk+$6qE!V`=g;7d|ht7q@Nt+0v+g(F_>X@osv}GfhaH3)) zOkYIGEx&|=aph68KDQu>psR{)Tsdn#*5(U^B8a|D1v3rf|3%BFJ)loJ>Y z<@xgtsnXHS#5K+^el6spro^d&A+M+vFJ(?7t{hHVv>R_mHI?2o2t;KBS?h9>b&0pq zNZVF*hVaqqgY&i+RL03(oGt<}=N^UGy`1H>Jb@J9QjNJ`4Z6Lu}+Vqrpu`A^pp5mZnp1JFQbmq$6-1D33x7BXaA?n_LaUY~5JjTPJ z)`K_&BAx?$n=1uE#_x2x`~ZtE(OMbg|M?v;!@zdR1atvM7)6Ag|5A|0lQ7|T^LB)Z z87TrZK&dOSuJb0ts_#+6k-e=t{MARwhQ8KC(5JA>BxZe9$pqjg?KS{hqgx8qNo4`*9Fi4A33aSP1 zGo(WD|H2_rvy$cD$Rc76SwilOYr4iL6wQ7AC(xlKU9aE=1eYSN`9Zx!w85tW;V@kB zb%TQbqduCg!UKeEmXo~%)E6*??0g>@SwJ^^%!%*68N$yCot?RDy*)sOWo@o6(JThp1yq**BvS4VkE)0M+BEtyAxd%4a=YaQFbhd%Lj#g{naivHrUO&g-vPIe+3%)d2S+ zLAQvTk9c+c>g+zf8FWa5dhAUw-X<&C_5B8B*XC0MqXxX0aNnuXv0rZ*AfifngqyuG z)gRp%+YsJGCF^@`_3KU9Y__0J0Y*FEa{=XorQ{}N2Ozt&;53An5G*07hMRA9maRJ4 zOe=1*@z9Po0dsf`fw@uEc2I8uL3!C;y@~nHrn5~GzMuc*rKQ}Hh7~KD9R2;(1@aX0s8C)6_>6|jGmVU}1oAu7|`vBpf{E-`~^Y0Jh%D>F}XJU9Kt=m@5{ zU}p|B9U29fE}Phd z76KC>w#&0Ef6E_*@=|s9s(gRW@KKq>DpbPGj?GB5?GUh&-;r0Ve(M=&d}ekTM1<%Y zI(z9`8bcuy5ajilL*`0uEWDygze|Zfl#%{b@H3Lb;MT^_iUR#EHh-ik98QyUpF+r$ z#_`Js&Dn=Z!ym_u!)q=RkOW=Q?&_sq;bcBAHcc~TgT0f}*P(B*hnJ3_kOBpq_{R#Q ze%A$K=Sv$g?BRzPF~oF)+3$Rmzyh}Ew>{5vpxN(dHtPPH>zrc2*6Dnqn|EGS%?wJX z1UA=)RAlOR&1|i5rMFtye=c-!J@VTZw&OTlA^Bx(<-#95ru$HQW?1_7~%Q0*b#z{|(btOuUU z-zpAA>&24FpraRt2Vd^*q~11P;H-tkD@;DFg)!W7qTivb`IdS!P?oEjfd|E&j-&+C z)kci7;BsEoHJf+U>GNeguC3ebq@Bp1W_=IxwGPmC;idj*FT24$0kbqP{5>BY^G-U; zJ*bD0c$q+&EIdl9yj^xRdbDq&9UuwIRE)FVzJ?@?H}fo-s`gc7l!ZvL27rYN4!NwT z+NN$@xo?b7$fxqXal4gOAq?D7vH5BME>>-NhzY0%zpu(3GI0aLrsEUd8?$Ds?gWEw z4KVY>eNGNfanG(?3S6X*{S0*pBD#v*C^ipiC+k(qH&_NFXR>LD*;Ve>U5QskP9!^X zPkl6zM&QM$Ev$&Sy@mk7|S_ijLzE9i4WdxxH2M@%#23WJEv-xEJ{&VqlS5KAoLVB_pdR7G)<=Us}-YHSQ2WaFghK9{pB9NqoPDe8^9fzMo)b^ z-bIx@$*{gudk)C318fgpugfhNI$d^*hhBtLj<9ntFWmgl`^B?2i=t#Pea=i2OJ2SL4OVf;%FB=NKbDPX; zcF8|kLP6qmwuDo-}E(ggqH%iJ!d_P!vi*n0b}cMF~2GBe(s;CQGs!Tjkp2vnNS`9++LLf}h^4z~$1 zbP4Ej_q@EV@06(Q+J6j??7!Wcsd*qIb+ztjP{dX}{=Of6X=s*XMr^^_^K~|A@TTqp zVv-L+I=ZryCe-Tc39*!ET~(F}-@hzGhR$+QbV}=2IVQv+>{-fHE26g3zHPK#?{h*@ zR}&sc`naN!HonHjqB7Ei)-Oghay^Yp93x_r6U7OWJP@n+qr|@sbwTkGSKG=? zw`o+qBIG8i$aoPagJ}e_mqGXsp~Em-B6R3%`WiZ(Ya{r|bFDkQ8H0A?3gZ*U5#36% zD|f!}kb2H}Q9Jk)QMH1a8q7R8S2v*zVJMC>PnMB&+ui-1vpG zJBS+6K^;;+e#?CliYucP}&N>cI@ZET11}Y`Xoz;ukIVP|3XH+UY#wS zQn2l?$~S25I97_DY_CXF8rLIUXy+$VV?DTcN-W}kmq2l50opKQV)G@u$jiSWrH$vAl)oRM!$>}pGp#O4iLrGT ze`aZ`kmOCOJC7p4>KZ?ZOioit__Edr$~kK}Qwk+lj7(EsKfr{Zm3<4tHql0ZCQPp? z`RXBr$KgRlUOR}<+dOR6tnK#dhZ6v)lw3cugDxMFATmHARymHc^DiLeib9#+5c99%)P0So@S5wt>P)swTC)@Ar#* z6wgfpEokSN-(qCihCE(*V|?!JKV7f38iGpUDl#mQ`Y^R!JMfTlm5j5)hBxmVxwEQ+ zVk7xZ+tMQyQBUu26y8OW+xD%~r;fxmO2OiteRQyrYS&NhV!yk|VMj(m*fg_pF4efc zZFFdjPF(4i?~mU7cW9;U-Tbk7!*h&88{YANGScZ~ zv2%i;0Iz+osC)!kl3{K8sM1WSTn%#=coU{yKOCU`6)hL!oLkvzlf~ad_bm|5z7hV$ z(O!|DQYP{Trbr0gD~?qP$sGaMi`p>Isp(_1KQ2j~r(^Ozl-Iqn=!@X3|E|$kA7m>R zZjHyaJOtT8?JOZ4(GjXqaaKt+P9oU?4piO$bC2%mIBOZPU|LG~!!?yf)sDZXIDFqD zmJYNy^2I&tl1q;Vl&rNwldGJaSPkBjsd5;5Kp4?Z`I`bvq`vye?0$0RXmgu8Yd#6* zb`?A6LxLqEi~nFXmgYUdKj0UFuGb~{eD`^=o|}(C2AH8oV81(ah3bk*yp~5%zt;=_ z>gqpjUc>#-{S@*{Jjomj^e5|6Lt04BUC#tUY@?m`up^E>j8&jLq9W(44F`~+#&7dZ3BO5W0sWvO>ihN455cw+M@R?-e6&sehCH`YEFdg?y z&mn&5o(C2>xUFQXULzPSMO`;J-GSf~a{nrK(CNC(JL(Z|t{3zzz$4W^neROGw!~b! zT@KzAbCAIN5eWjBz9UiP;HBE30INZYAfZkNh-xLj$A5Jv2lNu~6$~%@WM`jOM_v=y z(;^qB&7u=_fCD*}I;x~eL+MBz6halgfp&eq$8{kEedL;!f_-svL(Sv(cb zC^RG>Cq;ShV_hfRX1xY&MLOzLVO`Upx5E>`FB}5nFl|%vZ-iKrC{%-J;$+P&R-g4WM ztU$A+E?NM)-y${6o&f`!?>#lI3_5YGqIW7~5&6p7bQtd?P2)BPuI+NTJIc!zA-6P2 z<7Cf5%zQic3hoWF3!nSUPN=QCPrerP=GJ-M?}UhLplH_p#X~JKe;J)0gZ1_9YH{P_ zy!%FXrUd7d{d=h|17j1`6nhn<9@PY7&6eckMVHJO>ZHF_b2^|qZk{nAym>G>LZCd3 XT+)6Hnz#DjXSBw1UDXN|+tB|3dJj-6 literal 0 HcmV?d00001 diff --git a/fastflix/resources.py b/fastflix/resources.py index d306eca0..b3b95c2b 100644 --- a/fastflix/resources.py +++ b/fastflix/resources.py @@ -36,4 +36,5 @@ working_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/pending-work.png")).resolve()) advanced_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/advanced.png")).resolve()) info_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/info.png")).resolve()) +undo_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/undo-arrow.png")).resolve()) loading_movie = str(Path(pkg_resources.resource_filename(__name__, "data/icons/loading.gif")).resolve()) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 8137684b..26bf95a9 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1479,7 +1479,7 @@ def set_convert_button(self, convert=True): @reusables.log_exception("fastflix", show_traceback=False) def encode_video(self): # from fastflix.models.queue import save_queue - + # TODO make sure there is a video that can be encoded if self.converting: logger.debug(t("Canceling current encode")) self.app.fastflix.worker_queue.put(["cancel"]) @@ -1499,12 +1499,8 @@ def encode_video(self): # Command looks like (video_uuid, command_uuid, command, work_dir, filename) # Request looks like (queue command, log_dir, (commands)) requests = ["add_items", str(self.app.fastflix.log_path)] - commands = self.get_commands() - - if not commands: - return error_message(t("No new items in queue to convert")) - requests.append(tuple(commands)) + # TODO here check for videos if ready. Make shared function for ready? self.converting = True self.set_convert_button(False) @@ -1594,17 +1590,20 @@ def conversion_cancelled(self, data): self.app.fastflix.queue = get_queue() + self.video_options.update_queue() + + if self.video_options.queue.paused: + self.video_options.queue.pause_resume_queue() + + if not data: + return + try: video_uuid, *_ = data.split("|") cancelled_video = self.find_video(video_uuid) except Exception: return - self.video_options.update_queue() - - if self.video_options.queue.paused: - self.video_options.queue.pause_resume_queue() - if cancelled_video.video_settings.output_path.exists(): sm = QtWidgets.QMessageBox() sm.setWindowTitle(t("Cancelled")) diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index f324e531..4cfcc40e 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -12,7 +12,15 @@ from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.video import Video from fastflix.models.queue import get_queue, save_queue -from fastflix.resources import black_x_icon, down_arrow_icon, edit_box_icon, folder_icon, play_icon, up_arrow_icon +from fastflix.resources import ( + black_x_icon, + down_arrow_icon, + edit_box_icon, + folder_icon, + play_icon, + up_arrow_icon, + undo_icon, +) from fastflix.shared import no_border, open_folder from fastflix.widgets.panels.abstract_list import FlixList @@ -50,6 +58,7 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= down_button=QtWidgets.QPushButton(QtGui.QIcon(down_arrow_icon), ""), cancel_button=QtWidgets.QPushButton(QtGui.QIcon(black_x_icon), ""), reload_button=QtWidgets.QPushButton(QtGui.QIcon(edit_box_icon), ""), + retry_button=QtWidgets.QPushButton(QtGui.QIcon(undo_icon), ""), ) for widget in self.widgets.values(): @@ -88,6 +97,7 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= open_button.setStyleSheet(no_border) view_button.setStyleSheet(no_border) + add_retry = False status = t("Ready to encode") if video.status.error: status = t("Encoding errored") @@ -100,7 +110,7 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= ) elif video.status.cancelled: status = t("Cancelled") - # TODO add retry button + add_retry = True if not self.video.status.running: self.widgets.cancel_button.clicked.connect(lambda: self.parent.remove_item(self.video)) @@ -122,6 +132,10 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= if video.status.complete: grid.addWidget(view_button, 0, 8) grid.addWidget(open_button, 0, 9) + elif add_retry: + grid.addWidget(self.widgets.retry_button, 0, 8) + self.widgets.retry_button.setFixedWidth(25) + self.widgets.retry_button.clicked.connect(lambda: self.parent.retry_video(self.video)) right_buttons = QtWidgets.QHBoxLayout() right_buttons.addWidget(self.widgets.reload_button) @@ -305,3 +319,12 @@ def set_after_done(self): command = done_actions["linux"][option] self.app.fastflix.worker_queue.put(["set after done", command]) + + def retry_video(self, video): + for vid in self.app.fastflix.queue: + if vid.uuid == video.uuid: + vid.status.cancelled = False + vid.status.current_command = 0 + break + save_queue(self.app.fastflix.queue) + self.new_source() From 8eb3472462a6115a5d47050ebc755a37b21e1a44 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 09:49:34 -0600 Subject: [PATCH 28/50] * Fixing HDR10 details to be track specific (thanks to Harybo) --- CHANGES | 1 + fastflix/conversion_worker.py | 1 + .../encoders/nvencc_hevc/command_builder.py | 28 +++--- fastflix/flix.py | 90 ++++++++++--------- fastflix/models/video.py | 18 +++- 5 files changed, 76 insertions(+), 62 deletions(-) diff --git a/CHANGES b/CHANGES index 0cee93a4..371f706a 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,7 @@ * Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) * Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz) * Fixing that returning item back from queue of a different encoder type would crash Fastflix +* Fixing HDR10 details to be track specific (thanks to Harybo) ## Version 4.1.2 diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 38fb9d96..46eeedf3 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -188,6 +188,7 @@ def start_command(): currently_encoding = False allow_sleep_mode() logger.info(t("all conversions complete")) + status_queue.put(("complete",)) if after_done_command: logger.info(f"{t('Running after done command:')} {after_done_command}") try: diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 7b9d20aa..e66dc020 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -33,11 +33,15 @@ def build_audio(audio_tracks): bitrate = "" if track.conversion_codec not in lossless: bitrate = f"--audio-bitrate {track.outdex}?{track.conversion_bitrate.rstrip('k')} " - command_list.append(f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate}") - command_list.append( - f'--audio-metadata {track.outdex}?title="{track.title}" ' - f'--audio-metadata {track.outdex}?handler="{track.title}" ' - ) + command_list.append( + f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate} --audio-metadata {track.outdex}?clear" + ) + + if track.title: + command_list.append( + f'--audio-metadata {track.outdex}?title="{track.title}" ' + f'--audio-metadata {track.outdex}?handler="{track.title}" ' + ) return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" @@ -67,11 +71,6 @@ def build(fastflix: FastFlix): # --profile main10 --tier main - video_track = 0 - for i, track in enumerate(video.streams.video): - if int(track.index) == video.video_settings.selected_track: - video_track = i - master_display = None if fastflix.current_video.master_display: master_display = ( @@ -150,14 +149,14 @@ def build(fastflix: FastFlix): f'"{unixy(fastflix.config.nvencc)}"', "-i", f'"{unixy(video.source)}"', - f"--video-streamid {video_track}", + f"--video-streamid {int(video.current_video_stream['id'], 16)}", trim, (f"--vpp-rotate {video.video_settings.rotate}" if video.video_settings.rotate else ""), transform, (f'--output-res {video.video_settings.scale.replace(":", "x")}' if video.video_settings.scale else ""), crop, - (f"--video-metadata 1?clear" if video.video_settings.remove_metadata else "--video-metadata 1?copy"), - (f'--video-metadata 1?title="{video.video_settings.video_title}"' if video.video_settings.video_title else ""), + (f"--video-metadata clear" if video.video_settings.remove_metadata else "--video-metadata copy"), + (f'--video-metadata title="{video.video_settings.video_title}"' if video.video_settings.video_title else ""), ("--chapter-copy" if video.video_settings.copy_chapters else ""), "-c", "hevc", @@ -200,7 +199,6 @@ def build(fastflix: FastFlix): f"--avsync {'cfr' if video.frame_rate == video.average_frame_rate else 'vfr'}", (f"--interlace {video.interlaced}" if video.interlaced else ""), ("--vpp-yadif" if video.video_settings.deinterlace else ""), - (f"--output-res {video.video_settings.scale}" if video.video_settings.scale else ""), (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", @@ -211,7 +209,7 @@ def build(fastflix: FastFlix): f'"{unixy(video.video_settings.output_path)}"', ] - return [Command(command=" ".join(command), name="NVEncC Encode", exe="NVEncE")] + return [Command(command=" ".join(x for x in command if x), name="NVEncC Encode", exe="NVEncE")] # -i "Beverly Hills Duck Pond - HDR10plus - Jessica Payne.mp4" -c hevc --profile main10 --tier main --output-depth 10 --vbr 6000k --preset quality --multipass 2pass-full --aq --repeat-headers --colormatrix bt2020nc --transfer smpte2084 --colorprim bt2020 --lookahead 16 -o "nvenc-6000k.mkv" diff --git a/fastflix/flix.py b/fastflix/flix.py index f564c8b4..35355299 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -397,52 +397,54 @@ def parse_hdr_details(app: FastFlixApp, **_): logger.exception(f"Unexpected error while processing master-display from {streams.video[0]}") else: if master_display: - app.fastflix.current_video.master_display = master_display - app.fastflix.current_video.cll = cll - return - - result = execute( - [ - f"{app.fastflix.config.ffprobe}", - "-loglevel", - "panic", - "-select_streams", - f"v:{video_track}", - "-print_format", - "json", - "-show_frames", - "-read_intervals", - "%+#1", - "-show_entries", - "frame=color_space,color_primaries,color_transfer,side_data_list,pix_fmt", - f"{unixy(app.fastflix.current_video.source)}", - ] - ) + app.fastflix.current_video.hdr10_streams.append( + Box(index=video_stream.index, master_display=master_display, cll=cll) + ) + continue + + result = execute( + [ + f"{app.fastflix.config.ffprobe}", + "-loglevel", + "panic", + "-select_streams", + f"v:{video_stream.index}", + "-print_format", + "json", + "-show_frames", + "-read_intervals", + "%+#1", + "-show_entries", + "frame=color_space,color_primaries,color_transfer,side_data_list,pix_fmt", + f"{unixy(app.fastflix.current_video.source)}", + ] + ) - try: - data = Box.from_json(result.stdout, default_box=True, default_box_attr="") - except BoxError: - # Could not parse details - logger.error( - "COULD NOT PARSE FFPROBE HDR METADATA, PLEASE OPEN ISSUE WITH THESE DETAILS:" - f"\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}" - ) - return - if "frames" not in data or not len(data.frames): - return - data = data.frames[0] - if not data.get("side_data_list"): - return + try: + data = Box.from_json(result.stdout, default_box=True, default_box_attr="") + except BoxError: + # Could not parse details + logger.error( + "COULD NOT PARSE FFPROBE HDR METADATA, PLEASE OPEN ISSUE WITH THESE DETAILS:" + f"\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}" + ) + continue + if "frames" not in data or not len(data.frames): + continue + data = data.frames[0] + if not data.get("side_data_list"): + continue - try: - master_display, cll = convert_mastering_display(data) - except FlixError as err: - logger.error(str(err)) - except Exception: - logger.exception(f"Unexpected error while processing master-display from {streams.video[0]}") - else: - app.fastflix.current_video.master_display = master_display - app.fastflix.current_video.cll = cll + try: + master_display, cll = convert_mastering_display(data) + except FlixError as err: + logger.error(str(err)) + except Exception: + logger.exception(f"Unexpected error while processing master-display from {streams.video[0]}") + else: + app.fastflix.current_video.hdr10_streams.append( + Box(index=video_stream.index, master_display=master_display, cll=cll) + ) def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 071c4b5c..e019c0b8 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -108,15 +108,27 @@ class Video(BaseModel): format: Box = None interlaced: bool = True - # HDR10 Details - master_display: Optional[Box] = None - cll: Optional[str] = None + hdr10_streams: List[Box] = Field(default_factory=list) hdr10_plus: Optional[List[int]] = None video_settings: VideoSettings = Field(default_factory=VideoSettings) status: Status = Field(default_factory=Status) uuid: str = Field(default_factory=lambda: str(uuid.uuid4())) + @property + def master_display(self) -> Optional[Box]: + for track in self.hdr10_streams: + if track.index == self.video_settings.selected_track: + return track["master_display"] + return None + + @property + def cll(self) -> Optional[str]: + for track in self.hdr10_streams: + if track.index == self.video_settings.selected_track: + return track["cll"] + return None + @property def current_video_stream(self): try: From 250a4be9d1698645eecfa37b3c143206c4f30256 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 12:24:54 -0600 Subject: [PATCH 29/50] * Fixing HDR10 details to be track specific (thanks to Harybo) --- .../encoders/nvencc_hevc/command_builder.py | 53 +++++-------------- .../encoders/nvencc_hevc/settings_panel.py | 22 ++++---- fastflix/models/encode.py | 2 +- fastflix/widgets/main.py | 50 ++--------------- fastflix/widgets/panels/queue_panel.py | 9 +--- fastflix/widgets/profile_window.py | 28 +++++++--- 6 files changed, 50 insertions(+), 114 deletions(-) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index e66dc020..07775244 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -import re -import secrets -from typing import List, Tuple, Union +from typing import List import logging -from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null +from fastflix.encoders.common.helpers import Command from fastflix.models.encode import NVEncCSettings from fastflix.models.video import SubtitleTrack, Video from fastflix.models.fastflix import FastFlix @@ -65,12 +63,6 @@ def build(fastflix: FastFlix): video: Video = fastflix.current_video settings: NVEncCSettings = fastflix.current_video.video_settings.video_encoder_settings - # beginning, ending = generate_all(fastflix, "hevc_nvenc") - - # beginning += f'{f"-tune:v {settings.tune}" if settings.tune else ""} {generate_color_details(fastflix)} -spatial_aq:v {settings.spatial_aq} -tier:v {settings.tier} -rc-lookahead:v {settings.rc_lookahead} -gpu {settings.gpu} -b_ref_mode {settings.b_ref_mode} ' - - # --profile main10 --tier main - master_display = None if fastflix.current_video.master_display: master_display = ( @@ -145,11 +137,21 @@ def build(fastflix: FastFlix): if settings.max_q_i and settings.max_q_p and settings.max_q_b: max_q = f"{settings.max_q_i}:{settings.max_q_p}:{settings.max_q_b}" + try: + stream_id = int(video.current_video_stream["id"], 16) + except Exception: + if len(video.streams.video) > 1: + logger.warning("Could not get stream ID from source, the proper video track may not be selected!") + stream_id = None + + if video.current_video_stream.bit_depth > 8 and settings.profile != "main": + logger.warning("Profile should be set to 'main' for 8 bit videos") + command = [ f'"{unixy(fastflix.config.nvencc)}"', "-i", f'"{unixy(video.source)}"', - f"--video-streamid {int(video.current_video_stream['id'], 16)}", + (f"--video-streamid {stream_id}" if stream_id else ""), trim, (f"--vpp-rotate {video.video_settings.rotate}" if video.video_settings.rotate else ""), transform, @@ -210,32 +212,3 @@ def build(fastflix: FastFlix): ] return [Command(command=" ".join(x for x in command if x), name="NVEncC Encode", exe="NVEncE")] - - -# -i "Beverly Hills Duck Pond - HDR10plus - Jessica Payne.mp4" -c hevc --profile main10 --tier main --output-depth 10 --vbr 6000k --preset quality --multipass 2pass-full --aq --repeat-headers --colormatrix bt2020nc --transfer smpte2084 --colorprim bt2020 --lookahead 16 -o "nvenc-6000k.mkv" - -# -# if settings.profile: -# beginning += f"-profile:v {settings.profile} " -# -# if settings.rc: -# beginning += f"-rc:v {settings.rc} " -# -# if settings.level: -# beginning += f"-level:v {settings.level} " -# -# pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}" -# -# command_1 = ( -# f"{beginning} -pass 1 " -# f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} -2pass 1 ' -# f'{settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}' -# ) -# command_2 = ( -# f'{beginning} -pass 2 -passlogfile "{pass_log_file}" -2pass 1 ' -# f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} " -# ) + ending -# return [ -# Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"), -# Command(command=re.sub("[ ]+", " ", command_2), name="Second pass bitrate", exe="ffmpeg"), -# ] diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index d921a3bf..0e9dc011 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -153,6 +153,7 @@ def init_tune(self): ) def init_profile(self): + # TODO auto return self._add_combo_box( label="Profile_encoderopt", widget_name="profile", @@ -179,6 +180,16 @@ def init_tier(self): opt="tier", ) + def init_aq(self): + # TODO change to spatial or temporal + return self._add_combo_box( + label="Spatial AQ", + tooltip="", + widget_name="spatial_aq", + options=["off", "on"], + opt="spatial_aq", + ) + def init_spatial_aq(self): return self._add_combo_box( label="Spatial AQ", @@ -241,17 +252,6 @@ def init_level(self): self.widgets.level.setMinimumWidth(60) return layout - def init_gpu(self): - layout = self._add_combo_box( - label="GPU", - tooltip="Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on", - widget_name="gpu", - opt="gpu", - options=["any"] + [str(x) for x in range(8)], - ) - self.widgets.gpu.setMinimumWidth(50) - return layout - def init_b_ref_mode(self): layout = self._add_combo_box( label="B Ref Mode", diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index afed3243..a9931302 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -95,7 +95,7 @@ class NVEncCSettings(EncoderSettings): name = "HEVC (NVEncC)" preset: str = "quality" profile: str = "main" - bitrate: Optional[str] = "6000k" + bitrate: Optional[str] = "5000k" cqp: Optional[str] = None cq: int = 0 spatial_aq: bool = True diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 26bf95a9..8911853e 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1621,7 +1621,7 @@ def conversion_cancelled(self, data): except OSError: pass - @reusables.log_exception("fastflix", show_traceback=False) + @reusables.log_exception("fastflix", show_traceback=True) def dropEvent(self, event): if not event.mimeData().hasUrls: return event.ignore() @@ -1663,55 +1663,11 @@ def status_update(self): if mkv_prop_edit := shutil.which("mkvpropedit"): worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) worker.start() + video.status.subtitle_fixed = True + # TODO Save self.video_options.update_queue() - # try: - # command, video_uuid, command_uuid, *_ = status.split("|") - # except ValueError: - # logger.exception(f"Could not process status update from the command worker: {status}") - # return - # # - # try: - # video = self.find_video(video_uuid) - # command_index = self.find_command(video, command_uuid) - # except FlixError as err: - # logger.error(f"Could not update queue status due to not found video/command - {err}") - # return - # - # if command == "converted": - # if command_index == len(video.video_settings.conversion_commands): - # video.status.complete = True - # video.status.success = True - # video.status.running = False - # if video.video_settings.subtitle_tracks and not video.video_settings.subtitle_tracks[0].disposition: - # if mkv_prop_edit := shutil.which("mkvpropedit"): - # worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) - # worker.start() - # self.video_options.update_queue(currently_encoding=self.converting) - # else: - # logger.error(f"This should not happen? {status} - {video}") - # - # elif command == "running": - # video.status.current_command = command_index - # video.status.running = True - # self.video_options.update_queue(currently_encoding=self.converting) - # - # elif command == "error": - # video.status.error = True - # video.status.running = False - # self.video_options.update_queue(currently_encoding=self.converting) - # - # elif command == "cancelled": - # video.status.cancelled = True - # video.status.running = False - # self.video_options.update_queue(currently_encoding=self.converting) - # - # elif command in ("paused encode", "resumed encode"): - # pass - # else: - # logger.warning(f"status worker received unknown command: {command}") - def find_video(self, uuid) -> Video: for video in self.app.fastflix.queue: if uuid == video.uuid: diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index 4cfcc40e..fa76c59c 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -42,7 +42,7 @@ class EncodeItem(QtWidgets.QTabWidget): - def __init__(self, parent, video: Video, index, first=False, currently_encoding=False): + def __init__(self, parent, video: Video, index, first=False): self.loading = True super().__init__(parent) self.parent = parent @@ -50,7 +50,6 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= self.first = first self.last = False self.video = video - self.currently_encoding = currently_encoding self.setFixedHeight(60) self.widgets = Box( @@ -63,8 +62,6 @@ def __init__(self, parent, video: Video, index, first=False, currently_encoding= for widget in self.widgets.values(): widget.setStyleSheet(no_border) - # if self.currently_encoding: - # widget.setDisabled(True) title = QtWidgets.QLabel( video.video_settings.video_title @@ -165,11 +162,9 @@ def init_move_buttons(self): def set_first(self, first=True): self.first = first - self.widgets.up_button.setDisabled(True if self.currently_encoding else self.first) def set_last(self, last=True): self.last = last - self.widgets.down_button.setDisabled(True if self.currently_encoding else self.last) def set_outdex(self, outdex): pass @@ -258,7 +253,7 @@ def new_source(self): track.close() self.tracks = [] for i, video in enumerate(self.app.fastflix.queue, start=1): - self.tracks.append(EncodeItem(self, video, index=i, currently_encoding=self.encoding)) + self.tracks.append(EncodeItem(self, video, index=i)) super()._new_source(self.tracks) def clear_complete(self): diff --git a/fastflix/widgets/profile_window.py b/fastflix/widgets/profile_window.py index 8dc063bb..5ada6ed2 100644 --- a/fastflix/widgets/profile_window.py +++ b/fastflix/widgets/profile_window.py @@ -2,6 +2,7 @@ import shutil from pathlib import Path +import logging from box import Box from iso639 import Lang @@ -21,11 +22,15 @@ rav1eSettings, x264Settings, x265Settings, + NVEncCSettings, + FFmpegNVENCSettings, ) from fastflix.shared import error_message language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower()) +logger = logging.getLogger("fastflix") + class ProfileWindow(QtWidgets.QWidget): def __init__(self, app: FastFlixApp, main, *args, **kwargs): @@ -150,22 +155,29 @@ def save(self): if isinstance(self.encoder, x265Settings): new_profile.x265 = self.encoder - if isinstance(self.encoder, x264Settings): + elif isinstance(self.encoder, x264Settings): new_profile.x264 = self.encoder - if isinstance(self.encoder, rav1eSettings): + elif isinstance(self.encoder, rav1eSettings): new_profile.rav1e = self.encoder - if isinstance(self.encoder, SVTAV1Settings): + elif isinstance(self.encoder, SVTAV1Settings): new_profile.svt_av1 = self.encoder - if isinstance(self.encoder, VP9Settings): + elif isinstance(self.encoder, VP9Settings): new_profile.vp9 = self.encoder - if isinstance(self.encoder, AOMAV1Settings): + elif isinstance(self.encoder, AOMAV1Settings): new_profile.aom_av1 = self.encoder - if isinstance(self.encoder, GIFSettings): + elif isinstance(self.encoder, GIFSettings): new_profile.gif = self.encoder - if isinstance(self.encoder, WebPSettings): + elif isinstance(self.encoder, WebPSettings): new_profile.webp = self.encoder - if isinstance(self.encoder, CopySettings): + elif isinstance(self.encoder, CopySettings): new_profile.copy_settings = self.encoder + elif isinstance(self.encoder, NVEncCSettings): + new_profile.nvencc_hevc = self.encoder + elif isinstance(self.encoder, FFmpegNVENCSettings): + new_profile.ffmpeg_hevc_nvenc = self.encoder + else: + logger.error("Profile cannot be saved! Unknown encoder type.") + return self.app.fastflix.config.profiles[profile_name] = new_profile self.app.fastflix.config.selected_profile = profile_name From cc21d7364230605100772aea4fb8124c9d26a85d Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 15:34:08 -0600 Subject: [PATCH 30/50] properly disabling dups for NVEncC --- fastflix/application.py | 12 ++++++------ fastflix/encoders/nvencc_hevc/settings_panel.py | 1 + fastflix/widgets/panels/audio_panel.py | 7 +++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/fastflix/application.py b/fastflix/application.py index e00f4105..e0dc31ec 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -59,19 +59,19 @@ def init_encoders(app: FastFlixApp, **_): encoders = [ hevc_plugin, - avc_plugin, - gif_plugin, - vp9_plugin, - webp_plugin, + nvenc_plugin, av1_plugin, rav1e_plugin, svt_av1_plugin, - nvenc_plugin, + avc_plugin, + vp9_plugin, + gif_plugin, + webp_plugin, copy_plugin, ] if app.fastflix.config.nvencc: - encoders.insert(len(encoders) - 1, nvencc_plugin) + encoders.insert(1, nvencc_plugin) app.fastflix.encoders = { encoder.name: encoder diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 0e9dc011..e837bdd6 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -421,6 +421,7 @@ def update_video_encoder_settings(self): vbr_target=self.widgets.vbr_target.currentText() if self.widgets.vbr_target.currentIndex() > 0 else None, b_ref_mode=self.widgets.b_ref_mode.currentText(), ) + encode_type, q_value = self.get_mode_settings() settings.cqp = q_value if encode_type == "qp" else None settings.bitrate = q_value if encode_type == "bitrate" else None diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index f5d5621b..737271b4 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -37,6 +37,7 @@ def __init__( codecs=(), channels=2, all_info=None, + disable_dup=False, ): self.loading = True super(Audio, self).__init__(parent) @@ -104,6 +105,10 @@ def __init__( self.widgets.dup_button.clicked.connect(lambda: self.dup_me()) self.widgets.dup_button.setFixedWidth(20) + if disable_dup: + self.widgets.dup_button.hide() + self.widgets.dup_button.setDisabled(True) + self.widgets.delete_button.clicked.connect(lambda: self.del_me()) self.widgets.delete_button.setFixedWidth(20) @@ -351,6 +356,7 @@ def lang_match(self, track): def new_source(self, codecs): self.tracks: List[Audio] = [] self._first_selected = False + disable_dup = "nvencc" in self.main.convert_to.lower() for i, x in enumerate(self.app.fastflix.current_video.streams.audio, start=1): track_info = "" tags = x.get("tags", {}) @@ -379,6 +385,7 @@ def new_source(self, codecs): available_audio_encoders=self.available_audio_encoders, enabled=self.lang_match(x), all_info=x, + disable_dup=disable_dup, ) self.tracks.append(new_item) From 1deb68c530d22256d76c994acf89f080279bf2eb Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 19:36:12 -0600 Subject: [PATCH 31/50] random updates --- .../encoders/nvencc_hevc/command_builder.py | 8 +- .../encoders/nvencc_hevc/settings_panel.py | 79 +++++++++---------- fastflix/flix.py | 18 ----- fastflix/models/encode.py | 3 +- fastflix/models/video.py | 35 +++++++- fastflix/widgets/main.py | 34 +++++--- fastflix/widgets/panels/queue_panel.py | 2 - fastflix/widgets/profile_window.py | 1 + 8 files changed, 103 insertions(+), 77 deletions(-) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 07775244..22fa5099 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -147,6 +147,12 @@ def build(fastflix: FastFlix): if video.current_video_stream.bit_depth > 8 and settings.profile != "main": logger.warning("Profile should be set to 'main' for 8 bit videos") + aq = "--no-aq" + if settings.aq.lower() == "spatial": + aq = f"--aq --aq-strength {settings.aq_strength}" + elif settings.aq.lower() == "temporal": + aq = f"--aq-temporal --aq-strength {settings.aq_strength}" + command = [ f'"{unixy(fastflix.config.nvencc)}"', "-i", @@ -178,7 +184,7 @@ def build(fastflix: FastFlix): "--tier", settings.tier, (f"--lookahead {settings.lookahead}" if settings.lookahead else ""), - ("--aq" if settings.spatial_aq else "--no-aq"), + aq, "--colormatrix", (video.video_settings.color_space or "auto"), "--transfer", diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index e837bdd6..1a59ecbe 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -52,6 +52,13 @@ ] +def get_breaker(): + breaker_line = QtWidgets.QWidget() + breaker_line.setMaximumHeight(2) + breaker_line.setStyleSheet("background-color: #ccc; margin: auto 0; padding: auto 0;") + return breaker_line + + class NVENCC(SettingPanel): profile_name = "nvencc_hevc" hdr10plus_signal = QtCore.Signal(str) @@ -73,16 +80,24 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self._add_custom(title="Custom NVEncC options", disable_both_passes=True), 10, 0, 1, 6) grid.addLayout(self.init_preset(), 0, 0, 1, 2) - # grid.addLayout(self.init_max_mux(), 1, 0, 1, 2) - # grid.addLayout(self.init_tune(), 2, 0, 1, 2) grid.addLayout(self.init_profile(), 1, 0, 1, 2) - # grid.addLayout(self.init_pix_fmt(), 4, 0, 1, 2) grid.addLayout(self.init_tier(), 2, 0, 1, 2) + grid.addLayout(self.init_multipass(), 3, 0, 1, 2) + + breaker = QtWidgets.QHBoxLayout() + breaker_label = QtWidgets.QLabel(t("Advanced")) + breaker_label.setFont(QtGui.QFont("helvetica", 8, weight=55)) + + breaker.addWidget(get_breaker(), stretch=1) + breaker.addWidget(breaker_label, alignment=QtCore.Qt.AlignHCenter) + breaker.addWidget(get_breaker(), stretch=1) - grid.addLayout(self.init_spatial_aq(), 3, 0, 1, 2) - grid.addLayout(self.init_lookahead(), 4, 0, 1, 2) - grid.addLayout(self.init_mv_precision(), 5, 0, 1, 2) - grid.addLayout(self.init_multipass(), 6, 0, 1, 2) + grid.addLayout(breaker, 4, 0, 1, 6) + + grid.addLayout(self.init_aq(), 5, 0, 1, 2) + grid.addLayout(self.init_aq_strength(), 6, 0, 1, 2) + grid.addLayout(self.init_lookahead(), 7, 0, 1, 2) + grid.addLayout(self.init_mv_precision(), 8, 0, 1, 2) qp_line = QtWidgets.QHBoxLayout() qp_line.addLayout(self.init_vbr_target()) @@ -93,7 +108,7 @@ def __init__(self, parent, main, app: FastFlixApp): qp_line.addStretch(1) qp_line.addLayout(self.init_max_q()) - grid.addLayout(qp_line, 4, 2, 1, 4) + grid.addLayout(qp_line, 5, 2, 1, 4) advanced = QtWidgets.QHBoxLayout() advanced.addLayout(self.init_ref()) @@ -101,17 +116,16 @@ def __init__(self, parent, main, app: FastFlixApp): advanced.addLayout(self.init_b_frames()) advanced.addStretch(1) advanced.addLayout(self.init_level()) - # a.addLayout(self.init_gpu()) advanced.addStretch(1) advanced.addLayout(self.init_b_ref_mode()) advanced.addStretch(1) advanced.addLayout(self.init_metrics()) - grid.addLayout(advanced, 5, 2, 1, 4) + grid.addLayout(advanced, 6, 2, 1, 4) - grid.addLayout(self.init_dhdr10_info(), 6, 2, 1, 4) + grid.addLayout(self.init_dhdr10_info(), 7, 2, 1, 4) self.ffmpeg_level = QtWidgets.QLabel() - grid.addWidget(self.ffmpeg_level, 7, 2, 1, 4) + grid.addWidget(self.ffmpeg_level, 8, 2, 1, 4) grid.setRowStretch(9, 1) @@ -138,7 +152,7 @@ def init_preset(self): label="Preset", widget_name="preset", options=presets, - tooltip=("preset: The slower the preset, the better the compression and quality"), + tooltip="preset: The slower the preset, the better the compression and quality", connect="default", opt="preset", ) @@ -162,15 +176,6 @@ def init_profile(self): opt="profile", ) - # def init_pix_fmt(self): - # return self._add_combo_box( - # label="Bit Depth", - # tooltip="Pixel Format (requires at least 10-bit for HDR)", - # widget_name="pix_fmt", - # options=pix_fmts, - # opt="pix_fmt", - # ) - def init_tier(self): return self._add_combo_box( label="Tier", @@ -181,22 +186,21 @@ def init_tier(self): ) def init_aq(self): - # TODO change to spatial or temporal return self._add_combo_box( - label="Spatial AQ", + label="Adaptive Quantization", tooltip="", - widget_name="spatial_aq", - options=["off", "on"], - opt="spatial_aq", + widget_name="aq", + options=["off", "spatial", "temporal"], + opt="aq", ) - def init_spatial_aq(self): + def init_aq_strength(self): return self._add_combo_box( - label="Spatial AQ", + label="AQ Strength", tooltip="", - widget_name="spatial_aq", - options=["off", "on"], - opt="spatial_aq", + widget_name="aq_strength", + options=["Auto"] + [str(x) for x in range(1, 16)], + opt="aq_strength", ) def init_multipass(self): @@ -385,14 +389,6 @@ def setting_change(self, update=True): self.main.page_update() self.updating_settings = False - # def gather_q(self, group) -> Optional[str]: - # if self.mode.lower() != "bitrate": - # return None - # if self.widgets[f"{group}_q_i"].currentIndex() > 0: - # if self.widgets[f"{group}_q_p"].currentIndex() > 0 and self.widgets[f"{group}_q_b"].currentIndex() > 0: - # return f'{self.widgets[f"{group}_q_i"].currentText()}:{self.widgets[f"{group}_q_p"].currentText()}:{self.widgets[f"{group}_q_b"].currentText()}' - # return self.widgets[f"{group}_q_i"].currentText() - def update_video_encoder_settings(self): settings = NVEncCSettings( @@ -400,7 +396,8 @@ def update_video_encoder_settings(self): profile=self.widgets.profile.currentText(), tier=self.widgets.tier.currentText(), lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None, - spatial_aq=bool(self.widgets.spatial_aq.currentIndex()), + aq=self.widgets.aq.currentIndex(), + aq_strength=self.widgets.aq_strength.currentIndex(), hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"), multipass=self.widgets.multipass.currentText(), mv_precision=self.widgets.mv_precision.currentText(), diff --git a/fastflix/flix.py b/fastflix/flix.py index 35355299..9b2ac809 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -147,23 +147,6 @@ def probe(app: FastFlixApp, file: Path) -> Box: raise FlixError(result.stderr) -def determine_rotation(streams) -> Tuple[int, int]: - rotation = 0 - if "rotate" in streams.video[0].get("tags", {}): - rotation = abs(int(streams.video[0].tags.rotate)) - # elif 'side_data_list' in self.streams.video[0]: - # rots = [abs(int(x.rotation)) for x in self.streams.video[0].side_data_list if 'rotation' in x] - # rotation = rots[0] if rots else 0 - - if rotation in (90, 270): - video_width = streams.video[0].height - video_height = streams.video[0].width - else: - video_width = streams.video[0].width - video_height = streams.video[0].height - return video_width, video_height - - def parse(app: FastFlixApp, **_): data = probe(app, app.fastflix.current_video.source) if "streams" not in data: @@ -188,7 +171,6 @@ def parse(app: FastFlixApp, **_): app.fastflix.current_video.streams = streams app.fastflix.current_video.video_settings.selected_track = streams.video[0].index - app.fastflix.current_video.width, app.fastflix.current_video.height = determine_rotation(streams) app.fastflix.current_video.format = data.format app.fastflix.current_video.duration = float(data.format.get("duration", 0)) diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index a9931302..b9dd4a6b 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -98,7 +98,8 @@ class NVEncCSettings(EncoderSettings): bitrate: Optional[str] = "5000k" cqp: Optional[str] = None cq: int = 0 - spatial_aq: bool = True + aq: str = "off" + aq_strength: int = 0 lookahead: Optional[int] = None tier: str = "high" level: Optional[str] = None diff --git a/fastflix/models/video.py b/fastflix/models/video.py index e019c0b8..b2dfde04 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import uuid from pathlib import Path -from typing import List, Optional, Union +from typing import List, Optional, Union, Tuple from box import Box from pydantic import BaseModel, Field @@ -26,6 +26,27 @@ __all__ = ["VideoSettings", "Status", "Video", "Crop", "Status"] +def determine_rotation(streams, track: int = 0) -> Tuple[int, int]: + for stream in streams.video: + if int(track) == stream["index"]: + video_stream = stream + break + else: + return 0, 0 + + rotation = 0 + if "rotate" in streams.video[0].get("tags", {}): + rotation = abs(int(video_stream.tags.rotate)) + + if rotation in (90, 270): + video_width = video_stream.height + video_height = video_stream.width + else: + video_width = video_stream.width + video_height = video_stream.height + return video_width, video_height + + class Crop(BaseModel): top: int = 0 right: int = 0 @@ -99,8 +120,6 @@ def ready(self) -> bool: class Video(BaseModel): source: Path - width: int = 0 - height: int = 0 duration: Union[float, int] = 0 streams: Box = None @@ -115,6 +134,16 @@ class Video(BaseModel): status: Status = Field(default_factory=Status) uuid: str = Field(default_factory=lambda: str(uuid.uuid4())) + @property + def width(self): + w, _ = determine_rotation(self.streams, self.video_settings.selected_track) + return w + + @property + def height(self): + _, h = determine_rotation(self.streams, self.video_settings.selected_track) + return h + @property def master_display(self) -> Optional[Box]: for track in self.hdr10_streams: diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 8911853e..0d523212 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -32,7 +32,7 @@ from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.video import Status, Video, VideoSettings, Crop -from fastflix.models.queue import get_queue +from fastflix.models.queue import get_queue, save_queue from fastflix.resources import ( black_x_icon, folder_icon, @@ -374,7 +374,7 @@ def init_video_track_select(self): layout = QtWidgets.QHBoxLayout() self.widgets.video_track = QtWidgets.QComboBox() self.widgets.video_track.addItems([]) - self.widgets.video_track.currentIndexChanged.connect(lambda: self.page_update()) + self.widgets.video_track.currentIndexChanged.connect(self.video_track_update) track_label = QtWidgets.QLabel(t("Video Track")) track_label.setFixedWidth(65) @@ -1389,6 +1389,20 @@ def hdr_update(self): self.video_options.advanced.hdr_settings() self.encoder_settings_update() + def video_track_update(self): + self.loading_video = True + self.app.fastflix.current_video.video_settings.selected_track = self.widgets.video_track.currentText().split( + ":" + )[0] + self.widgets.crop.top.setText("0") + self.widgets.crop.left.setText("0") + self.widgets.crop.right.setText("0") + self.widgets.crop.bottom.setText("0") + self.widgets.scale.width.setText(str(self.app.fastflix.current_video.width)) + self.widgets.scale.height.setText(str(self.app.fastflix.current_video.height)) + self.loading_video = False + self.page_update(build_thumbnail=True) + def page_update(self, build_thumbnail=True): if not self.initialized or self.loading_video or not self.app.fastflix.current_video: return @@ -1568,8 +1582,6 @@ def conversion_complete(self, return_code): self.paused = False self.set_convert_button() - self.video_options.update_queue() - if return_code: error_message(t("There was an error during conversion and the queue has stopped"), title=t("Error")) else: @@ -1588,10 +1600,6 @@ def conversion_cancelled(self, data): self.paused = False self.set_convert_button() - self.app.fastflix.queue = get_queue() - - self.video_options.update_queue() - if self.video_options.queue.paused: self.video_options.queue.pause_resume_queue() @@ -1657,6 +1665,7 @@ def status_update(self): logger.debug(f"Updating queue from command worker") self.app.fastflix.queue = get_queue() + updated = False for video in self.app.fastflix.queue: if video.status.complete and not video.status.subtitle_fixed: if video.video_settings.subtitle_tracks and not video.video_settings.subtitle_tracks[0].disposition: @@ -1664,7 +1673,9 @@ def status_update(self): worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) worker.start() video.status.subtitle_fixed = True - # TODO Save + updated = True + if updated: + save_queue(self.app.fastflix.queue) self.video_options.update_queue() @@ -1695,8 +1706,9 @@ def run(self): while True: # Message looks like (command, video_uuid, command_uuid) status = self.status_queue.get() - if status[0] == "queue": - self.main.status_update_signal.emit() + self.main.status_update_signal.emit() + # if status[0] == "queue": + # self.main.status_update_signal.emit() if status[0] == "complete": self.main.completed.emit(0) elif status[0] == "error": diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index fa76c59c..edf036b9 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -262,13 +262,11 @@ def clear_complete(self): self.remove_item(queued_item.video) def remove_item(self, video): - # TODO Make sure this one not currently running self.app.fastflix.queue.remove(video) save_queue(self.app.fastflix.queue) self.new_source() def reload_from_queue(self, video): - # TODO Make sure this one not currently running self.main.reload_video_from_queue(video) self.app.fastflix.queue.remove(video) self.new_source() diff --git a/fastflix/widgets/profile_window.py b/fastflix/widgets/profile_window.py index 5ada6ed2..dc31cfe9 100644 --- a/fastflix/widgets/profile_window.py +++ b/fastflix/widgets/profile_window.py @@ -199,3 +199,4 @@ def delete_current_profile(self): self.main.widgets.profile_box.addItems(self.app.fastflix.config.profiles.keys()) self.main.loading_video = False self.main.widgets.profile_box.setCurrentText("Standard Profile") + self.main.widgets.convert_to.setCurrentIndex(0) From 17bf9d7a37e14547bf1dfbda2c8fa9295ab50e88 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 19:38:44 -0600 Subject: [PATCH 32/50] more language additions --- fastflix/data/languages.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index fefc21b8..9a935615 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2831,3 +2831,15 @@ Calculate PSNR and SSIM and show in the encoder output: eng: Calculate PSNR and SSIM and show in the encoder output Extracting HDR10+ metadata: eng: Extracting HDR10+ metadata +hq - High Quality, ll - Low Latency, ull - Ultra Low Latency: + eng: hq - High Quality, ll - Low Latency, ull - Ultra Low Latency +Rate Control: + eng: Rate Control +Override the preset rate-control: + eng: Override the preset rate-control +RC Lookahead: + eng: RC Lookahead +GPU: + eng: GPU +Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on: + eng: Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on From b521bbf831c4ecfcfed2d041595c538cbc9508d9 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 20:57:42 -0600 Subject: [PATCH 33/50] stuff --- fastflix/conversion_worker.py | 13 ------------- fastflix/widgets/main.py | 27 +++++++++++---------------- fastflix/widgets/video_options.py | 1 - 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 46eeedf3..4e8f1876 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -116,9 +116,6 @@ def queue_worker(gui_proc, worker_queue, status_queue, log_queue): paused = False video: Optional[Video] = None - def current_command(): - nonlocal video - def start_command(): nonlocal currently_encoding log_queue.put( @@ -154,7 +151,6 @@ def start_command(): currently_encoding = False set_status(video, errored=True) status_queue.put(("error",)) - commands_to_run = [] allow_sleep_mode() if gui_died: return @@ -226,15 +222,6 @@ def start_command(): if video: start_command() - # for command in request[2]: - # if command not in commands_to_run: - # logger.debug(t(f"Adding command to the queue for {command[4]} - {command[2]}")) - # commands_to_run.append(command) - # # else: - # # logger.debug(t(f"Command already in queue: {command[1]}")) - # if not runner.is_alive() and not paused: - # logger.debug(t("No encoding is currently in process, starting encode")) - # start_command() if request[0] == "cancel": logger.debug(t("Cancel has been requested, killing encoding")) runner.kill() diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 0d523212..8365442c 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -127,6 +127,7 @@ def __init__(self, parent, app: FastFlixApp): self.initialized = False self.loading_video = True self.scale_updating = False + self.last_thumb_hash = "" self.notifier = Notifier(self, self.app, self.app.fastflix.status_queue) self.notifier.start() @@ -1258,8 +1259,8 @@ def generate_thumbnail(self): settings["remove_hdr"] = True custom_filters = "scale='min(320\\,iw):-8'" - if self.app.fastflix.current_video.color_transfer == "arib-std-b67": - custom_filters += ",select=eq(pict_type\\,I)" + # if self.app.fastflix.current_video.color_transfer == "arib-std-b67": + custom_filters += ",select=eq(pict_type\\,I)" filters = helpers.generate_filters(custom_filters=custom_filters, **settings) @@ -1390,6 +1391,8 @@ def hdr_update(self): self.encoder_settings_update() def video_track_update(self): + if not self.app.fastflix.current_video: + return self.loading_video = True self.app.fastflix.current_video.video_settings.selected_track = self.widgets.video_track.currentText().split( ":" @@ -1406,10 +1409,16 @@ def video_track_update(self): def page_update(self, build_thumbnail=True): if not self.initialized or self.loading_video or not self.app.fastflix.current_video: return + logger.debug(f"page update {build_thumbnail}") self.last_page_update = time.time() self.video_options.refresh() self.build_commands() if build_thumbnail: + new_hash = f"{self.build_crop()}:{self.build_scale()}:{self.start_time}:{self.end_time}:{self.app.fastflix.current_video.video_settings.selected_track}:{int(self.remove_hdr)}" + if new_hash == self.last_thumb_hash: + logger.debug("No thumb change, not updating") + return + self.last_thumb_hash = new_hash self.generate_thumbnail() def close(self, no_cleanup=False, from_container=False): @@ -1430,10 +1439,6 @@ def convert_to(self): return self.widgets.convert_to.currentText().strip() return list(self.app.fastflix.encoders.keys())[0] - # @property - # def current_encoder(self): - # return self.app.fastflix.encoders[self.convert_to] - def encoding_checks(self): if not self.input_video: error_message(t("Have to select a video first")) @@ -1492,7 +1497,6 @@ def set_convert_button(self, convert=True): @reusables.log_exception("fastflix", show_traceback=False) def encode_video(self): - # from fastflix.models.queue import save_queue # TODO make sure there is a video that can be encoded if self.converting: logger.debug(t("Canceling current encode")) @@ -1509,9 +1513,6 @@ def encode_video(self): if add_current: if not self.add_to_queue(): return - # save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) - # Command looks like (video_uuid, command_uuid, command, work_dir, filename) - # Request looks like (queue command, log_dir, (commands)) requests = ["add_items", str(self.app.fastflix.log_path)] # TODO here check for videos if ready. Make shared function for ready? @@ -1707,21 +1708,15 @@ def run(self): # Message looks like (command, video_uuid, command_uuid) status = self.status_queue.get() self.main.status_update_signal.emit() - # if status[0] == "queue": - # self.main.status_update_signal.emit() if status[0] == "complete": self.main.completed.emit(0) elif status[0] == "error": - # self.main.status_update_signal.emit("|".join(status)) self.main.completed.emit(1) elif status[0] == "cancelled": self.main.cancelled.emit("|".join(status[1:])) - # self.main.status_update_signal.emit("|".join(status)) elif status[0] == "exit": try: self.terminate() finally: self.main.close_event.emit() return - # else: - # self.main.status_update_signal.emit("|".join(status)) diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py index 2c0c95ee..d7e007a4 100644 --- a/fastflix/widgets/video_options.py +++ b/fastflix/widgets/video_options.py @@ -124,7 +124,6 @@ def refresh(self): self.subtitles.refresh() self.advanced.update_settings() self.main.container.profile.update_settings() - self.debug.reset() def update_profile(self): self.current_settings.update_profile() From 2c39876f46cb4fc34f7ebe8b52b1932329fe9174 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 21:35:04 -0600 Subject: [PATCH 34/50] more fixes --- fastflix/data/languages.yaml | 6 ++++++ fastflix/flix.py | 7 +------ fastflix/widgets/background_tasks.py | 11 +++++------ fastflix/widgets/main.py | 23 ++++++++++++----------- fastflix/widgets/panels/queue_panel.py | 8 +++----- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 9a935615..b2909878 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2843,3 +2843,9 @@ GPU: eng: GPU Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on: eng: Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on +Fast first pass: + eng: Fast first pass +Set speed to 4 for first pass: + eng: Set speed to 4 for first pass +AQ Strength: + eng: AQ Strength diff --git a/fastflix/flix.py b/fastflix/flix.py index 9b2ac809..45342b1f 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -430,12 +430,7 @@ def parse_hdr_details(app: FastFlixApp, **_): def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): - if ( - not app.fastflix.current_video.master_display - or not config.hdr10plus_parser - or not config.hdr10plus_parser.exists() - ): - + if not config.hdr10plus_parser or not config.hdr10plus_parser.exists(): return hdr10plus_streams = [] diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 68a93c3e..9243c4a1 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -123,13 +123,12 @@ def run(self): output = self.app.fastflix.current_video.work_path / "metadata.json" - if ( - self.app.fastflix.current_video.video_settings.selected_track - not in self.app.fastflix.current_video.hdr10_plus - ): + track = self.app.fastflix.current_video.video_settings.selected_track + if track not in self.app.fastflix.current_video.hdr10_plus: self.main.thread_logging_signal.emit( - "WARNING:Selected video track not detected to have HDR10+ data, trying anyways" + "WARNING:Selected video track not detected to have HDR10+ data, selecting first track that does" ) + track = self.app.fastflix.current_video.hdr10_plus[0] self.main.thread_logging_signal.emit(f'INFO:{t("Extracting HDR10+ metadata")} to {output}') @@ -142,7 +141,7 @@ def run(self): "-i", str(self.app.fastflix.current_video.source).replace("\\", "/"), "-map", - f"0:{self.app.fastflix.current_video.video_settings.selected_track}", + f"0:{track}", "-c:v", "copy", "-vbsf", diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 8365442c..d2182b99 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1207,11 +1207,17 @@ def update_video_info(self): @property def video_track(self) -> int: - return int(self.widgets.video_track.currentIndex()) + return self.widgets.video_track.currentIndex() @property def original_video_track(self) -> int: - return int(self.widgets.video_track.currentText().split(":", 1)[0]) + if not self.app.fastflix.current_video or not self.widgets.video_track.currentText(): + return 0 + try: + return int(self.widgets.video_track.currentText().split(":", 1)[0]) + except Exception: + logger.exception("Could not get original_video_track") + return 0 @property def pix_fmt(self) -> str: @@ -1258,9 +1264,9 @@ def generate_thumbnail(self): ): settings["remove_hdr"] = True - custom_filters = "scale='min(320\\,iw):-8'" - # if self.app.fastflix.current_video.color_transfer == "arib-std-b67": - custom_filters += ",select=eq(pict_type\\,I)" + custom_filters = "scale='min(720\\,iw):-8'" + if self.app.fastflix.current_video.color_transfer == "arib-std-b67": + custom_filters += ",select=eq(pict_type\\,I)" filters = helpers.generate_filters(custom_filters=custom_filters, **settings) @@ -1335,7 +1341,6 @@ def get_all_settings(self): start_time=self.start_time, end_time=end_time, selected_track=self.original_video_track, - # stream_track=self.video_track, fast_seek=self.fast_time, rotate=self.rotation_to_transpose(), vertical_flip=v_flip, @@ -1394,9 +1399,7 @@ def video_track_update(self): if not self.app.fastflix.current_video: return self.loading_video = True - self.app.fastflix.current_video.video_settings.selected_track = self.widgets.video_track.currentText().split( - ":" - )[0] + self.app.fastflix.current_video.video_settings.selected_track = self.original_video_track self.widgets.crop.top.setText("0") self.widgets.crop.left.setText("0") self.widgets.crop.right.setText("0") @@ -1409,14 +1412,12 @@ def video_track_update(self): def page_update(self, build_thumbnail=True): if not self.initialized or self.loading_video or not self.app.fastflix.current_video: return - logger.debug(f"page update {build_thumbnail}") self.last_page_update = time.time() self.video_options.refresh() self.build_commands() if build_thumbnail: new_hash = f"{self.build_crop()}:{self.build_scale()}:{self.start_time}:{self.end_time}:{self.app.fastflix.current_video.video_settings.selected_track}:{int(self.remove_hdr)}" if new_hash == self.last_thumb_hash: - logger.debug("No thumb change, not updating") return self.last_thumb_hash = new_hash self.generate_thumbnail() diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index edf036b9..2817a450 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -3,6 +3,7 @@ import copy import sys +import logging import reusables from box import Box @@ -24,6 +25,8 @@ from fastflix.shared import no_border, open_folder from fastflix.widgets.panels.abstract_list import FlixList +logger = logging.getLogger("fastflix") + done_actions = { "linux": { "shutdown": 'shutdown -h 1 "FastFlix conversion complete, shutting down"', @@ -139,11 +142,6 @@ def __init__(self, parent, video: Video, index, first=False): right_buttons.addWidget(self.widgets.cancel_button) grid.addLayout(right_buttons, 0, 10, alignment=QtCore.Qt.AlignRight) - # grid.addLayout(disposition_layout, 0, 4) - # grid.addWidget(self.widgets.burn_in, 0, 5) - # grid.addLayout(self.init_language(), 0, 6) - # # grid.addWidget(self.init_extract_button(), 0, 6) - # grid.addWidget(self.widgets.enable_check, 0, 8) self.setLayout(grid) self.loading = False From 8aaba50402a0ecf2836a3b707ab308cfc1a7024b Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 2 Feb 2021 21:52:22 -0600 Subject: [PATCH 35/50] * Adding #166 More robust queue that is recoverable --- CHANGES | 4 +++- fastflix/models/queue.py | 2 ++ fastflix/models/video.py | 9 +++++++++ fastflix/widgets/main.py | 3 --- fastflix/widgets/panels/queue_panel.py | 22 +++++++++++++++++++++- 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 371f706a..109369cc 100644 --- a/CHANGES +++ b/CHANGES @@ -3,11 +3,13 @@ ## Version 4.2.0 * Adding #109 NVENC HEVC support based on FFmpeg -* Adding NVEenC initial trial for HEVC +* Adding NVEenC encoder for HEVC +* Adding #166 More robust queue that is recoverable * Adding ability to extract HDR10+ metadata if hdr10plus_parser is detected on path * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) * Adding Windows 10 notification for queue complete success * Adding #194 fast two pass encoding (thanks to Ugurtan) +* Fixing #176 Unable to change queue order or delete task from queue since 4.1.0 (thanks to Etz) * Fixing #185 need to specify channel layout when downmixing (thanks to Ugurtan) * Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) * Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz) diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index f0612cdd..c0e39ffa 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -17,6 +17,8 @@ def get_queue(lockless=False) -> List[Video]: + if not queue_file.exists(): + return [] if lockless: loaded = Box.from_yaml(filename=queue_file) diff --git a/fastflix/models/video.py b/fastflix/models/video.py index b2dfde04..93ab1823 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -117,6 +117,15 @@ class Status(BaseModel): def ready(self) -> bool: return not self.success and not self.error and not self.complete and not self.running and not self.cancelled + def clear(self): + self.success = False + self.error = False + self.complete = False + self.running = False + self.cancelled = False + self.subtitle_fixed = False + self.current_command = 0 + class Video(BaseModel): source: Path diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index d2182b99..0b21133f 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -283,9 +283,6 @@ def init_video_area(self): spacer = QtWidgets.QLabel() spacer.setFixedHeight(2) layout.addWidget(spacer) - # layout.addLayout(self.init_button_menu()) - # layout.addWidget(self.video_path_widget) - # layout.addLayout(self.init_encoder_drop_down()) output_layout = QtWidgets.QHBoxLayout() diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index 2817a450..bb4135dc 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -22,7 +22,7 @@ up_arrow_icon, undo_icon, ) -from fastflix.shared import no_border, open_folder +from fastflix.shared import no_border, open_folder, message from fastflix.widgets.panels.abstract_list import FlixList logger = logging.getLogger("fastflix") @@ -240,6 +240,26 @@ def __init__(self, parent, app: FastFlixApp): top_layout.addWidget(self.clear_queue, QtCore.Qt.AlignRight) super().__init__(app, parent, t("Queue"), "queue", top_row_layout=top_layout) + self.queue_startup_check() + + def queue_startup_check(self): + self.app.fastflix.queue = get_queue() + remove_vids = [] + for video in self.app.fastflix.queue: + if video.status.running: + video.status.clear() + if video.status.complete: + remove_vids.append(video) + + for video in remove_vids: + self.app.fastflix.queue.remove(video) + + if self.app.fastflix.queue: + message( + "Not all items in the queue were completed\n" + "They have been added back into the queue for your convenience" + ) + self.new_source() def reorder(self, update=True): super().reorder(update=update) From 9d6989ff8a2713315b7ab61c9836763acd23faad Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 3 Feb 2021 16:11:37 -0600 Subject: [PATCH 36/50] Adding resets for main page items, slider for picking thumbnail time --- fastflix/data/styles/default.qss | 52 ++++++ fastflix/encoders/common/helpers.py | 7 +- fastflix/encoders/hevc_x265/settings_panel.py | 2 +- .../encoders/nvencc_hevc/settings_panel.py | 2 +- fastflix/flix.py | 5 +- fastflix/models/video.py | 2 +- fastflix/widgets/main.py | 163 ++++++++++++++---- 7 files changed, 190 insertions(+), 43 deletions(-) diff --git a/fastflix/data/styles/default.qss b/fastflix/data/styles/default.qss index 2055fd5a..29dec831 100644 --- a/fastflix/data/styles/default.qss +++ b/fastflix/data/styles/default.qss @@ -56,3 +56,55 @@ QGroupBox { margin-top: 2px; border-bottom: 2px solid #ddd; } + +/* QSlider::groove:horizontal {*/ +/*border: 1px solid #bbb;*/ +/*background: none;*/ +/*height: 10px;*/ +/*border-radius: 4px;*/ +/*}*/ + +/*QSlider::sub-page:horizontal {*/ +/*background: none;*/ +/*border: 1px solid #bbb;*/ +/*height: 10px;*/ +/*border-radius: 4px;*/ +/*}*/ + +/*QSlider::add-page:horizontal {*/ +/*background: none;*/ +/*border: 1px solid #bbb;*/ +/*height: 10px;*/ +/*border-radius: 4px;*/ +/*}*/ + +/*QSlider::handle:horizontal {*/ +/*background: qlineargradient(x1:0, y1:0, x2:1, y2:1,stop:0 #eee, stop:1 #aaa);*/ +/*border: 1px solid #777;*/ +/*width: 13px;*/ +/*margin-top: -2px;*/ +/*margin-bottom: -2px;*/ +/*border-radius: 4px;*/ +/*}*/ + +/*QSlider::handle:horizontal:hover {*/ +/*background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #fff, stop:1 #ddd);*/ +/*border: 1px solid #444;*/ +/*border-radius: 4px;*/ +/*}*/ + +/*QSlider::sub-page:horizontal:disabled {*/ +/*background: #bbb;*/ +/*border-color: #999;*/ +/*}*/ + +/*QSlider::add-page:horizontal:disabled {*/ +/*background: #eee;*/ +/*border-color: #999;*/ +/*}*/ + +/*QSlider::handle:horizontal:disabled {*/ +/*background: #eee;*/ +/*border: 1px solid #aaa;*/ +/*border-radius: 4px;*/ +/*}*/ diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index 2932834c..b282fe06 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import uuid from pathlib import Path -from typing import List, Tuple, Union, Optional +from typing import List, Tuple, Union, Optional, Dict import reusables from pydantic import BaseModel, Field @@ -103,7 +103,7 @@ def generate_ending( def generate_filters( selected_track, source=None, - crop: Optional[Crop] = None, + crop: Optional[Dict] = None, scale=None, scale_filter="lanczos", remove_hdr=False, @@ -113,6 +113,7 @@ def generate_filters( burn_in_subtitle_track=None, burn_in_subtitle_type=None, custom_filters=None, + start_filters=None, raw_filters=False, deinterlace=False, tone_map: str = "hable", @@ -124,6 +125,8 @@ def generate_filters( ): filter_list = [] + if start_filters: + filter_list.append(start_filters) if deinterlace: filter_list.append(f"yadif") if crop: diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index 38f3b29d..937944f5 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -521,7 +521,7 @@ def hdr_opts(): def new_source(self): super().new_source() self.setting_change() - if self.app.fastflix.current_video.hdr10_plus is not None: + if self.app.fastflix.current_video.hdr10_plus: self.extract_button.show() else: self.extract_button.hide() diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 1a59ecbe..59848cf7 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -434,7 +434,7 @@ def set_mode(self, x): def new_source(self): super().new_source() - if self.app.fastflix.current_video.hdr10_plus is not None: + if self.app.fastflix.current_video.hdr10_plus: self.extract_button.show() else: self.extract_button.hide() diff --git a/fastflix/flix.py b/fastflix/flix.py index 45342b1f..54b77261 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -214,11 +214,8 @@ def extract_attachment(ffmpeg: Path, source: Path, stream: int, work_dir: Path, def generate_thumbnail_command( config: Config, source: Path, output: Path, filters: str, start_time: float = 0, input_track: int = 0 ) -> str: - start = "" - if start_time: - start = f"-ss {start_time}" return ( - f'"{config.ffmpeg}" {start} -loglevel error -i "{unixy(source)}" ' + f'"{config.ffmpeg}" -ss {start_time} -loglevel error -i "{unixy(source)}" ' f" {filters} -an -y -map_metadata -1 -map 0:{input_track} " f'-vframes 1 "{unixy(output)}" ' ) diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 93ab1823..1e36064f 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -137,7 +137,7 @@ class Video(BaseModel): interlaced: bool = True hdr10_streams: List[Box] = Field(default_factory=list) - hdr10_plus: Optional[List[int]] = None + hdr10_plus: List[int] = Field(default_factory=list) video_settings: VideoSettings = Field(default_factory=VideoSettings) status: Status = Field(default_factory=Status) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 0b21133f..2961f56e 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -42,6 +42,7 @@ settings_icon, video_add_icon, video_playlist_icon, + undo_icon, ) from fastflix.shared import error_message, message, time_to_number, yes_no_message from fastflix.windows_tools import show_windows_notification @@ -93,6 +94,8 @@ class MainWidgets(BaseModel): remove_hdr: QtWidgets.QCheckBox = None video_title: QtWidgets.QLineEdit = None profile_box: QtWidgets.QComboBox = None + thumb_time: QtWidgets.QSlider = None + thumb_key: QtWidgets.QCheckBox = None class Config: arbitrary_types_allowed = True @@ -150,7 +153,7 @@ def __init__(self, parent, app: FastFlixApp): self.buttons = [] - self.thumb_file = Path(self.app.fastflix.config.work_path, "thumbnail_preview.png") + self.thumb_file = Path(self.app.fastflix.config.work_path, "thumbnail_preview.jpg") self.video_options = VideoOptions( self, @@ -174,13 +177,14 @@ def __init__(self, parent, app: FastFlixApp): self.grid.addLayout(self.init_top_bar(), 0, 0, 1, 14) self.grid.addLayout(self.init_video_area(), 1, 0, 6, 6) - self.grid.addLayout(self.init_scale_and_crop(), 1, 6, 5, 4) + self.grid.addLayout(self.init_scale_and_crop(), 1, 6, 6, 4) self.grid.addWidget(self.init_preview_image(), 1, 10, 5, 4, (QtCore.Qt.AlignTop | QtCore.Qt.AlignRight)) + self.grid.addLayout(self.init_thumb_time_selector(), 6, 10, 1, 4, (QtCore.Qt.AlignTop | QtCore.Qt.AlignRight)) spacer = QtWidgets.QLabel() spacer.setFixedHeight(5) - self.grid.addWidget(spacer, 6, 0, 1, 14) - self.grid.addWidget(self.video_options, 7, 0, 10, 14) + self.grid.addWidget(spacer, 7, 0, 1, 14) + self.grid.addWidget(self.video_options, 8, 0, 10, 14) self.grid.setSpacing(5) self.paused = False @@ -252,6 +256,28 @@ def init_top_bar(self): return top_bar + def init_thumb_time_selector(self): + layout = QtWidgets.QHBoxLayout() + + self.widgets.thumb_key = QtWidgets.QCheckBox("Keyframe") + self.widgets.thumb_key.setChecked(False) + self.widgets.thumb_key.clicked.connect(self.thumb_time_change) + + self.widgets.thumb_time = QtWidgets.QSlider(QtCore.Qt.Horizontal) + self.widgets.thumb_time.setMinimum(1) + self.widgets.thumb_time.setMaximum(10) + self.widgets.thumb_time.setValue(2) + self.widgets.thumb_time.setTickPosition(QtWidgets.QSlider.TicksBelow) + self.widgets.thumb_time.setTickInterval(1) + self.widgets.thumb_time.setAutoFillBackground(False) + self.widgets.thumb_time.sliderReleased.connect(self.thumb_time_change) + layout.addWidget(self.widgets.thumb_key) + layout.addWidget(self.widgets.thumb_time) + return layout + + def thumb_time_change(self): + self.generate_thumbnail() + def get_temp_work_path(self): new_temp = self.app.fastflix.config.work_path / f"temp_{secrets.token_hex(12)}" if new_temp.exists(): @@ -274,7 +300,7 @@ def pause_resume(self): logger.info("Resuming FFmpeg conversion") def config_update(self): - self.thumb_file = Path(self.app.fastflix.config.work_path, "thumbnail_preview.png") + self.thumb_file = Path(self.app.fastflix.config.work_path, "thumbnail_preview.jpg") self.change_output_types() self.page_update(build_thumbnail=True) @@ -356,7 +382,6 @@ def init_video_area(self): transform_layout.addLayout(extra_details_layout) layout.addLayout(transform_layout) - layout.addWidget(self.init_start_time()) layout.addStretch() return layout @@ -365,6 +390,7 @@ def init_scale_and_crop(self): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.init_scale()) layout.addWidget(self.init_crop()) + layout.addWidget(self.init_start_time()) layout.addStretch() return layout @@ -510,12 +536,27 @@ def current_encoder(self): def init_start_time(self): group_box = QtWidgets.QGroupBox() group_box.setStyleSheet("QGroupBox{padding-top:18px; margin-top:-18px}") - self.widgets.start_time, layout = self.build_hoz_int_field( - f"{t('Start')} ", right_stretch=False, left_stretch=True, time_field=True + + layout = QtWidgets.QHBoxLayout() + + reset = QtWidgets.QPushButton(QtGui.QIcon(undo_icon), "") + reset.setIconSize(QtCore.QSize(10, 10)) + reset.clicked.connect(self.reset_time) + self.buttons.append(reset) + layout.addWidget(reset) + + self.widgets.start_time, start_layout = self.build_hoz_int_field( + f"{t('Start')} ", + right_stretch=False, + left_stretch=True, + time_field=True, ) - self.widgets.end_time, layout = self.build_hoz_int_field( - f" {t('End')} ", left_stretch=True, right_stretch=True, layout=layout, time_field=True + self.widgets.end_time, end_layout = self.build_hoz_int_field( + f" {t('End')} ", left_stretch=True, right_stretch=True, time_field=True ) + layout.addLayout(start_layout) + layout.addLayout(end_layout) + self.widgets.start_time.textChanged.connect(lambda: self.page_update()) self.widgets.end_time.textChanged.connect(lambda: self.page_update()) self.widgets.fast_time = QtWidgets.QComboBox() @@ -532,21 +573,38 @@ def init_start_time(self): group_box.setLayout(layout) return group_box + def reset_time(self): + self.widgets.start_time.setText(self.number_to_time(0)) + self.widgets.end_time.setText(self.number_to_time(self.app.fastflix.current_video.duration)) + def init_scale(self): scale_area = QtWidgets.QGroupBox(self) scale_area.setFont(self.app.font()) scale_area.setStyleSheet("QGroupBox{padding-top:15px; margin-top:-18px}") scale_layout = QtWidgets.QVBoxLayout() - self.widgets.scale.width, new_scale_layout = self.build_hoz_int_field(f"{t('Width')} ", right_stretch=False) - self.widgets.scale.height, new_scale_layout, lb, rb = self.build_hoz_int_field( - f" {t('Height')} ", left_stretch=False, layout=new_scale_layout, return_buttons=True + main_row = QtWidgets.QHBoxLayout() + + reset = QtWidgets.QPushButton(QtGui.QIcon(undo_icon), "") + reset.setIconSize(QtCore.QSize(10, 10)) + reset.clicked.connect(self.reset_scales) + self.buttons.append(reset) + main_row.addWidget(reset, alignment=(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)) + main_row.addStretch(1) + + self.widgets.scale.width, width_layout = self.build_hoz_int_field(f"{t('Width')} ") + self.widgets.scale.height, height_layout, lb, rb = self.build_hoz_int_field( + f" {t('Height')} ", return_buttons=True ) self.widgets.scale.height.setDisabled(True) self.widgets.scale.height.setText("Auto") lb.setDisabled(True) rb.setDisabled(True) - QtWidgets.QPushButton() + + main_row.addLayout(width_layout) + main_row.addLayout(height_layout) + main_row.addWidget(QtWidgets.QLabel(" ")) + main_row.addStretch(1) # TODO scale 0 error @@ -555,24 +613,29 @@ def init_scale(self): bottom_row = QtWidgets.QHBoxLayout() self.widgets.scale.keep_aspect = QtWidgets.QCheckBox(t("Keep aspect ratio")) - self.widgets.scale.keep_aspect.setMaximumHeight(40) + # self.widgets.scale.keep_aspect.setMaximumHeight(40) self.widgets.scale.keep_aspect.setChecked(True) self.widgets.scale.keep_aspect.toggled.connect(lambda: self.toggle_disable((self.widgets.scale.height, lb, rb))) self.widgets.scale.keep_aspect.toggled.connect(lambda: self.keep_aspect_update()) - label = QtWidgets.QLabel(t("Scale"), alignment=(QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight)) + label = QtWidgets.QLabel(t("Scale")) label.setStyleSheet("QLabel{color:#777}") label.setMaximumHeight(40) - bottom_row.addWidget(self.widgets.scale.keep_aspect, alignment=QtCore.Qt.AlignCenter) + bottom_row.addWidget(self.widgets.scale.keep_aspect, alignment=(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)) + bottom_row.addWidget(label, alignment=(QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight)) - scale_layout.addLayout(new_scale_layout) - bottom_row.addWidget(label) + scale_layout.addLayout(main_row) scale_layout.addLayout(bottom_row) scale_area.setLayout(scale_layout) - return scale_area + def reset_scales(self): + self.loading_video = True + self.widgets.scale.width.setText(str(self.app.fastflix.current_video.width)) + self.loading_video = False + self.widgets.scale.height.setText(str(self.app.fastflix.current_video.height)) + def init_crop(self): crop_box = QtWidgets.QGroupBox() crop_box.setStyleSheet("QGroupBox{padding-top:17px; margin-top:-18px}") @@ -600,7 +663,15 @@ def init_crop(self): auto_crop.clicked.connect(self.get_auto_crop) self.buttons.append(auto_crop) + reset = QtWidgets.QPushButton(QtGui.QIcon(undo_icon), "") + reset.setIconSize(QtCore.QSize(10, 10)) + reset.clicked.connect(self.reset_crop) + self.buttons.append(reset) + # crop_bottom_layout.addWidget(label) + l1 = QtWidgets.QVBoxLayout() + l1.addWidget(reset, alignment=(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)) + l2 = QtWidgets.QVBoxLayout() l2.addWidget(auto_crop, alignment=(QtCore.Qt.AlignTop | QtCore.Qt.AlignRight)) l2.addWidget(label, alignment=(QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight)) @@ -609,12 +680,21 @@ def init_crop(self): crop_layout.addLayout(crop_hz_layout) crop_layout.addLayout(crop_bottom_layout) outer = QtWidgets.QHBoxLayout() + outer.addLayout(l1) outer.addLayout(crop_layout) outer.addLayout(l2) crop_box.setLayout(outer) return crop_box + def reset_crop(self): + self.loading_video = True + self.widgets.crop.top.setText("0") + self.widgets.crop.left.setText("0") + self.widgets.crop.right.setText("0") + self.loading_video = False + self.widgets.crop.bottom.setText("0") + @staticmethod def toggle_disable(widget_list): for widget in widget_list: @@ -644,7 +724,7 @@ def build_hoz_int_field( layout = QtWidgets.QHBoxLayout() layout.setSpacing(0) if left_stretch: - layout.addStretch() + layout.addStretch(1) layout.addWidget(QtWidgets.QLabel(name)) minus_button = QtWidgets.QPushButton("-") minus_button.setAutoRepeat(True) @@ -674,7 +754,7 @@ def build_hoz_int_field( layout.addWidget(widget) layout.addWidget(plus_button) if right_stretch: - layout.addStretch() + layout.addStretch(1) if return_buttons: return widget, layout, minus_button, plus_button return widget, layout @@ -1066,8 +1146,14 @@ def reload_video_from_queue(self, video: Video): self.app.fastflix.current_video = video self.input_video = video.source + hdr10_indexes = (x.index for x in self.app.fastflix.current_video.hdr10_streams) + text_video_tracks = [ - f'{x.index}: {t("codec")} {x.codec_name} - {x.get("pix_fmt")} - {t("profile")} {x.get("profile")}' + ( + f'{x.index}: {t("codec")} {x.codec_name} - {x.get("pix_fmt")} - {t("profile")} {x.get("profile")}' + f'{" - HDR10" if x.index in hdr10_indexes else ""}' + f'{" | HDR10+" if x.index in self.app.fastflix.current_video.hdr10_plus else ""}' + ) for x in self.app.fastflix.current_video.streams.video ] self.widgets.video_track.clear() @@ -1144,8 +1230,14 @@ def update_video_info(self): self.clear_current_video() return + hdr10_indexes = (x.index for x in self.app.fastflix.current_video.hdr10_streams) + text_video_tracks = [ - f'{x.index}: {t("codec")} {x.codec_name} - {x.get("pix_fmt")} - {t("profile")} {x.get("profile")}' + ( + f'{x.index}: {t("codec")} {x.codec_name} - {x.get("pix_fmt")} - {t("profile")} {x.get("profile")}' + f'{" - HDR10" if x.index in hdr10_indexes else ""}' + f'{" | HDR10+" if x.index in self.app.fastflix.current_video.hdr10_plus else ""}' + ) for x in self.app.fastflix.current_video.streams.video ] self.widgets.video_track.clear() @@ -1248,6 +1340,11 @@ def copy_chapters(self) -> bool: def remove_hdr(self) -> bool: return self.widgets.remove_hdr.isChecked() + @property + def preview_place(self) -> Union[float, int]: + ticks = self.app.fastflix.current_video.duration / 10 + return (self.widgets.thumb_time.value() - 1) * ticks + @reusables.log_exception("fastflix", show_traceback=False) def generate_thumbnail(self): if not self.input_video or self.loading_video: @@ -1262,15 +1359,13 @@ def generate_thumbnail(self): settings["remove_hdr"] = True custom_filters = "scale='min(720\\,iw):-8'" - if self.app.fastflix.current_video.color_transfer == "arib-std-b67": - custom_filters += ",select=eq(pict_type\\,I)" - - filters = helpers.generate_filters(custom_filters=custom_filters, **settings) + # if self.app.fastflix.current_video.color_transfer == "arib-std-b67": + # custom_filters += ",select=eq(pict_type\\,I)" - preview_place = ( - self.app.fastflix.current_video.duration // 10 - if self.app.fastflix.current_video.video_settings.start_time == 0 - else self.app.fastflix.current_video.video_settings.start_time + filters = helpers.generate_filters( + start_filters="select=eq(pict_type\\,I)" if self.widgets.thumb_key.isChecked() else None, + custom_filters=custom_filters, + **settings, ) thumb_command = generate_thumbnail_command( @@ -1278,7 +1373,7 @@ def generate_thumbnail(self): source=self.input_video, output=self.thumb_file, filters=filters, - start_time=preview_place, + start_time=self.preview_place, input_track=self.app.fastflix.current_video.video_settings.selected_track, ) try: @@ -1413,7 +1508,7 @@ def page_update(self, build_thumbnail=True): self.video_options.refresh() self.build_commands() if build_thumbnail: - new_hash = f"{self.build_crop()}:{self.build_scale()}:{self.start_time}:{self.end_time}:{self.app.fastflix.current_video.video_settings.selected_track}:{int(self.remove_hdr)}" + new_hash = f"{self.build_crop()}:{self.build_scale()}:{self.start_time}:{self.end_time}:{self.app.fastflix.current_video.video_settings.selected_track}:{int(self.remove_hdr)}:{self.preview_place}" if new_hash == self.last_thumb_hash: return self.last_thumb_hash = new_hash From 4ae2e5085651728339a29b0aa516dc9fbaf7c4c4 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 3 Feb 2021 21:47:05 -0600 Subject: [PATCH 37/50] * Fixing queue up / down buttons * cleaning up title options --- fastflix/shared.py | 2 +- fastflix/widgets/main.py | 4 ++-- fastflix/widgets/panels/queue_panel.py | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/fastflix/shared.py b/fastflix/shared.py index dbbec3cf..43d22b51 100644 --- a/fastflix/shared.py +++ b/fastflix/shared.py @@ -227,7 +227,7 @@ def clean_logs(signal, app, **_): except UnicodeDecodeError: pass else: - if len(condensed) < len(original): + if (len(condensed) + 100) < len(original): logger.debug(f"Compressed {file.name} from {len(original)} characters to {len(condensed)}") file.write_text(condensed, encoding="utf-8") if is_old: diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 2961f56e..d80d8bc6 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1150,7 +1150,7 @@ def reload_video_from_queue(self, video: Video): text_video_tracks = [ ( - f'{x.index}: {t("codec")} {x.codec_name} - {x.get("pix_fmt")} - {t("profile")} {x.get("profile")}' + f'{x.index}: {x.codec_name} {x.get("bit_depth", "8")}-bit {x.get("color_primaries")}' f'{" - HDR10" if x.index in hdr10_indexes else ""}' f'{" | HDR10+" if x.index in self.app.fastflix.current_video.hdr10_plus else ""}' ) @@ -1234,7 +1234,7 @@ def update_video_info(self): text_video_tracks = [ ( - f'{x.index}: {t("codec")} {x.codec_name} - {x.get("pix_fmt")} - {t("profile")} {x.get("profile")}' + f'{x.index}: {x.codec_name} {x.get("bit_depth", "8")}-bit {x.get("color_primaries")}' f'{" - HDR10" if x.index in hdr10_indexes else ""}' f'{" | HDR10+" if x.index in self.app.fastflix.current_video.hdr10_plus else ""}' ) diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index bb4135dc..ab78152e 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -264,6 +264,12 @@ def queue_startup_check(self): def reorder(self, update=True): super().reorder(update=update) self.app.fastflix.queue = [track.video for track in self.tracks] + for track in self.tracks: + track.widgets.up_button.setDisabled(False) + track.widgets.down_button.setDisabled(False) + if self.tracks: + self.tracks[0].widgets.up_button.setDisabled(True) + self.tracks[-1].widgets.down_button.setDisabled(True) save_queue(self.app.fastflix.queue) def new_source(self): @@ -272,6 +278,9 @@ def new_source(self): self.tracks = [] for i, video in enumerate(self.app.fastflix.queue, start=1): self.tracks.append(EncodeItem(self, video, index=i)) + if self.tracks: + self.tracks[0].widgets.up_button.setDisabled(True) + self.tracks[-1].widgets.down_button.setDisabled(True) super()._new_source(self.tracks) def clear_complete(self): From 588702cdacb96b5da38811b03a27b6e967fd6dfd Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 3 Feb 2021 23:12:27 -0600 Subject: [PATCH 38/50] Fixing sending back from queue for correct track --- fastflix/widgets/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index d80d8bc6..fb3e2692 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1147,7 +1147,6 @@ def reload_video_from_queue(self, video: Video): self.input_video = video.source hdr10_indexes = (x.index for x in self.app.fastflix.current_video.hdr10_streams) - text_video_tracks = [ ( f'{x.index}: {x.codec_name} {x.get("bit_depth", "8")}-bit {x.get("color_primaries")}' @@ -1159,6 +1158,12 @@ def reload_video_from_queue(self, video: Video): self.widgets.video_track.clear() self.widgets.video_track.addItems(text_video_tracks) + selected_track = 0 + for track in self.app.fastflix.current_video.streams.video: + if track.index == self.app.fastflix.current_video.video_settings.selected_track: + selected_track = track.index + self.widgets.video_track.setCurrentIndex(selected_track) + end_time = self.app.fastflix.current_video.video_settings.end_time or video.duration if self.app.fastflix.current_video.video_settings.crop: @@ -1488,7 +1493,7 @@ def hdr_update(self): self.encoder_settings_update() def video_track_update(self): - if not self.app.fastflix.current_video: + if not self.app.fastflix.current_video or self.loading_video: return self.loading_video = True self.app.fastflix.current_video.video_settings.selected_track = self.original_video_track From 6d9e9a12226f9114bfa212f8913b122d37ebfc78 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 4 Feb 2021 19:51:00 -0600 Subject: [PATCH 39/50] lots of small fixes --- .../encoders/nvencc_hevc/command_builder.py | 5 ++- .../encoders/nvencc_hevc/settings_panel.py | 32 +++++++++---------- fastflix/flix.py | 7 ++-- fastflix/models/encode.py | 2 +- fastflix/models/queue.py | 2 +- fastflix/widgets/main.py | 16 ++++------ fastflix/widgets/panels/queue_panel.py | 2 +- 7 files changed, 31 insertions(+), 35 deletions(-) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 22fa5099..666dc05d 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -144,8 +144,7 @@ def build(fastflix: FastFlix): logger.warning("Could not get stream ID from source, the proper video track may not be selected!") stream_id = None - if video.current_video_stream.bit_depth > 8 and settings.profile != "main": - logger.warning("Profile should be set to 'main' for 8 bit videos") + profile = "main" if video.current_video_stream.bit_depth == 8 else "main10" aq = "--no-aq" if settings.aq.lower() == "spatial": @@ -180,7 +179,7 @@ def build(fastflix: FastFlix): "--preset", settings.preset, "--profile", - settings.profile, + profile, "--tier", settings.tier, (f"--lookahead {settings.lookahead}" if settings.lookahead else ""), diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 59848cf7..7c365768 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -80,9 +80,10 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self._add_custom(title="Custom NVEncC options", disable_both_passes=True), 10, 0, 1, 6) grid.addLayout(self.init_preset(), 0, 0, 1, 2) - grid.addLayout(self.init_profile(), 1, 0, 1, 2) - grid.addLayout(self.init_tier(), 2, 0, 1, 2) - grid.addLayout(self.init_multipass(), 3, 0, 1, 2) + # grid.addLayout(self.init_profile(), 1, 0, 1, 2) + grid.addLayout(self.init_tier(), 1, 0, 1, 2) + grid.addLayout(self.init_multipass(), 2, 0, 1, 2) + grid.addLayout(self.init_lookahead(), 3, 0, 1, 2) breaker = QtWidgets.QHBoxLayout() breaker_label = QtWidgets.QLabel(t("Advanced")) @@ -96,8 +97,7 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_aq(), 5, 0, 1, 2) grid.addLayout(self.init_aq_strength(), 6, 0, 1, 2) - grid.addLayout(self.init_lookahead(), 7, 0, 1, 2) - grid.addLayout(self.init_mv_precision(), 8, 0, 1, 2) + grid.addLayout(self.init_mv_precision(), 7, 0, 1, 2) qp_line = QtWidgets.QHBoxLayout() qp_line.addLayout(self.init_vbr_target()) @@ -166,15 +166,15 @@ def init_tune(self): opt="tune", ) - def init_profile(self): - # TODO auto - return self._add_combo_box( - label="Profile_encoderopt", - widget_name="profile", - tooltip="Enforce an encode profile", - options=["main", "main10", "main444"], - opt="profile", - ) + # def init_profile(self): + # # TODO auto + # return self._add_combo_box( + # label="Profile_encoderopt", + # widget_name="profile", + # tooltip="Enforce an encode profile", + # options=["main", "main10"], + # opt="profile", + # ) def init_tier(self): return self._add_combo_box( @@ -393,10 +393,10 @@ def update_video_encoder_settings(self): settings = NVEncCSettings( preset=self.widgets.preset.currentText().split("-")[0].strip(), - profile=self.widgets.profile.currentText(), + # profile=self.widgets.profile.currentText(), tier=self.widgets.tier.currentText(), lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None, - aq=self.widgets.aq.currentIndex(), + aq=self.widgets.aq.currentText(), aq_strength=self.widgets.aq_strength.currentIndex(), hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"), multipass=self.widgets.multipass.currentText(), diff --git a/fastflix/flix.py b/fastflix/flix.py index 54b77261..3364964e 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -421,9 +421,10 @@ def parse_hdr_details(app: FastFlixApp, **_): except Exception: logger.exception(f"Unexpected error while processing master-display from {streams.video[0]}") else: - app.fastflix.current_video.hdr10_streams.append( - Box(index=video_stream.index, master_display=master_display, cll=cll) - ) + if master_display: + app.fastflix.current_video.hdr10_streams.append( + Box(index=video_stream.index, master_display=master_display, cll=cll) + ) def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index b9dd4a6b..7bd8ae4d 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -94,7 +94,7 @@ class FFmpegNVENCSettings(EncoderSettings): class NVEncCSettings(EncoderSettings): name = "HEVC (NVEncC)" preset: str = "quality" - profile: str = "main" + profile: str = "auto" bitrate: Optional[str] = "5000k" cqp: Optional[str] = None cq: int = 0 diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index c0e39ffa..8198cf00 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -51,9 +51,9 @@ def get_queue(lockless=False) -> List[Video]: audio_tracks=audio, subtitle_tracks=subtitles, attachment_tracks=attachments, - video_encoder_settings=ves, crop=crop, ) + vs.video_encoder_settings = ves # No idea why this has to be called after, otherwise reset to x265 del video["video_settings"] queue.append(Video(**video, video_settings=vs, status=status)) return queue diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index fb3e2692..154010a8 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1145,11 +1145,11 @@ def reload_video_from_queue(self, video: Video): self.app.fastflix.current_video = video self.input_video = video.source - - hdr10_indexes = (x.index for x in self.app.fastflix.current_video.hdr10_streams) + hdr10_indexes = [x.index for x in self.app.fastflix.current_video.hdr10_streams] text_video_tracks = [ ( - f'{x.index}: {x.codec_name} {x.get("bit_depth", "8")}-bit {x.get("color_primaries")}' + f'{x.index}: {x.codec_name} {x.get("bit_depth", "8")}-bit ' + f'{x["color_primaries"] if x.get("color_primaries") else ""}' f'{" - HDR10" if x.index in hdr10_indexes else ""}' f'{" | HDR10+" if x.index in self.app.fastflix.current_video.hdr10_plus else ""}' ) @@ -1157,7 +1157,6 @@ def reload_video_from_queue(self, video: Video): ] self.widgets.video_track.clear() self.widgets.video_track.addItems(text_video_tracks) - selected_track = 0 for track in self.app.fastflix.current_video.streams.video: if track.index == self.app.fastflix.current_video.video_settings.selected_track: @@ -1165,7 +1164,6 @@ def reload_video_from_queue(self, video: Video): self.widgets.video_track.setCurrentIndex(selected_track) end_time = self.app.fastflix.current_video.video_settings.end_time or video.duration - if self.app.fastflix.current_video.video_settings.crop: self.widgets.crop.top.setText(str(self.app.fastflix.current_video.video_settings.crop.top)) self.widgets.crop.left.setText(str(self.app.fastflix.current_video.video_settings.crop.left)) @@ -1186,7 +1184,6 @@ def reload_video_from_queue(self, video: Video): self.widgets.remove_hdr.setChecked(self.app.fastflix.current_video.video_settings.remove_hdr) self.widgets.rotate.setCurrentIndex(self.transpose_to_rotation(video.video_settings.rotate)) self.widgets.fast_time.setCurrentIndex(0 if video.video_settings.fast_seek else 1) - if video.video_settings.vertical_flip: self.widgets.flip.setCurrentIndex(1) if video.video_settings.horizontal_flip: @@ -1207,7 +1204,6 @@ def reload_video_from_queue(self, video: Video): self.widgets.scale.width.setText(str(self.app.fastflix.current_video.width)) self.widgets.scale.height.setText("Auto") self.widgets.scale.keep_aspect.setChecked(True) - self.video_options.reload() self.enable_all() @@ -1235,11 +1231,11 @@ def update_video_info(self): self.clear_current_video() return - hdr10_indexes = (x.index for x in self.app.fastflix.current_video.hdr10_streams) - + hdr10_indexes = [x.index for x in self.app.fastflix.current_video.hdr10_streams] text_video_tracks = [ ( - f'{x.index}: {x.codec_name} {x.get("bit_depth", "8")}-bit {x.get("color_primaries")}' + f'{x.index}: {x.codec_name} {x.get("bit_depth", "8")}-bit ' + f'{x["color_primaries"] if x.get("color_primaries") else ""}' f'{" - HDR10" if x.index in hdr10_indexes else ""}' f'{" | HDR10+" if x.index in self.app.fastflix.current_video.hdr10_plus else ""}' ) diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index ab78152e..39d68129 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -52,7 +52,7 @@ def __init__(self, parent, video: Video, index, first=False): self.index = index self.first = first self.last = False - self.video = video + self.video = video.copy() self.setFixedHeight(60) self.widgets = Box( From ce3a36cb7e2e2312ba996c9d648b034740ed0d25 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 6 Feb 2021 21:58:24 -0600 Subject: [PATCH 40/50] queue stuff totally different --- fastflix/application.py | 4 +- fastflix/conversion_worker.py | 118 ++++++++++++++----------- fastflix/data/languages.yaml | 4 + fastflix/entry.py | 37 ++++---- fastflix/models/fastflix.py | 5 +- fastflix/models/queue.py | 14 +-- fastflix/widgets/main.py | 30 +++---- fastflix/widgets/panels/queue_panel.py | 52 +++++++---- 8 files changed, 153 insertions(+), 111 deletions(-) diff --git a/fastflix/application.py b/fastflix/application.py index e0dc31ec..358568ab 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -100,9 +100,9 @@ def register_app(): logger.exception("Could not set application ID for Windows, please raise issue in github with above error") -def start_app(worker_queue, status_queue, log_queue): +def start_app(worker_queue, status_queue, log_queue, queue_list, queue_lock): app = create_app() - app.fastflix = FastFlix() + app.fastflix = FastFlix(queue=queue_list, queue_lock=queue_lock) app.fastflix.log_queue = log_queue app.fastflix.status_queue = status_queue app.fastflix.worker_queue = worker_queue diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 4e8f1876..0c42d001 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -3,17 +3,20 @@ from pathlib import Path from queue import Empty from typing import Optional +from multiprocessing import Manager, Lock import reusables from appdirs import user_data_dir from box import Box +from filelock import FileLock from fastflix.command_runner import BackgroundRunner from fastflix.language import t from fastflix.shared import file_date -from fastflix.models.queue import get_queue, save_queue +from fastflix.models.queue import get_queue, save_queue, lock_file from fastflix.models.video import Video + logger = logging.getLogger("fastflix-core") log_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs" @@ -52,22 +55,25 @@ def allow_sleep_mode(): logger.debug("System has been allowed to enter sleep mode again") -def get_next_video() -> Optional[Video]: - queue = get_queue() - for video in queue: - if ( - not video.status.complete - and not video.status.success - and not video.status.cancelled - and not video.status.error - and not video.status.running - ): - return video +def get_next_video(queue_list, queue_lock) -> Optional[Video]: + with queue_lock: + for video in queue_list: + if ( + not video.status.complete + and not video.status.success + and not video.status.cancelled + and not video.status.error + and not video.status.running + ): + logger.debug(f"Next video is {video.uuid} - {video.status}") + return video.copy() def set_status( - current_video, - completed=None, + current_video: Video, + queue_list, + queue_lock, + complete=None, success=None, cancelled=None, errored=None, @@ -77,36 +83,45 @@ def set_status( ): if not current_video: return - queue = get_queue() - for video in queue: - if video.uuid == current_video.uuid: - if completed is not None: - video.status.complete = completed - if cancelled is not None: - video.status.cancelled = cancelled - if errored is not None: - video.status.error = errored - if success is not None: - video.status.success = success - if running is not None: - video.status.running = running - - if completed or cancelled or errored or success: - video.status.running = False - - if next_command: - video.status.current_command += 1 - if reset_commands: - video.status.current_command = 0 - break - else: - logger.error(f"Could not find video in queue to update status of!\n {current_video}") - return - save_queue(queue) + + with queue_lock: + video_copy = None + video_pos = 0 + for i, video in enumerate(queue_list): + if video.uuid == current_video.uuid: + video_copy = video.copy() + video_pos = i + break + else: + logger.error(f"Can't find video {current_video.uuid} in queue to update its status: {queue_list}") + return + + queue_list.pop(video_pos) + + if complete is not None: + video_copy.status.complete = complete + if cancelled is not None: + video_copy.status.cancelled = cancelled + if errored is not None: + video_copy.status.error = errored + if success is not None: + video_copy.status.success = success + if running is not None: + video_copy.status.running = running + + if complete or cancelled or errored or success: + video_copy.status.running = False + + if next_command: + video_copy.status.current_command += 1 + if reset_commands: + video_copy.status.current_command = 0 + + queue_list.insert(video_pos, video_copy) @reusables.log_exception(log="fastflix-core") -def queue_worker(gui_proc, worker_queue, status_queue, log_queue): +def queue_worker(gui_proc, worker_queue, status_queue, log_queue, queue_list, queue_lock: Lock): runner = BackgroundRunner(log_queue=log_queue) # Command looks like (video_uuid, command_uuid, command, work_dir) @@ -119,7 +134,7 @@ def queue_worker(gui_proc, worker_queue, status_queue, log_queue): def start_command(): nonlocal currently_encoding log_queue.put( - f"CLEAR_WINDOW:{video.uuid}:{video.video_settings.conversion_commands[video.status.current_command]['uuid']}" + f"CLEAR_WINDOW:{video.uuid}:{video.video_settings.conversion_commands[video.status.current_command].uuid}" ) reusables.remove_file_handlers(logger) new_file_handler = reusables.get_file_handler( @@ -133,10 +148,10 @@ def start_command(): prevent_sleep_mode() currently_encoding = True runner.start_exec( - video.video_settings.conversion_commands[video.status.current_command]["command"], + video.video_settings.conversion_commands[video.status.current_command].command, work_dir=str(video.work_path), ) - set_status(video, running=True) + set_status(video, queue_list=queue_list, queue_lock=queue_lock, running=True) status_queue.put(("queue",)) # status_queue.put(("running", commands_to_run[0][0], commands_to_run[0][1], runner.started_at.isoformat())) @@ -149,7 +164,7 @@ def start_command(): # Stop working! currently_encoding = False - set_status(video, errored=True) + set_status(video, queue_list=queue_list, queue_lock=queue_lock, errored=True) status_queue.put(("error",)) allow_sleep_mode() if gui_died: @@ -162,12 +177,13 @@ def start_command(): if len(video.video_settings.conversion_commands) > video.status.current_command: logger.debug("About to run next command for this video") - set_status(video, next_command=True) + set_status(video, queue_list=queue_list, queue_lock=queue_lock, next_command=True) status_queue.put(("queue",)) start_command() continue else: - set_status(video, next_command=True, completed=True) + logger.debug(f"{video.uuid} has been completed") + set_status(video, queue_list=queue_list, queue_lock=queue_lock, next_command=True, complete=True) status_queue.put(("queue",)) video = None @@ -177,7 +193,7 @@ def start_command(): logger.debug(t("Queue has been paused")) continue - if video := get_next_video(): + if video := get_next_video(queue_list=queue_list, queue_lock=queue_lock): start_command() continue else: @@ -218,7 +234,7 @@ def start_command(): # Request looks like (queue command, log_dir, (commands)) log_path = Path(request[1]) if not currently_encoding and not paused: - video = get_next_video() + video = get_next_video(queue_list=queue_list, queue_lock=queue_lock) if video: start_command() @@ -226,7 +242,7 @@ def start_command(): logger.debug(t("Cancel has been requested, killing encoding")) runner.kill() if video: - set_status(video, reset_commands=True, cancelled=True) + set_status(video, queue_list=queue_list, queue_lock=queue_lock, reset_commands=True, cancelled=True) currently_encoding = False allow_sleep_mode() status_queue.put(("cancelled", video.uuid if video else "")) @@ -240,7 +256,7 @@ def start_command(): logger.debug(t("Command worker received request to resume encoding")) if not currently_encoding: if not video: - video = get_next_video() + video = get_next_video(queue_list=queue_list, queue_lock=queue_lock) start_command() if request[0] == "set after done": diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index b2909878..56462706 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -2849,3 +2849,7 @@ Set speed to 4 for first pass: eng: Set speed to 4 for first pass AQ Strength: eng: AQ Strength +Extracting subtitles to: + eng: Extracting subtitles to +Extracted subtitles successfully: + eng: Extracted subtitles successfully diff --git a/fastflix/entry.py b/fastflix/entry.py index 17284a4f..881284e7 100644 --- a/fastflix/entry.py +++ b/fastflix/entry.py @@ -2,7 +2,7 @@ import logging import sys import traceback -from multiprocessing import Process, Queue, freeze_support +from multiprocessing import Process, Queue, freeze_support, Manager, Lock try: import coloredlogs @@ -26,12 +26,12 @@ sys.exit(1) -def separate_app_process(worker_queue, status_queue, log_queue): +def separate_app_process(worker_queue, status_queue, log_queue, queue_list, queue_lock): """ This prevents any QT components being imported in the main process""" from fastflix.application import start_app freeze_support() - start_app(worker_queue, status_queue, log_queue) + start_app(worker_queue, status_queue, log_queue, queue_list, queue_lock) def startup_options(): @@ -116,19 +116,18 @@ def main(): status_queue = Queue() log_queue = Queue() - gui_proc = Process( - target=separate_app_process, - args=( - worker_queue, - status_queue, - log_queue, - ), - ) - gui_proc.start() - exit_status = 1 - try: - queue_worker(gui_proc, worker_queue, status_queue, log_queue) - exit_status = 0 - finally: - gui_proc.kill() - return exit_status + queue_lock = Lock() + with Manager() as manager: + queue_list = manager.list() + gui_proc = Process( + target=separate_app_process, + args=(worker_queue, status_queue, log_queue, queue_list, queue_lock), + ) + gui_proc.start() + exit_status = 1 + try: + queue_worker(gui_proc, worker_queue, status_queue, log_queue, queue_list, queue_lock) + exit_status = 0 + finally: + gui_proc.kill() + return exit_status diff --git a/fastflix/models/fastflix.py b/fastflix/models/fastflix.py index 8e3960a4..5f6c23cc 100644 --- a/fastflix/models/fastflix.py +++ b/fastflix/models/fastflix.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from dataclasses import dataclass, field -# from multiprocessing import Queue +from multiprocessing import Lock from pathlib import Path from typing import Any, Dict, List, Optional @@ -30,4 +30,5 @@ class FastFlix(BaseModel): log_queue: Any = None current_video: Optional[Video] = None - queue: List[Video] = Field(default_factory=list) + queue: Any = None + queue_lock: Any = None diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index 8198cf00..01ed887f 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -3,7 +3,7 @@ import os from pathlib import Path -from box import Box +from box import Box, BoxError from pydantic import BaseModel, Field from filelock import FileLock from appdirs import user_data_dir @@ -20,11 +20,15 @@ def get_queue(lockless=False) -> List[Video]: if not queue_file.exists(): return [] - if lockless: - loaded = Box.from_yaml(filename=queue_file) - else: - with FileLock(lock_file): + try: + if lockless: loaded = Box.from_yaml(filename=queue_file) + else: + with FileLock(lock_file): + loaded = Box.from_yaml(filename=queue_file) + except BoxError: + # TODO log + return [] queue = [] for video in loaded["queue"]: diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 154010a8..86d48bd4 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -12,6 +12,7 @@ from pathlib import Path from typing import Tuple, Union +from filelock import FileLock import pkg_resources import reusables from box import Box @@ -32,7 +33,7 @@ from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.video import Status, Video, VideoSettings, Crop -from fastflix.models.queue import get_queue, save_queue +from fastflix.models.queue import get_queue, save_queue, lock_file from fastflix.resources import ( black_x_icon, folder_icon, @@ -1667,8 +1668,7 @@ def add_to_queue(self): self.app.fastflix.worker_queue.put(tuple(requests)) self.clear_current_video() - # from fastflix.models.queue import save_queue - # save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) + save_queue(self.app.fastflix.queue) return True @reusables.log_exception("fastflix", show_traceback=False) @@ -1759,19 +1759,15 @@ def dragMoveEvent(self, event): def status_update(self): logger.debug(f"Updating queue from command worker") - self.app.fastflix.queue = get_queue() - updated = False - for video in self.app.fastflix.queue: - if video.status.complete and not video.status.subtitle_fixed: - if video.video_settings.subtitle_tracks and not video.video_settings.subtitle_tracks[0].disposition: - if mkv_prop_edit := shutil.which("mkvpropedit"): - worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) - worker.start() - video.status.subtitle_fixed = True - updated = True - if updated: - save_queue(self.app.fastflix.queue) - + with self.app.fastflix.queue_lock: + for video in self.app.fastflix.queue: + if video.status.complete and not video.status.subtitle_fixed: + if video.video_settings.subtitle_tracks and not video.video_settings.subtitle_tracks[0].disposition: + if mkv_prop_edit := shutil.which("mkvpropedit"): + worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) + worker.start() + video.status.subtitle_fixed = True + save_queue(self.app.fastflix.queue) self.video_options.update_queue() def find_video(self, uuid) -> Video: @@ -1801,7 +1797,9 @@ def run(self): while True: # Message looks like (command, video_uuid, command_uuid) status = self.status_queue.get() + self.app.processEvents() self.main.status_update_signal.emit() + self.app.processEvents() if status[0] == "complete": self.main.completed.emit(0) elif status[0] == "error": diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index 39d68129..cf09daff 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -243,11 +243,12 @@ def __init__(self, parent, app: FastFlixApp): self.queue_startup_check() def queue_startup_check(self): - self.app.fastflix.queue = get_queue() + for item in get_queue(): + self.app.fastflix.queue.append(item) remove_vids = [] for video in self.app.fastflix.queue: - if video.status.running: - video.status.clear() + # if video.status.running: + # video.status.clear() if video.status.complete: remove_vids.append(video) @@ -263,14 +264,19 @@ def queue_startup_check(self): def reorder(self, update=True): super().reorder(update=update) - self.app.fastflix.queue = [track.video for track in self.tracks] + + with self.app.fastflix.queue_lock: + for i in range(len(self.app.fastflix.queue)): + self.app.fastflix.queue.pop() + for track in self.tracks: + self.app.fastflix.queue.append(track.video) + for track in self.tracks: track.widgets.up_button.setDisabled(False) track.widgets.down_button.setDisabled(False) if self.tracks: self.tracks[0].widgets.up_button.setDisabled(True) self.tracks[-1].widgets.down_button.setDisabled(True) - save_queue(self.app.fastflix.queue) def new_source(self): for track in self.tracks: @@ -289,13 +295,14 @@ def clear_complete(self): self.remove_item(queued_item.video) def remove_item(self, video): - self.app.fastflix.queue.remove(video) - save_queue(self.app.fastflix.queue) + with self.app.fastflix.queue_lock: + self.app.fastflix.queue.remove(video) self.new_source() def reload_from_queue(self, video): - self.main.reload_video_from_queue(video) - self.app.fastflix.queue.remove(video) + with self.app.fastflix.queue_lock: + self.main.reload_video_from_queue(video) + self.app.fastflix.queue.remove(video) self.new_source() def reset_pause_encode(self): @@ -340,11 +347,24 @@ def set_after_done(self): self.app.fastflix.worker_queue.put(["set after done", command]) - def retry_video(self, video): - for vid in self.app.fastflix.queue: - if vid.uuid == video.uuid: - vid.status.cancelled = False - vid.status.current_command = 0 - break - save_queue(self.app.fastflix.queue) + def retry_video(self, current_video): + # TODO pop / insert + with self.app.fastflix.queue_lock: + video_copy = None + video_pos = 0 + for i, video in enumerate(self.app.fastflix.queue): + if video.uuid == current_video.uuid: + video_copy = video.copy() + video_pos = i + break + else: + logger.error(f"Can't find video {current_video.uuid} in queue to update its status: {queue_list}") + return + + video_copy.status.cancelled = False + video_copy.status.current_command = 0 + + self.app.fastflix.queue.pop(video_pos) + self.app.fastflix.queue.insert(video_pos, video_copy) + self.new_source() From 333fe8ff7a528f07ecf863c765565f10062c2d71 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 7 Feb 2021 09:59:14 -0600 Subject: [PATCH 41/50] more queue updates --- fastflix/conversion_worker.py | 8 ++----- fastflix/models/fastflix.py | 1 - fastflix/models/queue.py | 22 ++++++----------- fastflix/widgets/main.py | 11 ++++----- fastflix/widgets/panels/queue_panel.py | 33 ++++++++++++++------------ 5 files changed, 31 insertions(+), 44 deletions(-) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 0c42d001..beed078a 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -8,12 +8,11 @@ import reusables from appdirs import user_data_dir from box import Box -from filelock import FileLock from fastflix.command_runner import BackgroundRunner from fastflix.language import t from fastflix.shared import file_date -from fastflix.models.queue import get_queue, save_queue, lock_file +from fastflix.models.queue import get_queue, save_queue from fastflix.models.video import Video @@ -85,18 +84,15 @@ def set_status( return with queue_lock: - video_copy = None - video_pos = 0 for i, video in enumerate(queue_list): if video.uuid == current_video.uuid: - video_copy = video.copy() video_pos = i break else: logger.error(f"Can't find video {current_video.uuid} in queue to update its status: {queue_list}") return - queue_list.pop(video_pos) + video_copy = queue_list.pop(video_pos) if complete is not None: video_copy.status.complete = complete diff --git a/fastflix/models/fastflix.py b/fastflix/models/fastflix.py index 5f6c23cc..13d25196 100644 --- a/fastflix/models/fastflix.py +++ b/fastflix/models/fastflix.py @@ -19,7 +19,6 @@ class FastFlix(BaseModel): data_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) log_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs" queue_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml" - queue_lock_file: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.lock" ffmpeg_version: str = "" ffmpeg_config: List[str] = "" ffprobe_version: str = "" diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index 01ed887f..283824d8 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -2,30 +2,25 @@ from typing import List, Optional, Union import os from pathlib import Path +import logging from box import Box, BoxError from pydantic import BaseModel, Field -from filelock import FileLock from appdirs import user_data_dir from fastflix.models.video import Video, VideoSettings, Status, Crop from fastflix.models.encode import AudioTrack, SubtitleTrack, AttachmentTrack from fastflix.models.encode import setting_types -queue_file = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml" -lock_file = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.lock" +logger = logging.getLogger("fastflix") -def get_queue(lockless=False) -> List[Video]: +def get_queue(queue_file: Path) -> List[Video]: if not queue_file.exists(): return [] try: - if lockless: - loaded = Box.from_yaml(filename=queue_file) - else: - with FileLock(lock_file): - loaded = Box.from_yaml(filename=queue_file) + loaded = Box.from_yaml(filename=queue_file) except BoxError: # TODO log return [] @@ -63,7 +58,7 @@ def get_queue(lockless=False) -> List[Video]: return queue -def save_queue(queue: List[Video], lockless=False): +def save_queue(queue: List[Video], queue_file: Path): items = [] for video in queue: video = video.dict() @@ -71,8 +66,5 @@ def save_queue(queue: List[Video], lockless=False): video["work_path"] = os.fspath(video["work_path"]) video["video_settings"]["output_path"] = os.fspath(video["video_settings"]["output_path"]) items.append(video) - if lockless: - Box(queue=items).to_yaml(filename=queue_file) - else: - with FileLock(lock_file): - Box(queue=items).to_yaml(filename=queue_file) + Box(queue=items).to_yaml(filename=queue_file) + logger.debug(f"queue saved to recovery file {queue_file}") diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 86d48bd4..2228dd86 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -12,7 +12,6 @@ from pathlib import Path from typing import Tuple, Union -from filelock import FileLock import pkg_resources import reusables from box import Box @@ -33,7 +32,7 @@ from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.video import Status, Video, VideoSettings, Crop -from fastflix.models.queue import get_queue, save_queue, lock_file +from fastflix.models.queue import save_queue from fastflix.resources import ( black_x_icon, folder_icon, @@ -1656,9 +1655,7 @@ def add_to_queue(self): # TODO ask if ok # return - video = self.app.fastflix.current_video - - self.app.fastflix.queue.append(copy.deepcopy(video)) + self.app.fastflix.queue.append(copy.deepcopy(self.app.fastflix.current_video)) self.video_options.update_queue() self.video_options.show_queue() @@ -1668,7 +1665,7 @@ def add_to_queue(self): self.app.fastflix.worker_queue.put(tuple(requests)) self.clear_current_video() - save_queue(self.app.fastflix.queue) + save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) return True @reusables.log_exception("fastflix", show_traceback=False) @@ -1767,7 +1764,7 @@ def status_update(self): worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) worker.start() video.status.subtitle_fixed = True - save_queue(self.app.fastflix.queue) + save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) self.video_options.update_queue() def find_video(self, uuid) -> Video: diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index cf09daff..1721e2ba 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -243,7 +243,7 @@ def __init__(self, parent, app: FastFlixApp): self.queue_startup_check() def queue_startup_check(self): - for item in get_queue(): + for item in get_queue(self.app.fastflix.queue_path): self.app.fastflix.queue.append(item) remove_vids = [] for video in self.app.fastflix.queue: @@ -296,14 +296,20 @@ def clear_complete(self): def remove_item(self, video): with self.app.fastflix.queue_lock: - self.app.fastflix.queue.remove(video) + for i, vid in enumerate(self.app.fastflix.queue): + if vid.uuid == video.uuid: + pos = i + break + else: + logger.error("No matching video found to remove from queue") + return + self.app.fastflix.queue.pop(pos) + save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) self.new_source() def reload_from_queue(self, video): - with self.app.fastflix.queue_lock: - self.main.reload_video_from_queue(video) - self.app.fastflix.queue.remove(video) - self.new_source() + self.main.reload_video_from_queue(video) + self.remove_item(video) def reset_pause_encode(self): self.pause_encode.setText(t("Pause Encode")) @@ -348,23 +354,20 @@ def set_after_done(self): self.app.fastflix.worker_queue.put(["set after done", command]) def retry_video(self, current_video): - # TODO pop / insert with self.app.fastflix.queue_lock: - video_copy = None - video_pos = 0 for i, video in enumerate(self.app.fastflix.queue): if video.uuid == current_video.uuid: - video_copy = video.copy() video_pos = i break else: - logger.error(f"Can't find video {current_video.uuid} in queue to update its status: {queue_list}") + logger.error(f"Can't find video {current_video.uuid} in queue to update its status") return - video_copy.status.cancelled = False - video_copy.status.current_command = 0 + video = self.app.fastflix.queue.pop(video_pos) + video.status.cancelled = False + video.status.current_command = 0 - self.app.fastflix.queue.pop(video_pos) - self.app.fastflix.queue.insert(video_pos, video_copy) + self.app.fastflix.queue.insert(video_pos, video) + save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) self.new_source() From d451c8fd47d2fb4833bf0728ba99ff570bfaa0d7 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 7 Feb 2021 10:31:44 -0600 Subject: [PATCH 42/50] fixing profile not setting encoder in drop down --- fastflix/widgets/main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 2228dd86..a2f0296f 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -410,11 +410,9 @@ def init_video_track_select(self): def set_profile(self): if self.loading_video: return - # self.video_options.new_source() - # previous_auto_crop = self.app.fastflix.config.opt("auto_crop") self.app.fastflix.config.selected_profile = self.widgets.profile_box.currentText() self.app.fastflix.config.save() - self.widgets.convert_to.setCurrentText(f" {self.app.fastflix.config.opt('encoder')}") + self.widgets.convert_to.setCurrentText(f" {self.app.fastflix.config.opt('encoder')}") if self.app.fastflix.config.opt("auto_crop") and not self.build_crop(): self.get_auto_crop() self.loading_video = True From 517db7c03014d9df37bf659eba289ddf998366fb Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 7 Feb 2021 11:35:21 -0600 Subject: [PATCH 43/50] adding language support for missing items --- README.md | 3 + fastflix/data/languages.yaml | 531 ++++++++++++++++++++++++----------- fastflix/widgets/about.py | 32 ++- requirements-build.txt | 1 - requirements.txt | 1 - 5 files changed, 388 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index b55715f0..2f401464 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ FastFlix (v4.0.2+) passes through HLG color transfer information to everything e FastFlix does not plan to support Dolby Vision's proprietary format at this time. +# Support FastFlix + +Check out the different ways you can help [support FastFlix](https://github.com/cdgriffith/FastFlix/wiki/Support-FastFlix)! # License diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 56462706..913953ff 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -5,6 +5,13 @@ ita: 4 o 5 disattiveranno l'ottimizzazione della distorsione del tasso, con un impatto ancora maggiore sulla qualità. spa: 4 o 5 desactivarán la optimización de la tasa de distorsión, lo que tendrá un impacto aún mayor en la calidad. zho: 4或5会关闭rate distortion optimization,对质量的影响会更大。 +AQ Strength: + deu: AQ-Stärke + eng: AQ Strength + fra: AQ Force + ita: Forza AQ + spa: Fuerza de AQ + zho: AQ强度 About: deu: Über eng: About @@ -54,6 +61,13 @@ Advanced: ita: Avanzato spa: Avanzado zho: 高级 +Advanced settings are currently not saved in Profiles: + deu: Erweiterte Einstellungen werden derzeit nicht in Profilen gespeichert + eng: Advanced settings are currently not saved in Profiles + fra: Les paramètres avancés ne sont actuellement pas enregistrés dans les Profils + ita: Le impostazioni avanzate non sono attualmente salvate in Profili + spa: Los ajustes avanzados no se guardan actualmente en Perfiles + zho: 高级设置目前不会保存到方案中 After Conversion: deu: Nach der Konvertierung eng: After Conversion @@ -152,6 +166,20 @@ B Adapt: ita: B Adattare spa: B Adaptar zho: B Adapt +B Frames: + deu: B-Frames + eng: B Frames + fra: B Cadres + ita: B Frames + spa: Fotogramas B + zho: B型框架 +B Ref Mode: + deu: B-Ref-Modus + eng: B Ref Mode + fra: B Mode Ref + ita: Modalità B Ref + spa: Modo B Ref + zho: B参考模式 Bit Depth: deu: Bit-Tiefe eng: Bit Depth @@ -173,6 +201,13 @@ Block Size: ita: Dimensione del blocco spa: Tamaño del bloque zho: 块大小 +Both Passes: + deu: Beide Durchgänge + eng: Both Passes + fra: Les deux passages + ita: Entrambi i Pass + spa: Ambos pases + zho: 两遍均应用 Bottom: deu: Unten eng: Bottom @@ -194,6 +229,13 @@ Break the video into rows to encode faster (lesser quality): ita: Suddividere il video in righe per codificare più velocemente (qualità inferiore) spa: Dividir el video en filas para codificar más rápido (menor calidad) zho: 将视频按行分割,以更快地进行编码(质量较差)。 +Bufsize: + deu: Bufsize + eng: Bufsize + fra: Bufsize + ita: Bufsize + spa: Bufsize + zho: Bufsize Build: deu: Erstellen eng: Build @@ -215,6 +257,13 @@ CPU Used: ita: CPU usata spa: CPU utilizado zho: CPU用量 +Calculate PSNR and SSIM and show in the encoder output: + deu: PSNR und SSIM berechnen und in der Encoder-Ausgabe anzeigen + eng: Calculate PSNR and SSIM and show in the encoder output + fra: Calculer PSNR et SSIM et afficher dans la sortie du codeur + ita: Calcolare PSNR e SSIM e mostrare nell'output del codificatore + spa: Calcular PSNR y SSIM y mostrar en la salida del codificador + zho: 计算PSNR和SSIM,并显示在编码器输出中。 Cancel: deu: Abbrechen eng: Cancel @@ -369,6 +418,13 @@ Config File: ita: File di configurazione spa: Archivo de configuración zho: 配置文件 +Constant: + deu: Konstant + eng: Constant + fra: Constant + ita: Costante + spa: Constante + zho: 恒定 Conversion: deu: Konvertierung eng: Conversion @@ -537,6 +593,13 @@ Crop: ita: Ritaglio spa: Crop zho: 裁剪 +Crop Detect Points: + deu: Crop-Erkennungspunkte + eng: Crop Detect Points + fra: Points de détection des cultures + ita: Ritagliare i punti di rilevamento + spa: Puntos de detección de cultivos + zho: 作物检测点 Current Profile Settings: deu: Aktuelle Profileinstellungen eng: Current Profile Settings @@ -551,6 +614,13 @@ Currently only works for image based subtitles.: ita: Attualmente funziona solo per i sottotitoli basati su immagini. spa: Actualmente sólo funciona para subtítulos basados en imágenes. zho: 目前只适用于基于图像的字幕。 +Custom NVEncC options: + deu: Benutzerdefinierte NVEncC-Optionen + eng: Custom NVEncC options + fra: Options NVEncC personnalisées + ita: Opzioni NVEncC personalizzate + spa: Opciones personalizadas de NVEncC + zho: 自定义NVEncC选项 Custom ffmpeg options: deu: Benutzerdefinierte ffmpeg-Optionen eng: Custom ffmpeg options @@ -628,6 +698,13 @@ Denoise: ita: Denoise spa: Denoise zho: 降噪 +Detect HDR10+: + deu: HDR10+ erkennen + eng: Detect HDR10+ + fra: Détecter le HDR10+ + ita: Rileva HDR10 + spa: Detectar HDR10+ + zho: 检测HDR10+ Detecting Interlace: deu: Erkennen von Interlace eng: Detecting Interlace @@ -677,6 +754,13 @@ Download: ita: Scaricare spa: Descargar zho: 下载 +Download Cancelled: + deu: Download abgebrochen + eng: Download Cancelled + fra: Téléchargement annulé + ita: Scaricamento annullato + spa: Descarga cancelada + zho: 下载取消 Download Newest FFmpeg: deu: Neuestes FFmpeg herunterladen eng: Download Newest FFmpeg @@ -698,6 +782,13 @@ Dual License: ita: Doppia Licenza spa: Licencia doble zho: 双重许可 +Enable VBV: + deu: VBV freigeben + eng: Enable VBV + fra: Activer la VBV + ita: Attivare VBV + spa: Activar VBV + zho: 启用VBV Enable row based multi-threading: deu: Zeilenbasiertes Multithreading aktivieren eng: Enable row based multi-threading @@ -803,6 +894,13 @@ Encoding complete: ita: Codifica completa spa: Codificación completa zho: 编码完成 +Encoding errored: + deu: Kodierung fehlgeschlagen + eng: Encoding errored + fra: Encodage erroné + ita: Codifica errata + spa: '' + zho: 编码错误 End: deu: Beenden eng: End @@ -880,6 +978,20 @@ Extra x265 params in opt=1:opt2=0 format: ita: Parametri extra x265 nel formato opt=1:opt2=0 spa: Parámetros extra x265 en formato opt=1:opt2=0 zho: 额外的x265参数,格式为opt=1:opt2=0 +Extract: + deu: Auszug + eng: Extract + fra: Extrait + ita: Estratto + spa: Extracto + zho: 提取 +Extract HDR10+: + deu: HDR10+ extrahieren + eng: Extract HDR10+ + fra: Extrait HDR10+ + ita: Estrarre HDR10 + spa: Extraer HDR10+ + zho: 提取HDR10+ Extract covers: deu: Extrahieren deckt ab eng: Extract covers @@ -887,6 +999,27 @@ Extract covers: ita: Coperture per l'estrazione spa: Extraer las cubiertas zho: 提取封面 +Extracted subtitles successfully: + deu: Untertitel erfolgreich extrahiert + eng: Extracted subtitles successfully + fra: Sous-titres extraits avec succès + ita: Sottotitoli estratti con successo + spa: Subtítulos extraídos con éxito + zho: 成功提取字幕 +Extracting HDR10+ metadata: + deu: Extrahieren von HDR10+-Metadaten + eng: Extracting HDR10+ metadata + fra: Extraction des métadonnées HDR10+. + ita: Estrarre i metadati HDR10 + spa: Extracción de metadatos HDR10+ + zho: 提取HDR10+元数据 +Extracting subtitles to: + deu: Extrahieren von Untertiteln auf + eng: Extracting subtitles to + fra: Extraction des sous-titres de + ita: Estrazione dei sottotitoli a + spa: Extracción de subtítulos a + zho: 提取字幕到 FFMPEG AV1 Encoding Guide: deu: FFMPEG AV1-Kodierungsleitfaden eng: FFMPEG AV1 Encoding Guide @@ -929,6 +1062,20 @@ FPS: ita: FPS spa: FPS zho: FPS +Fast first pass: + deu: Schneller erster Durchlauf + eng: Fast first pass + fra: Première passe rapide + ita: Primo passaggio veloce + spa: Primera pasada rápida + zho: 快速的第一道 +File: + deu: Datei + eng: File + fra: Fichier + ita: Archivio + spa: Archivo + zho: 文件 Flat UI: deu: Flaches UI eng: Flat UI @@ -971,6 +1118,20 @@ Frames Per Second: ita: Cornici al secondo spa: Cuadros por segundo zho: 每秒帧数 +GPU: + deu: GPU + eng: GPU + fra: GPU + ita: GPU + spa: GPU + zho: GPU +GUI Logging Level: + deu: GUI-Protokollierungsebene + eng: GUI Logging Level + fra: Niveau d'exploitation forestière + ita: Livello di registrazione GUI + spa: Nivel de registro GUI + zho: GUI日志级别 Gather FFmpeg audio encoders: deu: Sammeln von FFmpeg-Audio-Encodern eng: Gather FFmpeg audio encoders @@ -1076,6 +1237,13 @@ Horizontal Flip: ita: Capovolgimento orizzontale spa: Volteo horizontal zho: 水平翻转 +Init Q: + deu: Init Q + eng: Init Q + fra: Init Q + ita: Init Q + spa: Init Q + zho: 启动Q Initialize Encoders: deu: Kodierer initialisieren eng: Initialize Encoders @@ -1174,6 +1342,13 @@ Left: ita: Sinistra spa: Izquierda zho: 左侧 +Level: + deu: Pegel + eng: Level + fra: Niveau + ita: Livello + spa: Nivel + zho: 级别 Log2 of number of tile columns to encode faster (lesser quality): deu: Log2 der Anzahl der Kachelspalten, um schneller zu kodieren (geringere Qualität) eng: Log2 of number of tile columns to encode faster (lesser quality) @@ -1188,6 +1363,13 @@ Log2 of number of tile rows to encode faster (lesser quality): ita: Log2 del numero di file di tegole da codificare più velocemente (qualità inferiore) spa: Log2 del número de filas de azulejos para codificar más rápido (menor calidad) zho: Log2 of number of tile rows to encode faster (lesser quality) +Lookahead: + deu: Vorausschauende Betrachtung + eng: Lookahead + fra: Lookahead + ita: Lookahead + spa: Lookahead + zho: 瞻前顾后 Lossless: deu: Verlustfrei eng: Lossless @@ -1209,6 +1391,13 @@ Max Muxing Queue Size: ita: Dimensione massima della coda di Muxing spa: Tamaño máximo de la cola Muxing zho: 最大混流队列大小 +Max Q: + deu: Max Q + eng: Max Q + fra: Max Q + ita: Q massimo + spa: Max Q + zho: 最大Q值 Maximum B frames: deu: Maximale B-Frames eng: Maximum B frames @@ -1223,6 +1412,55 @@ Maximum B frames: ita: 'Numero massimo di b-frame consecutivi. ' spa: 'Número máximo de fotogramas B consecutivos. ' zho: 连续b帧的最大数量。 +Maxrate: + deu: Maxrate + eng: Maxrate + fra: Maxrate + ita: Maxrate + spa: Maxrate + zho: Maxrate +Metrics: + deu: Metriken + eng: Metrics + fra: Métriques + ita: Metriche + spa: Métricas + zho: 衡量标准 +Min Q: + deu: Min Q + eng: Min Q + fra: Min Q + ita: Min Q + spa: Q mínimo + zho: 最小Q +Motion vector accuracy: + deu: Bewegungsvektor-Genauigkeit + eng: Motion vector accuracy + fra: Précision du vecteur de mouvement + ita: Precisione del vettore di movimento + spa: Precisión del vector de movimiento + zho: 运动矢量精度 +Multipass: + deu: Multipass + eng: Multipass + fra: Multipass + ita: Multipass + spa: Multipass + zho: 多通道 +NVEncC Encoder support is still experimental!: + deu: NVEncC Encoder-Unterstützung ist noch experimentell! + eng: NVEncC Encoder support is still experimental! + fra: Le support des encodeurs NVEncC est encore expérimental ! + ita: Il supporto NVEncC Encoder è ancora sperimentale! + spa: La compatibilidad con el codificador NVEncC es todavía experimental. + zho: NVEncC编码器支持仍然是实验性的! +NVEncC Options: + deu: NVEncC-Optionen + eng: NVEncC Options + fra: Options NVEncC + ita: Opzioni NVEncC + spa: Opciones de NVEncC + zho: NVEncC选项 New Profile: deu: Neues Profil eng: New Profile @@ -1405,6 +1643,13 @@ Override Source FPS: ita: Annullare la sorgente FPS spa: Anular el FPS de la fuente zho: 覆盖来源 FPS +Override the preset rate-control: + deu: Überschreibt die voreingestellte Ratensteuerung + eng: Override the preset rate-control + fra: Annuler le contrôle des taux préétablis + ita: Sovrascrivere il controllo del tasso preimpostato + spa: Anula el control de velocidad preestablecido + zho: 覆盖预设的速率控制 PIR can replace keyframes by inserting a column of intra blocks in non-keyframes,: deu: PIR kann Keyframes durch Einfügen einer Spalte von Intrablöcken in Nicht-Keyframes ersetzen, eng: PIR can replace keyframes by inserting a column of intra blocks in non-keyframes, @@ -1447,6 +1692,13 @@ Pixel Format (requires at least 10-bit for HDR): ita: Formato pixel (richiede almeno 10 bit per HDR) spa: Formato de píxeles (requiere al menos 10 bits para el HDR) zho: 像素格式(HDR要求至少10-bit) +Please make sure seek method is set to exact: + deu: Bitte stellen Sie sicher, dass die Suchmethode auf exakt eingestellt ist + eng: Please make sure seek method is set to exact + fra: Veuillez vous assurer que la méthode de recherche est réglée sur l'exacte + ita: Si prega di assicurarsi che il metodo di ricerca sia impostato su + spa: Por favor, asegúrese de que el método de búsqueda se establece con exactitud + zho: 请确保检索方式已设置为exact Please provide a profile name: deu: Bitte geben Sie einen Profilnamen an eng: Please provide a profile name @@ -1489,6 +1741,13 @@ Preset: ita: Preset spa: Preset zho: 预设 +Profile Name: + deu: Profil-Name + eng: Profile Name + fra: Nom du profil + ita: Nome del profilo + spa: Nombre del perfil + zho: 方案名称 Profile_encoderopt: deu: Profil eng: Profile @@ -1510,13 +1769,6 @@ Profile_window: ita: Profilo spa: Perfil zho: 方案 -Profile Name: - deu: Profil-Name - eng: Profile Name - fra: Nom du profil - ita: Nome del profilo - spa: Nombre del perfil - zho: 方案名称 Profiles: deu: Profile eng: Profiles @@ -1531,6 +1783,13 @@ Python: ita: Python spa: Python zho: Python +Q-pel is highest precision: + deu: Q-pel ist höchste Genauigkeit + eng: Q-pel is highest precision + fra: Le Q-pel est de la plus haute précision + ita: Q-pel è la massima precisione + spa: Q-pel es la máxima precisión + zho: Q-pel是最高精度 Quality: deu: Qualität eng: Quality @@ -1580,6 +1839,13 @@ Queue has been paused: ita: La coda è stata messa in pausa spa: La cola se ha detenido zho: 队列已暂停 +RC Lookahead: + deu: RC Lookahead + eng: RC Lookahead + fra: RC Lookahead + ita: RC Lookahead + spa: RC Lookahead + zho: RC Lookahead Raise or lower per-block quantization based on complexity analysis of the source image.: deu: Erhöhen oder verringern Sie die Quantisierung pro Block basierend auf der Komplexitätsanalyse des Quellbilds. eng: Raise or lower per-block quantization based on complexity analysis of the source image. @@ -1587,6 +1853,13 @@ Raise or lower per-block quantization based on complexity analysis of the source ita: Aumentare o diminuire la quantizzazione per blocco in base all'analisi della complessità dell'immagine sorgente. spa: Aumentar o disminuir la cuantificación por bloque basada en el análisis de la complejidad de la imagen de origen. zho: 根据对源图像的复杂度分析,提升或降低各个块的量化。 +Rate Control: + deu: Ratensteuerung + eng: Rate Control + fra: Contrôle des tarifs + ita: Controllo della velocità + spa: Control de velocidad + zho: 速率控制 Raw Commands: deu: Raw-Befehle eng: Raw Commands @@ -1608,6 +1881,13 @@ Reconstructed output pictures are bit-exact to the input pictures.: ita: Le immagini di uscita ricostruite sono bit-esatte alle immagini di ingresso. spa: Las imágenes de salida reconstruidas son un poco exactas a las imágenes de entrada. zho: 重建后的输出图像与输入图像是逐位一致(bit-exact)的。 +Ref Frames: + deu: Ref-Frames + eng: Ref Frames + fra: Ref Frames + ita: Fotogrammi di rif. + spa: Fotogramas de referencia + zho: 参考框架 Remove HDR: deu: HDR entfernen eng: Remove HDR @@ -1734,6 +2014,13 @@ Save Commands: ita: Comandi di salvataggio spa: Salvar los comandos zho: 保存命令 +Save File: + deu: Datei speichern + eng: Save File + fra: Enregistrer le fichier + ita: Salva file + spa: Guardar archivo + zho: 保存文件 Save commands to file: deu: Befehle in Datei speichern eng: Save commands to file @@ -1755,6 +2042,20 @@ Scrub away all incoming metadata, like video titles, unique markings and so on.: ita: Cancella tutti i metadati in arrivo, come i titoli dei video, le marcature uniche e così via. spa: Borra todos los metadatos entrantes, como títulos de video, marcas únicas y demás. zho: 擦除输入文件中所有的元数据,如视频标题、唯一标记等。 +Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on: + deu: Wählt aus, welcher NVENC-fähige Grafikprozessor verwendet werden soll. Die erste GPU ist 0, die zweite ist 1, und so weiter + eng: Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on + fra: Sélectionne le GPU compatible NVENC à utiliser. Le premier GPU est 0, le second est 1, et ainsi de suite + ita: Seleziona quale GPU con capacità NVENC utilizzare. La prima GPU è 0, la seconda è 1, e così via + spa: Selecciona qué GPU con capacidad NVENC se va a utilizar. La primera GPU es 0, la segunda es 1, y así sucesivamente + zho: 选择使用哪个NVENC功能的GPU。第一个GPU为0,第二个为1,以此类推。 +Set speed to 4 for first pass: + deu: Setzen Sie die Geschwindigkeit für den ersten Durchlauf auf 4 + eng: Set speed to 4 for first pass + fra: Régler la vitesse à 4 pour le premier passage + ita: Imposta la velocità a 4 per il primo passaggio + spa: Establece la velocidad en 4 para la primera pasada + zho: 第一遍速度设置为4 Set the "title" tag, sometimes shown as "Movie Name": deu: Setzen Sie den "Titel"-Tag, der manchmal als "Filmname" angezeigt wird eng: Set the "title" tag, sometimes shown as "Movie Name" @@ -1762,6 +2063,20 @@ Set the "title" tag, sometimes shown as "Movie Name": ita: Impostare il tag "title", a volte mostrato come "Movie Name" (nome del film) spa: Poner la etiqueta "título", a veces se muestra como "Nombre de la película" zho: 设置“标题”(title,有时显示为“电影名称”(Movie Name))标签 +Set the encoding level restriction: + deu: Legen Sie die Einschränkung des Kodierungspegels fest + eng: Set the encoding level restriction + fra: Définir la restriction du niveau d'encodage + ita: Imposta la restrizione del livello di codifica + spa: Establezca la restricción del nivel de codificación + zho: 设置编码级别限制 +Set the encoding tier: + deu: Einstellen der Kodierungsebene + eng: Set the encoding tier + fra: Définir le niveau d'encodage + ita: Imposta il livello di codifica + spa: Establecer el nivel de codificación + zho: 设置编码层 Set the level of effort in determining B frame placement.: deu: Legen Sie den Grad des Aufwands bei der Bestimmung der B-Frame-Platzierung fest. eng: Set the level of effort in determining B frame placement. @@ -1853,6 +2168,13 @@ Source width: ita: Larghezza della fonte spa: Ancho de la fuente zho: 源文件宽度 +Spatial AQ: + deu: Spatial AQ + eng: Spatial AQ + fra: QA spatiale + ita: AQ spaziale + spa: AQ espacial + zho: 空间空气质量 Speed: deu: Geschwindigkeit eng: Speed @@ -1909,6 +2231,13 @@ Success: ita: Successo spa: Éxito zho: 成功 +Support FastFlix: + deu: Unterstützt FastFlix + eng: Support FastFlix + fra: Soutenez FastFlix + ita: Supporto FastFlix + spa: Soporta FastFlix + zho: 支持FastFlix Supported Image Files: deu: Unterstützte Bilddateien eng: Supported Image Files @@ -2028,13 +2357,6 @@ Tiles: ita: Piastrelle spa: Baldosas zho: Tiles -Time Left: - deu: Linke Zeit - eng: Time Left - fra: Temps restant - ita: Tempo rimanente - spa: Tiempo restante - zho: 剩余时间 Time Elapsed: deu: Verstrichene Zeit eng: Time Elapsed @@ -2042,6 +2364,13 @@ Time Elapsed: ita: Tempo trascorso spa: Tiempo transcurrido zho: 已用时间 +Time Left: + deu: Linke Zeit + eng: Time Left + fra: Temps restant + ita: Tempo rimanente + spa: Tiempo restante + zho: 剩余时间 Title: deu: Titel eng: Title @@ -2098,6 +2427,13 @@ Use --bframes 0 to force all P/I low-latency encodes.: ita: Utilizzare --bframes 0 per forzare tutte le codifiche P/I a bassa latenza. spa: Use --bframes 0 para forzar todos los códigos de baja latencia P/I. zho: 使用--bframes 0强制进行全P/I帧的低延迟编码。 +Use B frames as references: + deu: B-Frames als Referenzen verwenden + eng: Use B frames as references + fra: Utiliser les cadres B comme références + ita: Usa i fotogrammi B come riferimenti + spa: Utilizar los fotogramas B como referencia + zho: 用B帧作为参考 Use Sane Audio Selection (updatable in config file): deu: Sane Audio Selection verwenden (aktualisierbar in der Konfigurationsdatei) eng: Use Sane Audio Selection (updatable in config file) @@ -2133,6 +2469,13 @@ Using a single frame thread gives a slight improvement in compression,: ita: L'utilizzo di una filettatura a telaio singolo offre un leggero miglioramento della compressione, spa: Usar un solo hilo de cuadro da una ligera mejora en la compresión, zho: 使用单帧线程会使压缩率略有提高, +VBR Target: + deu: VBR-Ziel + eng: VBR Target + fra: Cible VBR + ita: Obiettivo VBR + spa: Objetivo VBR + zho: VBR目标 'Values: 0:none; 1:fast; 2:full(trellis) default': deu: 'Werte: 0:keine; 1:schnell; 2:voll(trellis) Standard' eng: 'Values: 0:none; 1:fast; 2:full(trellis) default' @@ -2490,6 +2833,13 @@ good is the default and recommended for most applications: ita: 'hdr10: Forza la segnalazione dei parametri HDR10 nei pacchetti SEI.' spa: 'hdr10: Forzar la señalización de los parámetros HDR10 en los paquetes SEI.' zho: hdr10:强制在SEI包中发送HDR10参数。 +hq - High Quality, ll - Low Latency, ull - Ultra Low Latency: + deu: hq - Hohe Qualität, ll - Niedrige Latenz, ull - Ultra niedrige Latenz + eng: hq - High Quality, ll - Low Latency, ull - Ultra Low Latency + fra: hq - Haute qualité, ll - Latence faible, ull - Latence ultra faible + ita: hq - Alta qualità, ll - Bassa latenza, ull - Ultra bassa latenza + spa: hq - Alta calidad, ll - Baja latencia, ull - Ultra baja latencia + zho: hq - 高质量,ll - 低延迟,ull - 超低延迟。 installer: deu: Installationsprogramm eng: installer @@ -2700,156 +3050,3 @@ vsync: ita: vsync spa: vsync zho: vsync -File: - deu: Datei - eng: File - fra: Fichier - ita: Archivio - spa: Archivo - zho: 文件 -Both Passes: - deu: Beide Durchgänge - eng: Both Passes - fra: Les deux passages - ita: Entrambi i Pass - spa: Ambos pases - zho: 两遍均应用 -Advanced settings are currently not saved in Profiles: - deu: Erweiterte Einstellungen werden derzeit nicht in Profilen gespeichert - eng: Advanced settings are currently not saved in Profiles - fra: Les paramètres avancés ne sont actuellement pas enregistrés dans les Profils - ita: Le impostazioni avanzate non sono attualmente salvate in Profili - spa: Los ajustes avanzados no se guardan actualmente en Perfiles - zho: 高级设置目前不会保存到方案中 -Constant: - deu: Konstant - eng: Constant - fra: Constant - ita: Costante - spa: Constante - zho: 恒定 -Please make sure seek method is set to exact: - deu: Bitte stellen Sie sicher, dass die Suchmethode auf exakt eingestellt ist - eng: Please make sure seek method is set to exact - fra: Veuillez vous assurer que la méthode de recherche est réglée sur l'exacte - ita: Si prega di assicurarsi che il metodo di ricerca sia impostato su - spa: Por favor, asegúrese de que el método de búsqueda se establece con exactitud - zho: 请确保检索方式已设置为exact -Extract: - deu: Auszug - eng: Extract - fra: Extrait - ita: Estratto - spa: Extracto - zho: 提取 -Save File: - deu: Datei speichern - eng: Save File - fra: Enregistrer le fichier - ita: Salva file - spa: Guardar archivo - zho: 保存文件 -GUI Logging Level: - deu: GUI-Protokollierungsebene - eng: GUI Logging Level - fra: Niveau d'exploitation forestière - ita: Livello di registrazione GUI - spa: Nivel de registro GUI - zho: GUI日志级别 -Enable VBV: - deu: VBV freigeben - eng: Enable VBV - fra: Activer la VBV - ita: Attivare VBV - spa: Activar VBV - zho: 启用VBV -Maxrate: - deu: Maxrate - eng: Maxrate - fra: Maxrate - ita: Maxrate - spa: Maxrate - zho: Maxrate -Bufsize: - deu: Bufsize - eng: Bufsize - fra: Bufsize - ita: Bufsize - spa: Bufsize - zho: Bufsize -Encoding errored: - eng: Encoding errored -Download Cancelled: - eng: Download Cancelled -Extract HDR10+: - eng: Extract HDR10+ -Detect HDR10+: - eng: Detect HDR10+ -Custom NVEncC options: - eng: Custom NVEncC options -Set the encoding tier: - eng: Set the encoding tier -Spatial AQ: - eng: Spatial AQ -Lookahead: - eng: Lookahead -Motion vector accuracy: - eng: Motion vector accuracy -Q-pel is highest precision: - eng: Q-pel is highest precision -Multipass: - eng: Multipass -NVEncC Options: - eng: NVEncC Options -'NVEncC Encoder support is still experimental!': - eng: 'NVEncC Encoder support is still experimental!' -Init Q: - eng: Init Q -Min Q: - eng: Min Q -Max Q: - eng: Max Q -Crop Detect Points: - eng: Crop Detect Points -VBR Target: - eng: VBR Target -Level: - eng: Level -Set the encoding level restriction: - eng: Set the encoding level restriction -B Frames: - eng: B Frames -Ref Frames: - eng: Ref Frames -B Ref Mode: - eng: B Ref Mode -Use B frames as references: - eng: Use B frames as references -Metrics: - eng: Metrics -Calculate PSNR and SSIM and show in the encoder output: - eng: Calculate PSNR and SSIM and show in the encoder output -Extracting HDR10+ metadata: - eng: Extracting HDR10+ metadata -hq - High Quality, ll - Low Latency, ull - Ultra Low Latency: - eng: hq - High Quality, ll - Low Latency, ull - Ultra Low Latency -Rate Control: - eng: Rate Control -Override the preset rate-control: - eng: Override the preset rate-control -RC Lookahead: - eng: RC Lookahead -GPU: - eng: GPU -Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on: - eng: Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on -Fast first pass: - eng: Fast first pass -Set speed to 4 for first pass: - eng: Set speed to 4 for first pass -AQ Strength: - eng: AQ Strength -Extracting subtitles to: - eng: Extracting subtitles to -Extracted subtitles successfully: - eng: Extracted subtitles successfully diff --git a/fastflix/widgets/about.py b/fastflix/widgets/about.py index 7a35a138..dae63de7 100644 --- a/fastflix/widgets/about.py +++ b/fastflix/widgets/about.py @@ -33,28 +33,38 @@ def __init__(self, parent=None): label.setAlignment(QtCore.Qt.AlignCenter) label.setOpenExternalLinks(True) label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + layout.addWidget(label) + + support_label = QtWidgets.QLabel( + f'{link("https://github.com/cdgriffith/FastFlix/wiki/Support-FastFlix", t("Support FastFlix"))}

' + ) + support_label.setOpenExternalLinks(True) + support_label.setFont(QtGui.QFont("Arial", 12)) + support_label.setAlignment((QtCore.Qt.AlignCenter | QtCore.Qt.AlignTop)) + layout.addWidget(support_label) + + bundle_label = QtWidgets.QLabel( + f"Conversion suites: {link('https://www.ffmpeg.org/download.html', 'FFmpeg')} ({t('Various')}), " + f"{link('https://github.com/rigaya/NVEnc', 'NVEncC')} (MIT)

" + f"Encoders:
{link('https://github.com/rigaya/NVEnc', 'NVEncC')} (MIT), " + f"SVT AV1 (MIT), rav1e (MIT), aom (MIT), x265 (GPL), x264 (GPL), libvpx (BSD)" + ) + bundle_label.setAlignment(QtCore.Qt.AlignCenter) + bundle_label.setOpenExternalLinks(True) + layout.addWidget(bundle_label) supporting_libraries_label = QtWidgets.QLabel( "Supporting libraries
" f"{link('https://www.python.org/', t('Python'))}{reusables.version_string} (PSF LICENSE), " f"{link('https://github.com/cdgriffith/Box', t('python-box'))} {box_version} (MIT), " f"{link('https://github.com/cdgriffith/Reusables', t('Reusables'))} {reusables.__version__} (MIT)
" - "mistune (BSD), colorama (BSD), coloredlogs (MIT), Requests (Apache 2.0)" + "mistune (BSD), colorama (BSD), coloredlogs (MIT), Requests (Apache 2.0)
" + "appdirs (MIT), iso639-lang (MIT), psutil (BSD), qtpy (MIT)
" ) supporting_libraries_label.setAlignment(QtCore.Qt.AlignCenter) supporting_libraries_label.setOpenExternalLinks(True) - - layout.addWidget(label) layout.addWidget(supporting_libraries_label) - bundle_label = QtWidgets.QLabel( - f"Conversion suite: {link('https://www.ffmpeg.org/download.html', 'FFmpeg')} ({t('Various')})

" - "Encoders:
SVT AV1 (MIT), rav1e (MIT), aom (MIT), x265 (GPL), x264 (GPL), libvpx (BSD)" - ) - bundle_label.setAlignment(QtCore.Qt.AlignCenter) - bundle_label.setOpenExternalLinks(True) - layout.addWidget(bundle_label) - if pyinstaller: pyinstaller_label = QtWidgets.QLabel( f"Packaged with: {link('https://www.pyinstaller.org/index.html', 'PyInstaller')}" diff --git a/requirements-build.txt b/requirements-build.txt index ac8d25e2..15d77e45 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1,7 +1,6 @@ appdirs colorama coloredlogs -filelock iso639-lang mistune psutil diff --git a/requirements.txt b/requirements.txt index d6e1273f..33995b64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ appdirs colorama coloredlogs -filelock iso639-lang mistune psutil From 1b1d780ac0701caaa4c5084607d5b2aa75ca70b8 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 7 Feb 2021 12:38:40 -0600 Subject: [PATCH 44/50] fixing return to queue mode select --- fastflix/encoders/common/setting_panel.py | 6 ++++++ fastflix/encoders/nvencc_hevc/command_builder.py | 11 ++++++----- fastflix/encoders/nvencc_hevc/settings_panel.py | 3 ++- fastflix/models/encode.py | 1 - fastflix/models/queue.py | 4 +--- fastflix/widgets/about.py | 2 +- fastflix/widgets/background_tasks.py | 2 +- fastflix/widgets/main.py | 1 - 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index 6a964130..f4e20df2 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -382,6 +382,7 @@ def new_source(self): def update_profile(self): global ffmpeg_extra_command + logger.debug("Update profile called") for widget_name, opt in self.opts.items(): if isinstance(self.widgets[widget_name], QtWidgets.QComboBox): default = self.determine_default( @@ -406,6 +407,7 @@ def update_profile(self): pass else: if bitrate: + self.mode = "Bitrate" self.qp_radio.setChecked(False) self.bitrate_radio.setChecked(True) for i, rec in enumerate(self.recommended_bitrates): @@ -416,6 +418,7 @@ def update_profile(self): self.widgets.bitrate.setCurrentText("Custom") self.widgets.custom_bitrate.setText(bitrate.rstrip("kKmMgGbB")) else: + self.mode = self.qp_name self.qp_radio.setChecked(True) self.bitrate_radio.setChecked(False) qp = str(self.app.fastflix.config.encoder_opt(self.profile_name, self.qp_name)) @@ -441,6 +444,7 @@ def init_max_mux(self): def reload(self): """This will reset the current settings to what is set in "current_video", useful for return from queue""" global ffmpeg_extra_command + logger.debug("Update reload called") self.updating_settings = True for widget_name, opt in self.opts.items(): data = getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, opt) @@ -464,6 +468,7 @@ def reload(self): if getattr(self, "qp_radio", None): bitrate = getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, "bitrate", None) if bitrate: + self.mode = "Bitrate" self.qp_radio.setChecked(False) self.bitrate_radio.setChecked(True) for i, rec in enumerate(self.recommended_bitrates): @@ -474,6 +479,7 @@ def reload(self): self.widgets.bitrate.setCurrentText("Custom") self.widgets.custom_bitrate.setText(bitrate.rstrip("k")) else: + self.mode = self.qp_name self.qp_radio.setChecked(True) self.bitrate_radio.setChecked(False) qp = str(getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, self.qp_name)) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 666dc05d..b7624465 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -32,7 +32,8 @@ def build_audio(audio_tracks): if track.conversion_codec not in lossless: bitrate = f"--audio-bitrate {track.outdex}?{track.conversion_bitrate.rstrip('k')} " command_list.append( - f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate} --audio-metadata {track.outdex}?clear" + f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate} " + f"--audio-metadata {track.outdex}?clear" ) if track.title: @@ -169,10 +170,10 @@ def build(fastflix: FastFlix): "hevc", (f"--vbr {settings.bitrate.rstrip('k')}" if settings.bitrate else f"--cqp {settings.cqp}"), vbv, - (f"--vbr-quality {settings.vbr_target}" if settings.vbr_target is not None else ""), - (f"--qp-init {init_q}" if init_q else ""), - (f"--qp-min {min_q}" if min_q else ""), - (f"--qp-max {max_q}" if max_q else ""), + (f"--vbr-quality {settings.vbr_target}" if settings.vbr_target is not None and settings.bitrate else ""), + (f"--qp-init {init_q}" if init_q and settings.bitrate else ""), + (f"--qp-min {min_q}" if min_q and settings.bitrate else ""), + (f"--qp-max {max_q}" if max_q and settings.bitrate else ""), (f"--bframes {settings.b_frames}" if settings.b_frames else ""), (f"--ref {settings.ref}" if settings.ref else ""), f"--bref-mode {settings.b_ref_mode}", diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 7c365768..29ba99f5 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -390,7 +390,7 @@ def setting_change(self, update=True): self.updating_settings = False def update_video_encoder_settings(self): - + logger.debug("Updating video settings") settings = NVEncCSettings( preset=self.widgets.preset.currentText().split("-")[0].strip(), # profile=self.widgets.profile.currentText(), @@ -423,6 +423,7 @@ def update_video_encoder_settings(self): settings.cqp = q_value if encode_type == "qp" else None settings.bitrate = q_value if encode_type == "bitrate" else None self.app.fastflix.current_video.video_settings.video_encoder_settings = settings + logger.debug(settings) def set_mode(self, x): self.mode = x.text() diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index 7bd8ae4d..c3150e18 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -97,7 +97,6 @@ class NVEncCSettings(EncoderSettings): profile: str = "auto" bitrate: Optional[str] = "5000k" cqp: Optional[str] = None - cq: int = 0 aq: str = "off" aq_strength: int = 0 lookahead: Optional[int] = None diff --git a/fastflix/models/queue.py b/fastflix/models/queue.py index 283824d8..74be1a67 100644 --- a/fastflix/models/queue.py +++ b/fastflix/models/queue.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -from typing import List, Optional, Union +from typing import List import os from pathlib import Path import logging from box import Box, BoxError -from pydantic import BaseModel, Field -from appdirs import user_data_dir from fastflix.models.video import Video, VideoSettings, Status, Crop from fastflix.models.encode import AudioTrack, SubtitleTrack, AttachmentTrack diff --git a/fastflix/widgets/about.py b/fastflix/widgets/about.py index dae63de7..50d8c817 100644 --- a/fastflix/widgets/about.py +++ b/fastflix/widgets/about.py @@ -18,7 +18,7 @@ def __init__(self, parent=None): super(About, self).__init__(parent) layout = QtWidgets.QGridLayout() - self.setMinimumSize(400, 400) + self.setMinimumSize(QtCore.QSize(400, 400)) build_file = Path(base_path, "build_version") diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 9243c4a1..9ee4dee5 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -11,7 +11,7 @@ logger = logging.getLogger("fastflix") -__all__ = ["ThumbnailCreator", "ExtractSubtitleSRT", "SubtitleFix"] +__all__ = ["ThumbnailCreator", "ExtractSubtitleSRT", "SubtitleFix", "ExtractHDR10"] class ThumbnailCreator(QtCore.QThread): diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index a2f0296f..b482b6eb 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1206,7 +1206,6 @@ def reload_video_from_queue(self, video: Video): self.enable_all() self.app.fastflix.current_video.status = Status() - self.loading_video = False self.page_update() From d87fcf350bdedbc24bc6b79a5992bf8da2d95684 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 7 Feb 2021 19:31:16 -0600 Subject: [PATCH 45/50] Adding NVENCC AVC --- CHANGES | 2 +- fastflix/application.py | 2 + fastflix/encoders/nvencc_avc/__init__.py | 0 .../encoders/nvencc_avc/command_builder.py | 209 +++++++++ fastflix/encoders/nvencc_avc/main.py | 104 +++++ .../encoders/nvencc_avc/settings_panel.py | 439 ++++++++++++++++++ .../encoders/nvencc_hevc/command_builder.py | 4 - .../encoders/nvencc_hevc/settings_panel.py | 1 - fastflix/models/config.py | 2 + fastflix/models/encode.py | 31 ++ fastflix/models/video.py | 2 + fastflix/widgets/container.py | 2 +- fastflix/widgets/main.py | 2 +- fastflix/widgets/profile_window.py | 3 + 14 files changed, 795 insertions(+), 8 deletions(-) create mode 100644 fastflix/encoders/nvencc_avc/__init__.py create mode 100644 fastflix/encoders/nvencc_avc/command_builder.py create mode 100644 fastflix/encoders/nvencc_avc/main.py create mode 100644 fastflix/encoders/nvencc_avc/settings_panel.py diff --git a/CHANGES b/CHANGES index 109369cc..73958f88 100644 --- a/CHANGES +++ b/CHANGES @@ -3,7 +3,7 @@ ## Version 4.2.0 * Adding #109 NVENC HEVC support based on FFmpeg -* Adding NVEenC encoder for HEVC +* Adding NVEenC encoder for HEVC and AVC * Adding #166 More robust queue that is recoverable * Adding ability to extract HDR10+ metadata if hdr10plus_parser is detected on path * Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne) diff --git a/fastflix/application.py b/fastflix/application.py index 358568ab..a6710112 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -56,6 +56,7 @@ def init_encoders(app: FastFlixApp, **_): from fastflix.encoders.vp9 import main as vp9_plugin from fastflix.encoders.webp import main as webp_plugin from fastflix.encoders.nvencc_hevc import main as nvencc_plugin + from fastflix.encoders.nvencc_avc import main as nvencc_avc_plugin encoders = [ hevc_plugin, @@ -72,6 +73,7 @@ def init_encoders(app: FastFlixApp, **_): if app.fastflix.config.nvencc: encoders.insert(1, nvencc_plugin) + encoders.insert(7, nvencc_avc_plugin) app.fastflix.encoders = { encoder.name: encoder diff --git a/fastflix/encoders/nvencc_avc/__init__.py b/fastflix/encoders/nvencc_avc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastflix/encoders/nvencc_avc/command_builder.py b/fastflix/encoders/nvencc_avc/command_builder.py new file mode 100644 index 00000000..d7c5fab2 --- /dev/null +++ b/fastflix/encoders/nvencc_avc/command_builder.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +from typing import List +import logging + +from fastflix.encoders.common.helpers import Command +from fastflix.models.encode import NVEncCAVCSettings +from fastflix.models.video import SubtitleTrack, Video +from fastflix.models.fastflix import FastFlix +from fastflix.flix import unixy + +lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] + +logger = logging.getLogger("fastflix") + + +def build_audio(audio_tracks): + command_list = [] + copies = [] + track_ids = set() + + for track in audio_tracks: + if track.index in track_ids: + logger.warning("NVEncC does not support copy and duplicate of audio tracks!") + track_ids.add(track.index) + if track.language: + command_list.append(f"--audio-metadata {track.outdex}?language={track.language}") + if not track.conversion_codec or track.conversion_codec == "none": + copies.append(str(track.outdex)) + elif track.conversion_codec: + downmix = f"--audio-stream {track.outdex}?:{track.downmix}" if track.downmix else "" + bitrate = "" + if track.conversion_codec not in lossless: + bitrate = f"--audio-bitrate {track.outdex}?{track.conversion_bitrate.rstrip('k')} " + command_list.append( + f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate} " + f"--audio-metadata {track.outdex}?clear" + ) + + if track.title: + command_list.append( + f'--audio-metadata {track.outdex}?title="{track.title}" ' + f'--audio-metadata {track.outdex}?handler="{track.title}" ' + ) + + return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" + + +def build_subtitle(subtitle_tracks: List[SubtitleTrack]) -> str: + command_list = [] + copies = [] + for i, track in enumerate(subtitle_tracks, start=1): + if track.burn_in: + command_list.append(f"--vpp-subburn track={i}") + else: + copies.append(str(i)) + if track.disposition: + command_list.append(f"--sub-disposition {i}?{track.disposition}") + command_list.append(f"--sub-metadata {i}?language='{track.language}'") + + return f" --sub-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" + + +def build(fastflix: FastFlix): + video: Video = fastflix.current_video + settings: NVEncCAVCSettings = fastflix.current_video.video_settings.video_encoder_settings + + master_display = None + if fastflix.current_video.master_display: + master_display = ( + f'--master-display "G{fastflix.current_video.master_display.green}' + f"B{fastflix.current_video.master_display.blue}" + f"R{fastflix.current_video.master_display.red}" + f"WP{fastflix.current_video.master_display.white}" + f'L{fastflix.current_video.master_display.luminance}"' + ) + + max_cll = None + if fastflix.current_video.cll: + max_cll = f'--max-cll "{fastflix.current_video.cll}"' + + dhdr = None + if settings.hdr10plus_metadata: + dhdr = f'--dhdr10-info "{settings.hdr10plus_metadata}"' + + trim = "" + try: + if "/" in video.frame_rate: + over, under = [int(x) for x in video.frame_rate.split("/")] + rate = over / under + else: + rate = float(video.frame_rate) + except Exception: + logger.exception("Could not get framerate of this movie!") + else: + if video.video_settings.end_time: + end_frame = int(video.video_settings.end_time * rate) + start_frame = 0 + if video.video_settings.start_time: + start_frame = int(video.video_settings.start_time * rate) + trim = f"--trim {start_frame}:{end_frame}" + elif video.video_settings.start_time: + trim = f"--seek {video.video_settings.start_time}" + + if (video.frame_rate != video.average_frame_rate) and trim: + logger.warning("Cannot use 'trim' when working with variable frame rate videos") + trim = "" + + transform = "" + if video.video_settings.vertical_flip or video.video_settings.horizontal_flip: + transform = f"--vpp-transform flip_x={'true' if video.video_settings.horizontal_flip else 'false'},flip_y={'true' if video.video_settings.vertical_flip else 'false'}" + + remove_hdr = "" + if video.video_settings.remove_hdr: + remove_type = ( + video.video_settings.tone_map + if video.video_settings.tone_map in ("mobius", "hable", "reinhard") + else "mobius" + ) + remove_hdr = f"--vpp-colorspace hdr2sdr={remove_type}" if video.video_settings.remove_hdr else "" + + crop = "" + if video.video_settings.crop: + crop = f"--crop {video.video_settings.crop.left},{video.video_settings.crop.top},{video.video_settings.crop.right},{video.video_settings.crop.bottom}" + + vbv = "" + if video.video_settings.maxrate: + vbv = f"--max-bitrate {video.video_settings.maxrate} --vbv-bufsize {video.video_settings.bufsize}" + + init_q = settings.init_q_i + if settings.init_q_i and settings.init_q_p and settings.init_q_b: + init_q = f"{settings.init_q_i}:{settings.init_q_p}:{settings.init_q_b}" + + min_q = settings.min_q_i + if settings.min_q_i and settings.min_q_p and settings.min_q_b: + min_q = f"{settings.min_q_i}:{settings.min_q_p}:{settings.min_q_b}" + + max_q = settings.max_q_i + if settings.max_q_i and settings.max_q_p and settings.max_q_b: + max_q = f"{settings.max_q_i}:{settings.max_q_p}:{settings.max_q_b}" + + try: + stream_id = int(video.current_video_stream["id"], 16) + except Exception: + if len(video.streams.video) > 1: + logger.warning("Could not get stream ID from source, the proper video track may not be selected!") + stream_id = None + + aq = "--no-aq" + if settings.aq.lower() == "spatial": + aq = f"--aq --aq-strength {settings.aq_strength}" + elif settings.aq.lower() == "temporal": + aq = f"--aq-temporal --aq-strength {settings.aq_strength}" + + command = [ + f'"{unixy(fastflix.config.nvencc)}"', + "-i", + f'"{unixy(video.source)}"', + (f"--video-streamid {stream_id}" if stream_id else ""), + trim, + (f"--vpp-rotate {video.video_settings.rotate}" if video.video_settings.rotate else ""), + transform, + (f'--output-res {video.video_settings.scale.replace(":", "x")}' if video.video_settings.scale else ""), + crop, + (f"--video-metadata clear" if video.video_settings.remove_metadata else "--video-metadata copy"), + (f'--video-metadata title="{video.video_settings.video_title}"' if video.video_settings.video_title else ""), + ("--chapter-copy" if video.video_settings.copy_chapters else ""), + "-c", + "avc", + (f"--vbr {settings.bitrate.rstrip('k')}" if settings.bitrate else f"--cqp {settings.cqp}"), + vbv, + (f"--vbr-quality {settings.vbr_target}" if settings.vbr_target is not None and settings.bitrate else ""), + (f"--qp-init {init_q}" if init_q and settings.bitrate else ""), + (f"--qp-min {min_q}" if min_q and settings.bitrate else ""), + (f"--qp-max {max_q}" if max_q and settings.bitrate else ""), + (f"--bframes {settings.b_frames}" if settings.b_frames else ""), + (f"--ref {settings.ref}" if settings.ref else ""), + f"--bref-mode {settings.b_ref_mode}", + "--preset", + settings.preset, + (f"--lookahead {settings.lookahead}" if settings.lookahead else ""), + aq, + "--colormatrix", + (video.video_settings.color_space or "auto"), + "--transfer", + (video.video_settings.color_transfer or "auto"), + "--colorprim", + (video.video_settings.color_primaries or "auto"), + "--multipass", + settings.multipass, + "--mv-precision", + settings.mv_precision, + "--chromaloc", + "auto", + "--colorrange", + "auto", + f"--avsync {'cfr' if video.frame_rate == video.average_frame_rate else 'vfr'}", + (f"--interlace {video.interlaced}" if video.interlaced else ""), + ("--vpp-yadif" if video.video_settings.deinterlace else ""), + (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), + remove_hdr, + "--psnr --ssim" if settings.metrics else "", + build_audio(video.video_settings.audio_tracks), + build_subtitle(video.video_settings.subtitle_tracks), + settings.extra, + "-o", + f'"{unixy(video.video_settings.output_path)}"', + ] + + return [Command(command=" ".join(x for x in command if x), name="NVEncC Encode", exe="NVEncE")] diff --git a/fastflix/encoders/nvencc_avc/main.py b/fastflix/encoders/nvencc_avc/main.py new file mode 100644 index 00000000..a6c34e95 --- /dev/null +++ b/fastflix/encoders/nvencc_avc/main.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +__author__ = "Chris Griffith" +from pathlib import Path + +import pkg_resources + +name = "AVC (NVEncC)" + +video_extension = "mkv" +video_dimension_divisor = 1 +icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_nvencc.png")).resolve()) + +enable_subtitles = True +enable_audio = True +enable_attachments = False + +audio_formats = [ + "aac", + "aac_mf", + "libfdk_aac", + "ac3", + "ac3_fixed", + "ac3_mf", + "adpcm_adx", + "g722", + "g726", + "g726le", + "adpcm_ima_qt", + "adpcm_ima_ssi", + "adpcm_ima_wav", + "adpcm_ms", + "adpcm_swf", + "adpcm_yamaha", + "alac", + "libopencore_amrnb", + "libvo_amrwbenc", + "aptx", + "aptx_hd", + "comfortnoise", + "dca", + "eac3", + "flac", + "g723_1", + "libgsm", + "libgsm_ms", + "libilbc", + "mlp", + "mp2", + "mp2fixed", + "libtwolame", + "mp3_mf", + "libmp3lame", + "nellymoser", + "opus", + "libopus", + "pcm_alaw", + "pcm_dvd", + "pcm_f32be", + "pcm_f32le", + "pcm_f64be", + "pcm_f64le", + "pcm_mulaw", + "pcm_s16be", + "pcm_s16be_planar", + "pcm_s16le", + "pcm_s16le_planar", + "pcm_s24be", + "pcm_s24daud", + "pcm_s24le", + "pcm_s24le_planar", + "pcm_s32be", + "pcm_s32le", + "pcm_s32le_planar", + "pcm_s64be", + "pcm_s64le", + "pcm_s8", + "pcm_s8_planar", + "pcm_u16be", + "pcm_u16le", + "pcm_u24be", + "pcm_u24le", + "pcm_u32be", + "pcm_u32le", + "pcm_u8", + "pcm_vidc", + "real_144", + "roq_dpcm", + "s302m", + "sbc", + "sonic", + "sonicls", + "libspeex", + "truehd", + "tta", + "vorbis", + "libvorbis", + "wavpack", + "wmav1", + "wmav2", +] + +from fastflix.encoders.nvencc_avc.command_builder import build +from fastflix.encoders.nvencc_avc.settings_panel import NVENCCAVC as settings_panel diff --git a/fastflix/encoders/nvencc_avc/settings_panel.py b/fastflix/encoders/nvencc_avc/settings_panel.py new file mode 100644 index 00000000..96a41c99 --- /dev/null +++ b/fastflix/encoders/nvencc_avc/settings_panel.py @@ -0,0 +1,439 @@ +# -*- coding: utf-8 -*- +import logging +from typing import List, Optional + +from box import Box +from qtpy import QtCore, QtWidgets, QtGui + +from fastflix.encoders.common.setting_panel import SettingPanel +from fastflix.language import t +from fastflix.models.encode import NVEncCAVCSettings +from fastflix.models.fastflix_app import FastFlixApp +from fastflix.shared import link +from fastflix.exceptions import FastFlixInternalException +from fastflix.resources import loading_movie, warning_icon + +logger = logging.getLogger("fastflix") + +presets = ["default", "performance", "quality"] + +recommended_bitrates = [ + "200k (320x240p @ 30fps)", + "300k (640x360p @ 30fps)", + "1000k (640x480p @ 30fps)", + "1750k (1280x720p @ 30fps)", + "2500k (1280x720p @ 60fps)", + "4000k (1920x1080p @ 30fps)", + "5000k (1920x1080p @ 60fps)", + "7000k (2560x1440p @ 30fps)", + "10000k (2560x1440p @ 60fps)", + "15000k (3840x2160p @ 30fps)", + "20000k (3840x2160p @ 60fps)", + "Custom", +] + +recommended_crfs = [ + "28", + "27", + "26", + "25", + "24", + "23", + "22", + "21", + "20", + "19", + "18", + "17", + "16", + "15", + "14", + "Custom", +] + + +def get_breaker(): + breaker_line = QtWidgets.QWidget() + breaker_line.setMaximumHeight(2) + breaker_line.setStyleSheet("background-color: #ccc; margin: auto 0; padding: auto 0;") + return breaker_line + + +class NVENCCAVC(SettingPanel): + profile_name = "nvencc_avc" + hdr10plus_signal = QtCore.Signal(str) + hdr10plus_ffmpeg_signal = QtCore.Signal(str) + + def __init__(self, parent, main, app: FastFlixApp): + super().__init__(parent, main, app) + self.main = main + self.app = app + + grid = QtWidgets.QGridLayout() + + self.widgets = Box(mode=None) + + self.mode = "Bitrate" + self.updating_settings = False + + grid.addLayout(self.init_modes(), 0, 2, 4, 4) + grid.addLayout(self._add_custom(title="Custom NVEncC options", disable_both_passes=True), 10, 0, 1, 6) + + grid.addLayout(self.init_preset(), 0, 0, 1, 2) + # grid.addLayout(self.init_profile(), 1, 0, 1, 2) + # grid.addLayout(self.init_tier(), 1, 0, 1, 2) + grid.addLayout(self.init_multipass(), 2, 0, 1, 2) + grid.addLayout(self.init_lookahead(), 3, 0, 1, 2) + + breaker = QtWidgets.QHBoxLayout() + breaker_label = QtWidgets.QLabel(t("Advanced")) + breaker_label.setFont(QtGui.QFont("helvetica", 8, weight=55)) + + breaker.addWidget(get_breaker(), stretch=1) + breaker.addWidget(breaker_label, alignment=QtCore.Qt.AlignHCenter) + breaker.addWidget(get_breaker(), stretch=1) + + grid.addLayout(breaker, 4, 0, 1, 6) + + grid.addLayout(self.init_aq(), 5, 0, 1, 2) + grid.addLayout(self.init_aq_strength(), 6, 0, 1, 2) + grid.addLayout(self.init_mv_precision(), 7, 0, 1, 2) + + qp_line = QtWidgets.QHBoxLayout() + qp_line.addLayout(self.init_vbr_target()) + qp_line.addStretch(1) + qp_line.addLayout(self.init_init_q()) + qp_line.addStretch(1) + qp_line.addLayout(self.init_min_q()) + qp_line.addStretch(1) + qp_line.addLayout(self.init_max_q()) + + grid.addLayout(qp_line, 5, 2, 1, 4) + + advanced = QtWidgets.QHBoxLayout() + advanced.addLayout(self.init_ref()) + advanced.addStretch(1) + advanced.addLayout(self.init_b_frames()) + advanced.addStretch(1) + advanced.addLayout(self.init_level()) + advanced.addStretch(1) + advanced.addLayout(self.init_b_ref_mode()) + advanced.addStretch(1) + advanced.addLayout(self.init_metrics()) + grid.addLayout(advanced, 6, 2, 1, 4) + + grid.addLayout(self.init_dhdr10_info(), 7, 2, 1, 4) + + self.ffmpeg_level = QtWidgets.QLabel() + grid.addWidget(self.ffmpeg_level, 8, 2, 1, 4) + + grid.setRowStretch(9, 1) + + guide_label = QtWidgets.QLabel( + link("https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md", t("NVEncC Options")) + ) + + warning_label = QtWidgets.QLabel() + warning_label.setPixmap(QtGui.QIcon(warning_icon).pixmap(22)) + + guide_label.setAlignment(QtCore.Qt.AlignBottom) + guide_label.setOpenExternalLinks(True) + grid.addWidget(guide_label, 11, 0, 1, 4) + grid.addWidget(warning_label, 11, 4, 1, 1, alignment=QtCore.Qt.AlignRight) + grid.addWidget(QtWidgets.QLabel(t("NVEncC Encoder support is still experimental!")), 11, 5, 1, 1) + + self.setLayout(grid) + self.hide() + self.hdr10plus_signal.connect(self.done_hdr10plus_extract) + self.hdr10plus_ffmpeg_signal.connect(lambda x: self.ffmpeg_level.setText(x)) + + def init_preset(self): + return self._add_combo_box( + label="Preset", + widget_name="preset", + options=presets, + tooltip="preset: The slower the preset, the better the compression and quality", + connect="default", + opt="preset", + ) + + def init_tune(self): + return self._add_combo_box( + label="Tune", + widget_name="tune", + tooltip="Tune the settings for a particular type of source or situation\nhq - High Quality, ll - Low Latency, ull - Ultra Low Latency", + options=["hq", "ll", "ull", "lossless"], + opt="tune", + ) + + # def init_profile(self): + # # TODO auto + # return self._add_combo_box( + # label="Profile_encoderopt", + # widget_name="profile", + # tooltip="Enforce an encode profile", + # options=["main", "main10"], + # opt="profile", + # ) + + # def init_tier(self): + # return self._add_combo_box( + # label="Tier", + # tooltip="Set the encoding tier", + # widget_name="tier", + # options=["main", "high"], + # opt="tier", + # ) + + def init_aq(self): + return self._add_combo_box( + label="Adaptive Quantization", + tooltip="", + widget_name="aq", + options=["off", "spatial", "temporal"], + opt="aq", + ) + + def init_aq_strength(self): + return self._add_combo_box( + label="AQ Strength", + tooltip="", + widget_name="aq_strength", + options=["Auto"] + [str(x) for x in range(1, 16)], + opt="aq_strength", + ) + + def init_multipass(self): + return self._add_combo_box( + label="Multipass", + tooltip="", + widget_name="multipass", + options=["None", "2pass-quarter", "2pass-full"], + opt="multipass", + ) + + def init_mv_precision(self): + return self._add_combo_box( + label="Motion vector accuracy", + tooltip="Q-pel is highest precision", + widget_name="mv_precision", + options=["Auto", "Q-pel", "half-pel", "full-pel"], + opt="mv_precision", + ) + + def init_lookahead(self): + return self._add_combo_box( + label="Lookahead", + tooltip="", + widget_name="lookahead", + opt="lookahead", + options=["off"] + [str(x) for x in range(1, 33)], + ) + + def init_level(self): + layout = self._add_combo_box( + label="Level", + tooltip="Set the encoding level restriction", + widget_name="level", + options=[ + t("Auto"), + "1.0", + "2.0", + "2.1", + "3.0", + "3.1", + "4.0", + "4.1", + "5.0", + "5.1", + "5.2", + "6.0", + "6.1", + "6.2", + ], + opt="level", + ) + self.widgets.level.setMinimumWidth(60) + return layout + + def init_b_ref_mode(self): + layout = self._add_combo_box( + label="B Ref Mode", + tooltip="Use B frames as references", + widget_name="b_ref_mode", + opt="b_ref_mode", + options=["disabled", "each", "middle"], + min_width=60, + ) + return layout + + @staticmethod + def _qp_range(): + return [str(x) for x in range(0, 52)] + + def init_min_q(self): + layout = QtWidgets.QHBoxLayout() + layout.addWidget(QtWidgets.QLabel(t("Min Q"))) + layout.addWidget( + self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=45, opt="min_q_i") + ) + layout.addWidget( + self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=45, opt="min_q_p") + ) + layout.addWidget( + self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=45, opt="min_q_b") + ) + return layout + + def init_init_q(self): + layout = QtWidgets.QHBoxLayout() + layout.addWidget(QtWidgets.QLabel(t("Init Q"))) + layout.addWidget( + self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=45, opt="init_q_i") + ) + layout.addWidget( + self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=45, opt="init_q_p") + ) + layout.addWidget( + self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=45, opt="init_q_b") + ) + return layout + + def init_max_q(self): + layout = QtWidgets.QHBoxLayout() + layout.addWidget(QtWidgets.QLabel(t("Max Q"))) + layout.addWidget( + self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=45, opt="max_q_i") + ) + layout.addWidget( + self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=45, opt="max_q_p") + ) + layout.addWidget( + self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=45, opt="max_q_b") + ) + return layout + + def init_vbr_target(self): + return self._add_combo_box( + widget_name="vbr_target", + label="VBR Target", + options=[t("Auto")] + self._qp_range(), + opt="vbr_target", + min_width=60, + ) + + def init_b_frames(self): + return self._add_combo_box( + widget_name="b_frames", + label="B Frames", + options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"], + opt="b_frames", + min_width=60, + ) + + def init_ref(self): + return self._add_combo_box( + widget_name="ref", + label="Ref Frames", + options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"], + opt="ref", + min_width=60, + ) + + def init_metrics(self): + return self._add_check_box( + widget_name="metrics", + opt="metrics", + label="Metrics", + tooltip="Calculate PSNR and SSIM and show in the encoder output", + ) + + def init_dhdr10_info(self): + layout = self._add_file_select( + label="HDR10+ Metadata", + widget_name="hdr10plus_metadata", + button_action=lambda: self.dhdr10_update(), + tooltip="dhdr10_info: Path to HDR10+ JSON metadata file", + ) + self.labels["hdr10plus_metadata"].setFixedWidth(200) + self.extract_button = QtWidgets.QPushButton(t("Extract HDR10+")) + self.extract_button.hide() + self.extract_button.clicked.connect(self.extract_hdr10plus) + + self.extract_label = QtWidgets.QLabel(self) + self.extract_label.hide() + self.movie = QtGui.QMovie(loading_movie) + self.movie.setScaledSize(QtCore.QSize(25, 25)) + self.extract_label.setMovie(self.movie) + + layout.addWidget(self.extract_button) + layout.addWidget(self.extract_label) + + return layout + + def init_modes(self): + layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="cqp") + return layout + + def mode_update(self): + self.widgets.custom_cqp.setDisabled(self.widgets.cqp.currentText() != "Custom") + self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom") + self.main.build_commands() + + def setting_change(self, update=True): + if self.updating_settings: + return + self.updating_settings = True + + if update: + self.main.page_update() + self.updating_settings = False + + def update_video_encoder_settings(self): + settings = NVEncCAVCSettings( + preset=self.widgets.preset.currentText().split("-")[0].strip(), + # profile=self.widgets.profile.currentText(), + # tier=self.widgets.tier.currentText(), + lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None, + aq=self.widgets.aq.currentText(), + aq_strength=self.widgets.aq_strength.currentIndex(), + hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"), + multipass=self.widgets.multipass.currentText(), + mv_precision=self.widgets.mv_precision.currentText(), + init_q_i=self.widgets.init_q_i.currentText() if self.widgets.init_q_i.currentIndex() != 0 else None, + init_q_p=self.widgets.init_q_p.currentText() if self.widgets.init_q_p.currentIndex() != 0 else None, + init_q_b=self.widgets.init_q_b.currentText() if self.widgets.init_q_b.currentIndex() != 0 else None, + max_q_i=self.widgets.max_q_i.currentText() if self.widgets.max_q_i.currentIndex() != 0 else None, + max_q_p=self.widgets.max_q_p.currentText() if self.widgets.max_q_p.currentIndex() != 0 else None, + max_q_b=self.widgets.max_q_b.currentText() if self.widgets.max_q_b.currentIndex() != 0 else None, + min_q_i=self.widgets.min_q_i.currentText() if self.widgets.min_q_i.currentIndex() != 0 else None, + min_q_p=self.widgets.min_q_p.currentText() if self.widgets.min_q_p.currentIndex() != 0 else None, + min_q_b=self.widgets.min_q_b.currentText() if self.widgets.min_q_b.currentIndex() != 0 else None, + extra=self.ffmpeg_extras, + metrics=self.widgets.metrics.isChecked(), + level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, + b_frames=self.widgets.b_frames.currentText() if self.widgets.b_frames.currentIndex() != 0 else None, + ref=self.widgets.ref.currentText() if self.widgets.ref.currentIndex() != 0 else None, + vbr_target=self.widgets.vbr_target.currentText() if self.widgets.vbr_target.currentIndex() > 0 else None, + b_ref_mode=self.widgets.b_ref_mode.currentText(), + ) + + encode_type, q_value = self.get_mode_settings() + settings.cqp = q_value if encode_type == "qp" else None + settings.bitrate = q_value if encode_type == "bitrate" else None + self.app.fastflix.current_video.video_settings.video_encoder_settings = settings + + def set_mode(self, x): + self.mode = x.text() + for group in ("init", "max", "min"): + for frame_type in ("i", "p", "b"): + self.widgets[f"{group}_q_{frame_type}"].setEnabled(self.mode.lower() == "bitrate") + self.widgets.vbr_target.setEnabled(self.mode.lower() == "bitrate") + self.main.build_commands() + + def new_source(self): + super().new_source() + if self.app.fastflix.current_video.hdr10_plus: + self.extract_button.show() + else: + self.extract_button.hide() diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index b7624465..ec90217c 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -145,8 +145,6 @@ def build(fastflix: FastFlix): logger.warning("Could not get stream ID from source, the proper video track may not be selected!") stream_id = None - profile = "main" if video.current_video_stream.bit_depth == 8 else "main10" - aq = "--no-aq" if settings.aq.lower() == "spatial": aq = f"--aq --aq-strength {settings.aq_strength}" @@ -179,8 +177,6 @@ def build(fastflix: FastFlix): f"--bref-mode {settings.b_ref_mode}", "--preset", settings.preset, - "--profile", - profile, "--tier", settings.tier, (f"--lookahead {settings.lookahead}" if settings.lookahead else ""), diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 29ba99f5..c9189444 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -423,7 +423,6 @@ def update_video_encoder_settings(self): settings.cqp = q_value if encode_type == "qp" else None settings.bitrate = q_value if encode_type == "bitrate" else None self.app.fastflix.current_video.video_settings.video_encoder_settings = settings - logger.debug(settings) def set_mode(self, x): self.mode = x.text() diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 2d648de5..fd87f536 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -22,6 +22,7 @@ x264Settings, x265Settings, NVEncCSettings, + NVEncCAVCSettings, setting_types, ) from fastflix.version import __version__ @@ -71,6 +72,7 @@ class Profile(BaseModel): copy_settings: Optional[CopySettings] = None ffmpeg_hevc_nvenc: Optional[FFmpegNVENCSettings] = None nvencc_hevc: Optional[NVEncCSettings] = None + nvencc_avc: Optional[NVEncCAVCSettings] = None empty_profile = Profile(x265=x265Settings()) diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index c3150e18..82c7cbdb 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -121,6 +121,36 @@ class NVEncCSettings(EncoderSettings): metrics: bool = True +class NVEncCAVCSettings(EncoderSettings): + name = "AVC (NVEncC)" + preset: str = "quality" + profile: str = "auto" + bitrate: Optional[str] = "5000k" + cqp: Optional[str] = None + aq: str = "off" + aq_strength: int = 0 + lookahead: Optional[int] = None + tier: str = "high" + level: Optional[str] = None + hdr10plus_metadata: str = "" + multipass: str = "2pass-full" + mv_precision: str = "Auto" + init_q_i: Optional[str] = None + init_q_p: Optional[str] = None + init_q_b: Optional[str] = None + min_q_i: Optional[str] = None + min_q_p: Optional[str] = None + min_q_b: Optional[str] = None + max_q_i: Optional[str] = None + max_q_p: Optional[str] = None + max_q_b: Optional[str] = None + vbr_target: Optional[str] = None + b_frames: Optional[str] = None + b_ref_mode: str = "disabled" + ref: Optional[str] = None + metrics: bool = True + + class rav1eSettings(EncoderSettings): name = "AV1 (rav1e)" speed: str = "-1" @@ -199,4 +229,5 @@ class CopySettings(EncoderSettings): "copy_settings": CopySettings, "ffmpeg_hevc_nvenc": FFmpegNVENCSettings, "nvencc_hevc": NVEncCSettings, + "nvencc_avc": NVEncCAVCSettings, } diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 1e36064f..3a1b1dba 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -21,6 +21,7 @@ x264Settings, x265Settings, NVEncCSettings, + NVEncCAVCSettings, ) __all__ = ["VideoSettings", "Status", "Video", "Crop", "Status"] @@ -97,6 +98,7 @@ class VideoSettings(BaseModel): CopySettings, FFmpegNVENCSettings, NVEncCSettings, + NVEncCAVCSettings, ] = None audio_tracks: List[AudioTrack] = Field(default_factory=list) subtitle_tracks: List[SubtitleTrack] = Field(default_factory=list) diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 891e69e6..0af3c32b 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -47,7 +47,7 @@ def __init__(self, app: FastFlixApp, **kwargs): self.profile = ProfileWindow(self.app, self.main) self.setCentralWidget(self.main) - self.setMinimumSize(QtCore.QSize(1200, 650)) + self.setMinimumSize(QtCore.QSize(1280, 650)) self.icon = QtGui.QIcon(main_icon) self.setWindowIcon(self.icon) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index b482b6eb..bba75954 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -565,7 +565,7 @@ def init_start_time(self): "vs a specific [exact] frame lookup. (GIF encodings use [fast])" ) self.widgets.fast_time.currentIndexChanged.connect(lambda: self.page_update(build_thumbnail=False)) - self.widgets.fast_time.setFixedWidth(75) + self.widgets.fast_time.setFixedWidth(65) layout.addWidget(QtWidgets.QLabel(" ")) layout.addWidget(self.widgets.fast_time, QtCore.Qt.AlignRight) group_box.setLayout(layout) diff --git a/fastflix/widgets/profile_window.py b/fastflix/widgets/profile_window.py index dc31cfe9..4357d1e9 100644 --- a/fastflix/widgets/profile_window.py +++ b/fastflix/widgets/profile_window.py @@ -23,6 +23,7 @@ x264Settings, x265Settings, NVEncCSettings, + NVEncCAVCSettings, FFmpegNVENCSettings, ) from fastflix.shared import error_message @@ -173,6 +174,8 @@ def save(self): new_profile.copy_settings = self.encoder elif isinstance(self.encoder, NVEncCSettings): new_profile.nvencc_hevc = self.encoder + elif isinstance(self.encoder, NVEncCAVCSettings): + new_profile.nvencc_avc = self.encoder elif isinstance(self.encoder, FFmpegNVENCSettings): new_profile.ffmpeg_hevc_nvenc = self.encoder else: From 6028ea270192b28dd88ba9c6e50a34fffb944216 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Mon, 8 Feb 2021 12:28:28 -0600 Subject: [PATCH 46/50] Fix timer signal Fix allowed to covert when no video ready Adding remaining / size eta for nvencc --- fastflix/conversion_worker.py | 1 + fastflix/data/languages.yaml | 2 + .../encoders/nvencc_hevc/command_builder.py | 47 ++++++++++-------- .../encoders/nvencc_hevc/settings_panel.py | 1 - fastflix/flix.py | 8 ++-- fastflix/models/video.py | 2 +- fastflix/widgets/main.py | 12 +++-- fastflix/widgets/panels/status_panel.py | 48 ++++++++++++------- 8 files changed, 77 insertions(+), 44 deletions(-) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index beed078a..7b70cc06 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -170,6 +170,7 @@ def start_command(): # Successfully encoded, do next one if it exists # First check if the current video has more commands video.status.current_command += 1 + log_queue.put("STOP_TIMER") if len(video.video_settings.conversion_commands) > video.status.current_command: logger.debug("About to run next command for this video") diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 913953ff..09b288e5 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -3050,3 +3050,5 @@ vsync: ita: vsync spa: vsync zho: vsync +There are no videos to start converting: + eng: There are no videos to start converting diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index ec90217c..512a8b1f 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from typing import List +from typing import List, Dict import logging from fastflix.encoders.common.helpers import Command @@ -13,49 +13,58 @@ logger = logging.getLogger("fastflix") -def build_audio(audio_tracks): +def get_stream_pos(streams) -> Dict: + return {x.index: i for i, x in enumerate(streams, start=1)} + + +def build_audio(audio_tracks, audio_streams): command_list = [] copies = [] track_ids = set() + stream_ids = get_stream_pos(audio_streams) - for track in audio_tracks: + for track in sorted(audio_tracks, key=lambda x: x.outdex): if track.index in track_ids: logger.warning("NVEncC does not support copy and duplicate of audio tracks!") track_ids.add(track.index) + audio_id = stream_ids[track.index] if track.language: - command_list.append(f"--audio-metadata {track.outdex}?language={track.language}") + command_list.append(f"--audio-metadata {audio_id}?language={track.language}") if not track.conversion_codec or track.conversion_codec == "none": - copies.append(str(track.outdex)) + copies.append(str(audio_id)) elif track.conversion_codec: - downmix = f"--audio-stream {track.outdex}?:{track.downmix}" if track.downmix else "" + downmix = f"--audio-stream {audio_id}?:{track.downmix}" if track.downmix else "" bitrate = "" if track.conversion_codec not in lossless: - bitrate = f"--audio-bitrate {track.outdex}?{track.conversion_bitrate.rstrip('k')} " + bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate.rstrip('k')} " command_list.append( - f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate} " - f"--audio-metadata {track.outdex}?clear" + f"{downmix} --audio-codec {audio_id}?{track.conversion_codec} {bitrate} " + f"--audio-metadata {audio_id}?clear" ) if track.title: command_list.append( - f'--audio-metadata {track.outdex}?title="{track.title}" ' - f'--audio-metadata {track.outdex}?handler="{track.title}" ' + f'--audio-metadata {audio_id}?title="{track.title}" ' + f'--audio-metadata {audio_id}?handler="{track.title}" ' ) return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" -def build_subtitle(subtitle_tracks: List[SubtitleTrack]) -> str: +def build_subtitle(subtitle_tracks: List[SubtitleTrack], subtitle_streams) -> str: command_list = [] copies = [] - for i, track in enumerate(subtitle_tracks, start=1): + stream_ids = get_stream_pos(subtitle_streams) + + for track in sorted(subtitle_tracks, key=lambda x: x.outdex): + sub_id = stream_ids[track.index] if track.burn_in: - command_list.append(f"--vpp-subburn track={i}") + command_list.append(f"--vpp-subburn track={sub_id}") else: - copies.append(str(i)) + copies.append(str(sub_id)) if track.disposition: - command_list.append(f"--sub-disposition {i}?{track.disposition}") - command_list.append(f"--sub-metadata {i}?language='{track.language}'") + command_list.append(f"--sub-disposition {sub_id}?{track.disposition}") + command_list.append(f"--sub-metadata {sub_id}?language='{track.language}'") return f" --sub-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" @@ -206,8 +215,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks), - build_subtitle(video.video_settings.subtitle_tracks), + build_audio(video.video_settings.audio_tracks, video.streams.audio), + build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle), settings.extra, "-o", f'"{unixy(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index c9189444..ae5a31ab 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -390,7 +390,6 @@ def setting_change(self, update=True): self.updating_settings = False def update_video_encoder_settings(self): - logger.debug("Updating video settings") settings = NVEncCSettings( preset=self.widgets.preset.currentText().split("-")[0].strip(), # profile=self.widgets.profile.currentText(), diff --git a/fastflix/flix.py b/fastflix/flix.py index 3364964e..1637b306 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -71,8 +71,7 @@ def guess_bit_depth(pix_fmt: str, color_primaries: str = None) -> int: if color_primaries and color_primaries.startswith("bt2020"): return 10 - else: - return 8 + return 8 def execute(command: List, work_dir: Union[Path, str] = None, timeout: int = None) -> CompletedProcess: @@ -153,7 +152,10 @@ def parse(app: FastFlixApp, **_): raise FlixError(f"Not a video file, FFprobe output: {data}") streams = Box({"video": [], "audio": [], "subtitle": [], "attachment": [], "data": []}) for track in data.streams: - if track.codec_type == "video" and track.get("disposition", {}).get("attached_pic"): + if track.codec_type == "video" and ( + track.get("disposition", {}).get("attached_pic") + or track.get("tags", {}).get("MIMETYPE", "").startswith("image") + ): streams.attachment.append(track) elif track.codec_type in streams: streams[track.codec_type].append(track) diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 3a1b1dba..f23c21da 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -136,7 +136,7 @@ class Video(BaseModel): work_path: Path = None format: Box = None - interlaced: bool = True + interlaced: Union[str, bool] = False hdr10_streams: List[Box] = Field(default_factory=list) hdr10_plus: List[int] = Field(default_factory=list) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index bba75954..f996ff61 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1588,14 +1588,11 @@ def set_convert_button(self, convert=True): @reusables.log_exception("fastflix", show_traceback=False) def encode_video(self): - # TODO make sure there is a video that can be encoded if self.converting: logger.debug(t("Canceling current encode")) self.app.fastflix.worker_queue.put(["cancel"]) self.video_options.queue.reset_pause_encode() return - else: - logger.debug(t("Starting conversion process")) if not self.app.fastflix.queue or self.app.fastflix.current_video: add_current = True @@ -1606,7 +1603,14 @@ def encode_video(self): return requests = ["add_items", str(self.app.fastflix.log_path)] - # TODO here check for videos if ready. Make shared function for ready? + for video in self.app.fastflix.queue: + if video.status.ready: + break + else: + error_message(t("There are no videos to start converting")) + return + + logger.debug(t("Starting conversion process")) self.converting = True self.set_convert_button(False) diff --git a/fastflix/widgets/panels/status_panel.py b/fastflix/widgets/panels/status_panel.py index 2d7d1cb2..25f5cf42 100644 --- a/fastflix/widgets/panels/status_panel.py +++ b/fastflix/widgets/panels/status_panel.py @@ -20,6 +20,7 @@ class StatusPanel(QtWidgets.QWidget): speed = QtCore.Signal(str) bitrate = QtCore.Signal(str) + nvencc_signal = QtCore.Signal(str) tick_signal = QtCore.Signal() def __init__(self, parent, app: FastFlixApp): @@ -29,7 +30,7 @@ def __init__(self, parent, app: FastFlixApp): self.current_video: Optional[Video] = None self.started_at = None - self.ticker_thread = ElapsedTimeTicker(self, self.main.status_update_signal, self.tick_signal) + self.ticker_thread = ElapsedTimeTicker(self, self.tick_signal) self.ticker_thread.start() layout = QtWidgets.QGridLayout() @@ -62,6 +63,7 @@ def __init__(self, parent, app: FastFlixApp): self.speed.connect(self.update_speed) self.bitrate.connect(self.update_bitrate) + self.nvencc_signal.connect(self.update_nvencc) self.main.status_update_signal.connect(self.on_status_update) self.tick_signal.connect(self.update_time_elapsed) @@ -122,6 +124,18 @@ def update_bitrate(self, bitrate): self.size_label.setText(f"{t('Size Estimate')}: {size_eta:.2f}MB") + def update_nvencc(self, raw_line): + """ + Example line: + [53.1%] 19/35 frames: 150.57 fps, 5010 kb/s, remain 0:01:55, GPU 10%, VE 96%, VD 42%, est out size 920.6MB + """ + for section in raw_line.split(","): + section = section.strip() + if section.startswith("remain"): + self.eta_label.setText(f"{t('Time Left')}: {section.rsplit(maxsplit=1)[1]}") + elif section.startswith("est out size"): + self.size_label.setText(f"{t('Size Estimate')}: {section.rsplit(maxsplit=1)[1]}") + def update_title_bar(self): pass @@ -152,6 +166,7 @@ def close(self): class Logs(QtWidgets.QTextBrowser): log_signal = QtCore.Signal(str) clear_window = QtCore.Signal(str) + timer_signal = QtCore.Signal(str) def __init__(self, parent, app: FastFlixApp, main, log_queue): super(Logs, self).__init__(parent) @@ -163,6 +178,7 @@ def __init__(self, parent, app: FastFlixApp, main, log_queue): self.current_command = None self.log_signal.connect(self.update_text) self.clear_window.connect(self.blank) + self.timer_signal.connect(self.timer_update) self.log_updater = LogUpdater(self, log_queue) self.log_updater.start() @@ -184,6 +200,8 @@ def update_text(self, msg): self.status_panel.bitrate.emit(frame.get("bitrate", "")) except Exception: pass + elif "remain" in msg: + self.status_panel.nvencc_signal.emit(msg) self.append(msg) def blank(self, data): @@ -195,15 +213,20 @@ def blank(self, data): self.current_command = None self.setText("") self.parent.update_title_bar() + self.parent.started_at = datetime.datetime.now(datetime.timezone.utc) + + def timer_update(self, cmd): + self.parent.ticker_thread.state_signal.emit(cmd == "START") def closeEvent(self, event): self.hide() class ElapsedTimeTicker(QtCore.QThread): - stop_signal = QtCore.Signal() + state_signal = QtCore.Signal(bool) + stop_signal = QtCore.Signal() # Clean exit of program - def __init__(self, parent, status_update_signal, tick_signal): + def __init__(self, parent, tick_signal): super().__init__(parent) self.parent = parent self.tick_signal = tick_signal @@ -211,7 +234,7 @@ def __init__(self, parent, status_update_signal, tick_signal): self.send_tick_signal = False self.stop_received = False - status_update_signal.connect(self.on_status_update) + self.state_signal.connect(self.set_state) self.stop_signal.connect(self.on_stop) def __del__(self): @@ -228,18 +251,8 @@ def run(self): logger.debug("Ticker thread stopped") - def on_status_update(self): - # update_type = msg.split("|")[0] - # - # if update_type in ("complete", "error", "cancelled", "converted"): - # self.send_tick_signal = False - # else: - # self.send_tick_signal = True - - if self.parent.main.converting: - self.send_tick_signal = True - else: - self.send_tick_signal = False + def set_state(self, state): + self.send_tick_signal = state def on_stop(self): self.stop_received = True @@ -259,6 +272,9 @@ def run(self): msg = self.log_queue.get() if msg.startswith("CLEAR_WINDOW"): self.parent.clear_window.emit(msg) + self.parent.timer_signal.emit("START") + elif msg == "STOP_TIMER": + self.parent.timer_signal.emit("STOP") elif msg == "UPDATE_QUEUE": self.parent.status_panel.main.video_options.update_queue(currently_encoding=self.parent.converting) else: From f478705638ca58b786a1b7b21686fc597bd39325 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 10 Feb 2021 10:00:46 -0600 Subject: [PATCH 47/50] Safer paths Adding output directory selection (config only) Fixing queue startup handling --- fastflix/conversion_worker.py | 5 ++++- fastflix/encoders/common/attachments.py | 3 ++- fastflix/encoders/common/helpers.py | 11 +++++------ fastflix/encoders/gif/command_builder.py | 3 ++- .../encoders/nvencc_avc/command_builder.py | 2 +- fastflix/flix.py | 3 ++- fastflix/models/config.py | 3 ++- fastflix/shared.py | 5 +++++ fastflix/widgets/about.py | 2 +- fastflix/widgets/background_tasks.py | 7 ++++--- fastflix/widgets/container.py | 5 ++++- fastflix/widgets/main.py | 11 +++++++++-- fastflix/widgets/panels/queue_panel.py | 18 ++++++++++++++---- requirements-build.txt | 1 + requirements.txt | 1 + 15 files changed, 57 insertions(+), 23 deletions(-) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 7b70cc06..500a1694 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -8,6 +8,7 @@ import reusables from appdirs import user_data_dir from box import Box +from pathvalidate import sanitize_filename from fastflix.command_runner import BackgroundRunner from fastflix.language import t @@ -135,7 +136,9 @@ def start_command(): reusables.remove_file_handlers(logger) new_file_handler = reusables.get_file_handler( log_path - / f"flix_conversion_{video.video_settings.video_title or video.video_settings.output_path.stem}_{file_date()}.log", + / sanitize_filename( + f"flix_conversion_{video.video_settings.video_title or video.video_settings.output_path.stem}_{file_date()}.log" + ), level=logging.DEBUG, log_format="%(asctime)s - %(message)s", encoding="utf-8", diff --git a/fastflix/encoders/common/attachments.py b/fastflix/encoders/common/attachments.py index 16cbfc98..320f5470 100644 --- a/fastflix/encoders/common/attachments.py +++ b/fastflix/encoders/common/attachments.py @@ -3,6 +3,7 @@ from typing import List from fastflix.models.encode import AttachmentTrack +from fastflix.shared import unixy def image_type(file: Path): @@ -20,7 +21,7 @@ def build_attachments(attachments: List[AttachmentTrack]) -> str: for attachment in attachments: if attachment.attachment_type == "cover": mime_type, ext_type = image_type(attachment.file_path) - unixy_path = str(attachment.file_path).replace("\\", "/") + unixy_path = unixy(attachment.file_path) commands.append( f' -attach "{unixy_path}" -metadata:s:{attachment.outdex} mimetype="{mime_type}" ' f'-metadata:s:{attachment.outdex} filename="{attachment.filename}.{ext_type}" ' diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index b282fe06..c8ae93c7 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -10,7 +10,7 @@ from fastflix.encoders.common.audio import build_audio from fastflix.encoders.common.subtitles import build_subtitle from fastflix.models.fastflix import FastFlix -from fastflix.models.video import Crop +from fastflix.shared import unixy null = "/dev/null" if reusables.win_based: @@ -50,8 +50,8 @@ def generate_ffmpeg_start( incoming_fps = f"-r {source_fps}" if source_fps else "" vsync_text = f"-vsync {vsync}" if vsync else "" title = f'-metadata title="{video_title}"' if video_title else "" - source = str(source).replace("\\", "/") - ffmpeg = str(ffmpeg).replace("\\", "/") + source = unixy(source) + ffmpeg = unixy(ffmpeg) return " ".join( [ @@ -93,7 +93,7 @@ def generate_ending( f"{audio} {subtitles} {cover} " ) if output_video and not null_ending: - output_video = str(output_video).replace("\\", "/") + output_video = unixy(output_video) ending += f'"{output_video}"' else: ending += null @@ -167,8 +167,7 @@ def generate_filters( else: filter_complex = f"[0:{selected_track}][0:{burn_in_subtitle_track}]overlay[v]" else: - unixy = str(source).replace("\\", "/") - filter_complex = f"[0:{selected_track}]{f'{filters},' if filters else ''}subtitles='{unixy}':si={burn_in_subtitle_track}[v]" + filter_complex = f"[0:{selected_track}]{f'{filters},' if filters else ''}subtitles='{unixy(source)}':si={burn_in_subtitle_track}[v]" elif filters: filter_complex = f"[0:{selected_track}]{filters}[v]" else: diff --git a/fastflix/encoders/gif/command_builder.py b/fastflix/encoders/gif/command_builder.py index a4bc711d..7a1423f3 100644 --- a/fastflix/encoders/gif/command_builder.py +++ b/fastflix/encoders/gif/command_builder.py @@ -4,6 +4,7 @@ from fastflix.encoders.common.helpers import Command, generate_filters from fastflix.models.encode import GIFSettings from fastflix.models.fastflix import FastFlix +from fastflix.shared import unixy def build(fastflix: FastFlix): @@ -15,7 +16,7 @@ def build(fastflix: FastFlix): custom_filters=f"fps={settings.fps:.2f}", raw_filters=True, **fastflix.current_video.video_settings.dict() ) - output_video = str(fastflix.current_video.video_settings.output_path).replace("\\", "/") + output_video = unixy(fastflix.current_video.video_settings.output_path) beginning = ( f'"{fastflix.config.ffmpeg}" -y ' f'{f"-ss {fastflix.current_video.video_settings.start_time}" if fastflix.current_video.video_settings.start_time else ""} ' diff --git a/fastflix/encoders/nvencc_avc/command_builder.py b/fastflix/encoders/nvencc_avc/command_builder.py index d7c5fab2..1f363b9c 100644 --- a/fastflix/encoders/nvencc_avc/command_builder.py +++ b/fastflix/encoders/nvencc_avc/command_builder.py @@ -6,7 +6,7 @@ from fastflix.models.encode import NVEncCAVCSettings from fastflix.models.video import SubtitleTrack, Video from fastflix.models.fastflix import FastFlix -from fastflix.flix import unixy +from fastflix.shared import unixy lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] diff --git a/fastflix/flix.py b/fastflix/flix.py index 1637b306..3678f76b 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -8,6 +8,7 @@ import reusables from box import Box, BoxError +from pathvalidate import sanitize_filepath from fastflix.exceptions import FlixError from fastflix.language import t @@ -23,7 +24,7 @@ def unixy(source): - return str(source).replace("\\", "/") + return str(sanitize_filepath(source, platform="Windows" if reusables.win_based else "Linux")).replace("\\", "/") def guess_bit_depth(pix_fmt: str, color_primaries: str = None) -> int: diff --git a/fastflix/models/config.py b/fastflix/models/config.py index fd87f536..e99a932e 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -123,6 +123,7 @@ class Config(BaseModel): hdr10plus_parser: Optional[Path] = Field(default_factory=lambda: where("hdr10plus_parser")) mkvpropedit: Optional[Path] = Field(default_factory=lambda: where("mkvpropedit")) nvencc: Optional[Path] = Field(default_factory=lambda: where("NVEncC")) + output_directory: Optional[Path] = False flat_ui: bool = True language: str = "en" logging_level: int = 10 @@ -192,7 +193,7 @@ def load(self): "there may be non-recoverable errors while loading it." ) - paths = ("work_path", "ffmpeg", "ffprobe", "hdr10plus_parser", "mkvpropedit", "nvencc") + paths = ("work_path", "ffmpeg", "ffprobe", "hdr10plus_parser", "mkvpropedit", "nvencc", "output_directory") for key, value in data.items(): if key == "profiles": self.profiles = {} diff --git a/fastflix/shared.py b/fastflix/shared.py index 43d22b51..f19d7172 100644 --- a/fastflix/shared.py +++ b/fastflix/shared.py @@ -11,6 +11,7 @@ import pkg_resources import requests import reusables +from pathvalidate import sanitize_filepath try: @@ -257,3 +258,7 @@ def timedelta_to_str(delta): output_string = output_string.split(".")[0] # Remove .XXX microseconds return output_string + + +def unixy(source): + return str(sanitize_filepath(source, platform="Windows" if reusables.win_based else "Linux")).replace("\\", "/") diff --git a/fastflix/widgets/about.py b/fastflix/widgets/about.py index 50d8c817..48c2f6ad 100644 --- a/fastflix/widgets/about.py +++ b/fastflix/widgets/about.py @@ -59,7 +59,7 @@ def __init__(self, parent=None): f"{link('https://github.com/cdgriffith/Box', t('python-box'))} {box_version} (MIT), " f"{link('https://github.com/cdgriffith/Reusables', t('Reusables'))} {reusables.__version__} (MIT)
" "mistune (BSD), colorama (BSD), coloredlogs (MIT), Requests (Apache 2.0)
" - "appdirs (MIT), iso639-lang (MIT), psutil (BSD), qtpy (MIT)
" + "appdirs (MIT), iso639-lang (MIT), psutil (BSD), qtpy (MIT), pathvalidate (MIT)
" ) supporting_libraries_label.setAlignment(QtCore.Qt.AlignCenter) supporting_libraries_label.setOpenExternalLinks(True) diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 9ee4dee5..8d84c2ff 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -8,6 +8,7 @@ from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp +from fastflix.shared import unixy logger = logging.getLogger("fastflix") @@ -47,7 +48,7 @@ def __init__(self, main, mkv_prop_edit, video_path): self.video_path = video_path def run(self): - output_file = str(self.video_path).replace("\\", "/") + output_file = unixy(self.video_path) self.main.thread_logging_signal.emit(f'INFO:{t("Will fix first subtitle track to not be default")}') try: result = run( @@ -139,7 +140,7 @@ def run(self): self.app.fastflix.config.ffmpeg, "-y", "-i", - str(self.app.fastflix.current_video.source).replace("\\", "/"), + unixy(self.app.fastflix.current_video.source), "-map", f"0:{track}", "-c:v", @@ -156,7 +157,7 @@ def run(self): ) process_two = Popen( - [self.app.fastflix.config.hdr10plus_parser, "-o", str(output).replace("\\", "/"), "-"], + [self.app.fastflix.config.hdr10plus_parser, "-o", unixy(output), "-"], stdout=PIPE, stderr=PIPE, stdin=process.stdout, diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 0af3c32b..b0b7d0a9 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -228,7 +228,10 @@ def __init__(self, parent, path): self.path = str(path) def __del__(self): - self.wait() + try: + self.wait() + except BaseException: + pass def run(self): if reusables.win_based: diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index f996ff61..7827e02b 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -840,6 +840,8 @@ def open_file(self): @property def generate_output_filename(self): + if self.app.fastflix.config.output_directory: + return f"{self.app.fastflix.config.output_directory / self.input_video.stem}-fastflix-{secrets.token_hex(2)}.{self.current_encoder.video_extension}" if self.input_video: return f"{self.input_video.parent / self.input_video.stem}-fastflix-{secrets.token_hex(2)}.{self.current_encoder.video_extension}" return f"{Path('~').expanduser()}{os.sep}fastflix-{secrets.token_hex(2)}.{self.current_encoder.video_extension}" @@ -1758,13 +1760,18 @@ def status_update(self): logger.debug(f"Updating queue from command worker") with self.app.fastflix.queue_lock: - for video in self.app.fastflix.queue: + fixed_vids = [] + for i, video in enumerate(self.app.fastflix.queue): if video.status.complete and not video.status.subtitle_fixed: if video.video_settings.subtitle_tracks and not video.video_settings.subtitle_tracks[0].disposition: if mkv_prop_edit := shutil.which("mkvpropedit"): worker = SubtitleFix(self, mkv_prop_edit, video.video_settings.output_path) worker.start() - video.status.subtitle_fixed = True + fixed_vids.append(i) + for index in fixed_vids: + video = self.app.fastflix.queue.pop(index) + video.status.subtitle_fixed = True + self.app.fastflix.queue.insert(index, video) save_queue(self.app.fastflix.queue, self.app.fastflix.queue_path) self.video_options.update_queue() diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index 1721e2ba..012d3a5f 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -240,18 +240,28 @@ def __init__(self, parent, app: FastFlixApp): top_layout.addWidget(self.clear_queue, QtCore.Qt.AlignRight) super().__init__(app, parent, t("Queue"), "queue", top_row_layout=top_layout) - self.queue_startup_check() + try: + self.queue_startup_check() + except Exception: + logger.exception("Could not load queue as it is outdated or malformed. Deleting for safety.") + save_queue([], queue_file=self.app.fastflix.queue_path) def queue_startup_check(self): for item in get_queue(self.app.fastflix.queue_path): self.app.fastflix.queue.append(item) + reset_vids = [] remove_vids = [] - for video in self.app.fastflix.queue: - # if video.status.running: - # video.status.clear() + for i, video in enumerate(self.app.fastflix.queue): + if video.status.running: + reset_vids.append(i) if video.status.complete: remove_vids.append(video) + for index in reset_vids: + vid = self.app.fastflix.queue.pop(index) + vid.status.reset() + self.app.fastflix.queue.insert(index, vid) + for video in remove_vids: self.app.fastflix.queue.remove(video) diff --git a/requirements-build.txt b/requirements-build.txt index 15d77e45..0b5f1048 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -3,6 +3,7 @@ colorama coloredlogs iso639-lang mistune +pathvalidate psutil pydantic pyinstaller==4.2 diff --git a/requirements.txt b/requirements.txt index 33995b64..7b86ccba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ colorama coloredlogs iso639-lang mistune +pathvalidate psutil pydantic pyqt5 From d59111e86716e12b58d6544fe5f453c304f04b70 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 13 Feb 2021 12:28:18 -0600 Subject: [PATCH 48/50] * Fixing #171 Be able to select encoder before selecting video --- CHANGES | 1 + fastflix/conversion_worker.py | 1 + fastflix/data/languages.yaml | 2 + fastflix/encoders/common/nvencc_helpers.py | 65 +++++++++++++++++++ fastflix/encoders/hevc_x265/settings_panel.py | 2 + .../encoders/nvencc_avc/command_builder.py | 53 +-------------- .../encoders/nvencc_avc/settings_panel.py | 2 + .../encoders/nvencc_hevc/command_builder.py | 62 +----------------- .../encoders/nvencc_hevc/settings_panel.py | 2 + fastflix/widgets/container.py | 1 + fastflix/widgets/main.py | 42 ++++++------ fastflix/widgets/video_options.py | 2 + 12 files changed, 105 insertions(+), 130 deletions(-) create mode 100644 fastflix/encoders/common/nvencc_helpers.py diff --git a/CHANGES b/CHANGES index 078ce94c..dc24fbb0 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,7 @@ * Adding Windows 10 notification for queue complete success * Adding #194 fast two pass encoding (thanks to Ugurtan) * Fixing German translations (thanks to SMESH) +* Fixing #171 Be able to select encoder before selecting video * Fixing #176 Unable to change queue order or delete task from queue since 4.1.0 (thanks to Etz) * Fixing #185 need to specify channel layout when downmixing (thanks to Ugurtan) * Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson) diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py index 500a1694..b0d95386 100644 --- a/fastflix/conversion_worker.py +++ b/fastflix/conversion_worker.py @@ -246,6 +246,7 @@ def start_command(): currently_encoding = False allow_sleep_mode() status_queue.put(("cancelled", video.uuid if video else "")) + log_queue.put("STOP_TIMER") if request[0] == "pause queue": logger.debug(t("Command worker received request to pause encoding after the current item completes")) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 1350d1e8..89b41592 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -3052,3 +3052,5 @@ vsync: zho: vsync There are no videos to start converting: eng: There are no videos to start converting +No crop, scale, rotation,flip nor any other filters will be applied.: + eng: No crop, scale, rotation,flip nor any other filters will be applied. diff --git a/fastflix/encoders/common/nvencc_helpers.py b/fastflix/encoders/common/nvencc_helpers.py new file mode 100644 index 00000000..8a445d8c --- /dev/null +++ b/fastflix/encoders/common/nvencc_helpers.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from typing import List, Dict +import logging + +from fastflix.models.video import SubtitleTrack, AudioTrack +from fastflix.encoders.common.audio import lossless + + +logger = logging.getLogger("fastflix") + + +def get_stream_pos(streams) -> Dict: + return {x.index: i for i, x in enumerate(streams, start=1)} + + +def build_audio(audio_tracks: List[AudioTrack], audio_streams): + command_list = [] + copies = [] + track_ids = set() + stream_ids = get_stream_pos(audio_streams) + + for track in sorted(audio_tracks, key=lambda x: x.outdex): + if track.index in track_ids: + logger.warning("NVEncC does not support copy and duplicate of audio tracks!") + track_ids.add(track.index) + audio_id = stream_ids[track.index] + if track.language: + command_list.append(f"--audio-metadata {audio_id}?language={track.language}") + if not track.conversion_codec or track.conversion_codec == "none": + copies.append(str(audio_id)) + elif track.conversion_codec: + downmix = f"--audio-stream {audio_id}?:{track.downmix}" if track.downmix else "" + bitrate = "" + if track.conversion_codec not in lossless: + bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate.rstrip('k')} " + command_list.append( + f"{downmix} --audio-codec {audio_id}?{track.conversion_codec} {bitrate} " + f"--audio-metadata {audio_id}?clear" + ) + + if track.title: + command_list.append( + f'--audio-metadata {audio_id}?title="{track.title}" ' + f'--audio-metadata {audio_id}?handler="{track.title}" ' + ) + + return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" + + +def build_subtitle(subtitle_tracks: List[SubtitleTrack], subtitle_streams) -> str: + command_list = [] + copies = [] + stream_ids = get_stream_pos(subtitle_streams) + + for track in sorted(subtitle_tracks, key=lambda x: x.outdex): + sub_id = stream_ids[track.index] + if track.burn_in: + command_list.append(f"--vpp-subburn track={sub_id}") + else: + copies.append(str(sub_id)) + if track.disposition: + command_list.append(f"--sub-disposition {sub_id}?{track.disposition}") + command_list.append(f"--sub-metadata {sub_id}?language='{track.language}'") + + return f" --sub-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index 937944f5..47b35e90 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -519,6 +519,8 @@ def hdr_opts(): self.updating_settings = False def new_source(self): + if not self.app.fastflix.current_video: + return super().new_source() self.setting_change() if self.app.fastflix.current_video.hdr10_plus: diff --git a/fastflix/encoders/nvencc_avc/command_builder.py b/fastflix/encoders/nvencc_avc/command_builder.py index 3cd415f0..4a3389c7 100644 --- a/fastflix/encoders/nvencc_avc/command_builder.py +++ b/fastflix/encoders/nvencc_avc/command_builder.py @@ -1,65 +1,16 @@ # -*- coding: utf-8 -*- -from typing import List import logging from fastflix.encoders.common.helpers import Command from fastflix.models.encode import NVEncCAVCSettings -from fastflix.models.video import SubtitleTrack, Video +from fastflix.models.video import Video from fastflix.models.fastflix import FastFlix from fastflix.shared import unixy - -lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] +from fastflix.encoders.common.nvencc_helpers import build_subtitle, build_audio logger = logging.getLogger("fastflix") -def build_audio(audio_tracks): - command_list = [] - copies = [] - track_ids = set() - - for track in audio_tracks: - if track.index in track_ids: - logger.warning("NVEncC does not support copy and duplicate of audio tracks!") - track_ids.add(track.index) - if track.language: - command_list.append(f"--audio-metadata {track.outdex}?language={track.language}") - if not track.conversion_codec or track.conversion_codec == "none": - copies.append(str(track.outdex)) - elif track.conversion_codec: - downmix = f"--audio-stream {track.outdex}?:{track.downmix}" if track.downmix else "" - bitrate = "" - if track.conversion_codec not in lossless: - bitrate = f"--audio-bitrate {track.outdex}?{track.conversion_bitrate.rstrip('k')} " - command_list.append( - f"{downmix} --audio-codec {track.outdex}?{track.conversion_codec} {bitrate} " - f"--audio-metadata {track.outdex}?clear" - ) - - if track.title: - command_list.append( - f'--audio-metadata {track.outdex}?title="{track.title}" ' - f'--audio-metadata {track.outdex}?handler="{track.title}" ' - ) - - return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" - - -def build_subtitle(subtitle_tracks: List[SubtitleTrack]) -> str: - command_list = [] - copies = [] - for i, track in enumerate(subtitle_tracks, start=1): - if track.burn_in: - command_list.append(f"--vpp-subburn track={i}") - else: - copies.append(str(i)) - if track.disposition: - command_list.append(f"--sub-disposition {i}?{track.disposition}") - command_list.append(f"--sub-metadata {i}?language='{track.language}'") - - return f" --sub-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" - - def build(fastflix: FastFlix): video: Video = fastflix.current_video settings: NVEncCAVCSettings = fastflix.current_video.video_settings.video_encoder_settings diff --git a/fastflix/encoders/nvencc_avc/settings_panel.py b/fastflix/encoders/nvencc_avc/settings_panel.py index 96a41c99..13f8dc03 100644 --- a/fastflix/encoders/nvencc_avc/settings_panel.py +++ b/fastflix/encoders/nvencc_avc/settings_panel.py @@ -432,6 +432,8 @@ def set_mode(self, x): self.main.build_commands() def new_source(self): + if not self.app.fastflix.current_video: + return super().new_source() if self.app.fastflix.current_video.hdr10_plus: self.extract_button.show() diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 35aa6538..2f9a4366 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -1,74 +1,16 @@ # -*- coding: utf-8 -*- -from typing import List, Dict import logging from fastflix.encoders.common.helpers import Command from fastflix.models.encode import NVEncCSettings -from fastflix.models.video import SubtitleTrack, Video +from fastflix.models.video import Video from fastflix.models.fastflix import FastFlix +from fastflix.encoders.common.nvencc_helpers import build_subtitle, build_audio from fastflix.flix import unixy -lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"] - logger = logging.getLogger("fastflix") -def get_stream_pos(streams) -> Dict: - return {x.index: i for i, x in enumerate(streams, start=1)} - - -def build_audio(audio_tracks, audio_streams): - command_list = [] - copies = [] - track_ids = set() - stream_ids = get_stream_pos(audio_streams) - - for track in sorted(audio_tracks, key=lambda x: x.outdex): - if track.index in track_ids: - logger.warning("NVEncC does not support copy and duplicate of audio tracks!") - track_ids.add(track.index) - audio_id = stream_ids[track.index] - if track.language: - command_list.append(f"--audio-metadata {audio_id}?language={track.language}") - if not track.conversion_codec or track.conversion_codec == "none": - copies.append(str(audio_id)) - elif track.conversion_codec: - downmix = f"--audio-stream {audio_id}?:{track.downmix}" if track.downmix else "" - bitrate = "" - if track.conversion_codec not in lossless: - bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate.rstrip('k')} " - command_list.append( - f"{downmix} --audio-codec {audio_id}?{track.conversion_codec} {bitrate} " - f"--audio-metadata {audio_id}?clear" - ) - - if track.title: - command_list.append( - f'--audio-metadata {audio_id}?title="{track.title}" ' - f'--audio-metadata {audio_id}?handler="{track.title}" ' - ) - - return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" - - -def build_subtitle(subtitle_tracks: List[SubtitleTrack], subtitle_streams) -> str: - command_list = [] - copies = [] - stream_ids = get_stream_pos(subtitle_streams) - - for track in sorted(subtitle_tracks, key=lambda x: x.outdex): - sub_id = stream_ids[track.index] - if track.burn_in: - command_list.append(f"--vpp-subburn track={sub_id}") - else: - copies.append(str(sub_id)) - if track.disposition: - command_list.append(f"--sub-disposition {sub_id}?{track.disposition}") - command_list.append(f"--sub-metadata {sub_id}?language='{track.language}'") - - return f" --sub-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}" - - def build(fastflix: FastFlix): video: Video = fastflix.current_video settings: NVEncCSettings = fastflix.current_video.video_settings.video_encoder_settings diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index ae5a31ab..de88e93d 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -432,6 +432,8 @@ def set_mode(self, x): self.main.build_commands() def new_source(self): + if not self.app.fastflix.current_video: + return super().new_source() if self.app.fastflix.current_video.hdr10_plus: self.extract_button.show() diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index b0b7d0a9..fd6b700b 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -50,6 +50,7 @@ def __init__(self, app: FastFlixApp, **kwargs): self.setMinimumSize(QtCore.QSize(1280, 650)) self.icon = QtGui.QIcon(main_icon) self.setWindowIcon(self.icon) + self.main.set_profile() def closeEvent(self, a0: QtGui.QCloseEvent) -> None: if self.pb: diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 51de376f..4594fc8c 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -191,9 +191,9 @@ def __init__(self, parent, app: FastFlixApp): self.disable_all() self.setLayout(self.grid) - self.set_profile() self.show() self.initialized = True + self.loading_video = False self.last_page_update = time.time() def init_top_bar(self): @@ -412,23 +412,25 @@ def set_profile(self): return self.app.fastflix.config.selected_profile = self.widgets.profile_box.currentText() self.app.fastflix.config.save() - self.widgets.convert_to.setCurrentText(f" {self.app.fastflix.config.opt('encoder')}") + self.widgets.convert_to.setCurrentText(self.app.fastflix.config.opt("encoder")) if self.app.fastflix.config.opt("auto_crop") and not self.build_crop(): self.get_auto_crop() self.loading_video = True - self.widgets.scale.keep_aspect.setChecked(self.app.fastflix.config.opt("keep_aspect_ratio")) - self.widgets.rotate.setCurrentIndex(self.app.fastflix.config.opt("rotate") or 0 // 90) - - v_flip = self.app.fastflix.config.opt("vertical_flip") - h_flip = self.app.fastflix.config.opt("horizontal_flip") - - self.widgets.flip.setCurrentIndex(self.flip_to_int(v_flip, h_flip)) - self.video_options.change_conversion(self.app.fastflix.config.opt("encoder")) - self.video_options.update_profile() - if self.app.fastflix.current_video: - self.video_options.new_source() - # Hack to prevent a lot of thumbnail generation - self.loading_video = False + try: + self.widgets.scale.keep_aspect.setChecked(self.app.fastflix.config.opt("keep_aspect_ratio")) + self.widgets.rotate.setCurrentIndex(self.app.fastflix.config.opt("rotate") or 0 // 90) + + v_flip = self.app.fastflix.config.opt("vertical_flip") + h_flip = self.app.fastflix.config.opt("horizontal_flip") + + self.widgets.flip.setCurrentIndex(self.flip_to_int(v_flip, h_flip)) + self.video_options.change_conversion(self.app.fastflix.config.opt("encoder")) + self.video_options.update_profile() + if self.app.fastflix.current_video: + self.video_options.new_source() + finally: + # Hack to prevent a lot of thumbnail generation + self.loading_video = False self.page_update() def save_profile(self): @@ -483,7 +485,7 @@ def init_rotate(self): def change_output_types(self): self.widgets.convert_to.clear() - self.widgets.convert_to.addItems([f" {x}" for x in self.app.fastflix.encoders.keys()]) + self.widgets.convert_to.addItems(self.app.fastflix.encoders.keys()) for i, plugin in enumerate(self.app.fastflix.encoders.values()): if getattr(plugin, "icon", False): self.widgets.convert_to.setItemIcon(i, QtGui.QIcon(plugin.icon)) @@ -507,9 +509,11 @@ def init_encoder_drop_down(self): return layout def change_encoder(self): - if not self.initialized or not self.app.fastflix.current_video or not self.convert_to: + if not self.initialized or not self.convert_to: return self.video_options.change_conversion(self.convert_to) + if not self.app.fastflix.current_video: + return if not self.output_video_path_widget.text().endswith(self.current_encoder.video_extension): # Make sure it's using the right file extension self.output_video_path_widget.setText(self.generate_output_filename) @@ -977,7 +981,7 @@ def keep_aspect_update(self) -> None: def disable_all(self): for name, widget in self.widgets.items(): - if name in ("preview", "convert_button", "pause_resume"): + if name in ("preview", "convert_button", "pause_resume", "convert_to", "profile_box"): continue if isinstance(widget, dict): for sub_widget in widget.values(): @@ -992,7 +996,7 @@ def disable_all(self): def enable_all(self): for name, widget in self.widgets.items(): - if name in ("preview", "convert_button", "pause_resume"): + if name in ("preview", "convert_button", "pause_resume", "convert_to", "profile_box"): continue if isinstance(widget, dict): for sub_widget in widget.values(): diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py index d7e007a4..f7556ceb 100644 --- a/fastflix/widgets/video_options.py +++ b/fastflix/widgets/video_options.py @@ -104,6 +104,8 @@ def get_settings(self): self.main.container.profile.update_settings() def new_source(self): + if not self.app.fastflix.current_video: + return if getattr(self.main.current_encoder, "enable_audio", False): self.audio.new_source(self.audio_formats) if getattr(self.main.current_encoder, "enable_subtitles", False): From c3b21e8244cf88befac9899c06701f635cb5479e Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 13 Feb 2021 14:02:21 -0600 Subject: [PATCH 49/50] adding missing languages --- fastflix/data/languages.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 89b41592..e398dd9b 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -3051,6 +3051,16 @@ vsync: spa: vsync zho: vsync There are no videos to start converting: + deu: Es sind keine Videos vorhanden, die konvertiert werden können eng: There are no videos to start converting + fra: Il n'y a pas de vidéos à convertir + ita: Non ci sono video da convertire + spa: No hay vídeos para empezar a convertir + zho: 没有视频可以开始转换 No crop, scale, rotation,flip nor any other filters will be applied.: + deu: Es werden weder Beschneiden, Skalieren, Drehen, Spiegeln noch andere Filter angewendet. eng: No crop, scale, rotation,flip nor any other filters will be applied. + fra: Aucun filtre de culture, d'échelle, de rotation, de retournement ou autre ne sera appliqué. + ita: Nessun ritaglio, scala, rotazione, flip o qualsiasi altro filtro sarà applicato. + spa: No se aplicará ningún filtro de recorte, escala, rotación, volteo ni ningún otro. + zho: 不会应用裁剪、缩放、旋转、翻转或任何其他滤镜。 From b60565376e4a0bb7e01c68bd5930f979c59b0212 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sat, 13 Feb 2021 22:30:26 -0600 Subject: [PATCH 50/50] Fixing nvenc AVC build command --- fastflix/encoders/nvencc_avc/command_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastflix/encoders/nvencc_avc/command_builder.py b/fastflix/encoders/nvencc_avc/command_builder.py index 4a3389c7..0036f8eb 100644 --- a/fastflix/encoders/nvencc_avc/command_builder.py +++ b/fastflix/encoders/nvencc_avc/command_builder.py @@ -132,8 +132,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks), - build_subtitle(video.video_settings.subtitle_tracks), + build_audio(video.video_settings.audio_tracks, video.streams.audio), + build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle), settings.extra, "-o", f'"{unixy(video.video_settings.output_path)}"',