工作流,更新双GPU加速
This commit is contained in:
parent
a43b47c0b8
commit
12504b9cf5
107
config.py
Normal file
107
config.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import configparser
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
|
# [danmaku]
|
||||||
|
# exec
|
||||||
|
DANMAKU_FACTORY_EXEC = "DanmakuFactory"
|
||||||
|
# speed
|
||||||
|
DANMAKU_SPEED = 12
|
||||||
|
# font
|
||||||
|
DEFAULT_FONT_NAME = "Sarasa Term SC"
|
||||||
|
# resolution
|
||||||
|
VIDEO_RESOLUTION = "1280x720"
|
||||||
|
# [ffmpeg]
|
||||||
|
# exec
|
||||||
|
FFMPEG_EXEC = "ffmpeg"
|
||||||
|
# nvidia_gpu
|
||||||
|
FFMPEG_USE_NVIDIA_GPU = True
|
||||||
|
# intel_gpu
|
||||||
|
FFMPEG_USE_INTEL_GPU = True
|
||||||
|
# bitrate
|
||||||
|
VIDEO_BITRATE = "2.5M"
|
||||||
|
# [video]
|
||||||
|
# title
|
||||||
|
VIDEO_TITLE = "【永恒de草薙直播录播】直播于 {}"
|
||||||
|
# [clip]
|
||||||
|
# each_sec
|
||||||
|
VIDEO_CLIP_EACH_SEC = 6000
|
||||||
|
# overflow_sec
|
||||||
|
VIDEO_CLIP_OVERFLOW_SEC = 5
|
||||||
|
# [recorder]
|
||||||
|
# bili_dir
|
||||||
|
BILILIVE_RECORDER_DIRECTORY = "./"
|
||||||
|
# xigua_dir
|
||||||
|
XIGUALIVE_RECORDER_DIRECTORY = "./"
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
if not os.path.exists("config.ini"):
|
||||||
|
write_config()
|
||||||
|
return False
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("config.ini", encoding="utf-8")
|
||||||
|
if config.has_section("danmaku"):
|
||||||
|
section = config['danmaku']
|
||||||
|
global DANMAKU_FACTORY_EXEC, DANMAKU_SPEED, DEFAULT_FONT_NAME, VIDEO_RESOLUTION
|
||||||
|
DANMAKU_FACTORY_EXEC = section.get('exec', DANMAKU_FACTORY_EXEC)
|
||||||
|
DANMAKU_SPEED = section.getfloat('speed', DANMAKU_SPEED)
|
||||||
|
DEFAULT_FONT_NAME = section.get('font', DEFAULT_FONT_NAME)
|
||||||
|
VIDEO_RESOLUTION = section.get('resolution', VIDEO_RESOLUTION)
|
||||||
|
if config.has_section("video"):
|
||||||
|
section = config['video']
|
||||||
|
global VIDEO_TITLE
|
||||||
|
VIDEO_TITLE = section.get('title', VIDEO_TITLE)
|
||||||
|
if config.has_section("clip"):
|
||||||
|
section = config['clip']
|
||||||
|
global VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC
|
||||||
|
VIDEO_CLIP_EACH_SEC = section.getfloat('each_sec', VIDEO_CLIP_EACH_SEC)
|
||||||
|
VIDEO_CLIP_OVERFLOW_SEC = section.getfloat('overflow_sec', VIDEO_CLIP_OVERFLOW_SEC)
|
||||||
|
if config.has_section("ffmpeg"):
|
||||||
|
section = config['ffmpeg']
|
||||||
|
global FFMPEG_EXEC, FFMPEG_USE_NVIDIA_GPU, FFMPEG_USE_INTEL_GPU, VIDEO_BITRATE
|
||||||
|
FFMPEG_EXEC = section.get('exec', FFMPEG_EXEC)
|
||||||
|
FFMPEG_USE_NVIDIA_GPU = section.getboolean('nvidia_gpu', FFMPEG_USE_NVIDIA_GPU)
|
||||||
|
FFMPEG_USE_INTEL_GPU = section.getboolean('intel_gpu', FFMPEG_USE_INTEL_GPU)
|
||||||
|
VIDEO_BITRATE = section.get('bitrate', VIDEO_BITRATE)
|
||||||
|
if config.has_section("recorder"):
|
||||||
|
global BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY
|
||||||
|
section = config['recorder']
|
||||||
|
BILILIVE_RECORDER_DIRECTORY = section.get('bili_dir', BILILIVE_RECORDER_DIRECTORY)
|
||||||
|
XIGUALIVE_RECORDER_DIRECTORY = section.get('xigua_dir', XIGUALIVE_RECORDER_DIRECTORY)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_config():
|
||||||
|
config = {
|
||||||
|
'danmaku': {
|
||||||
|
'exec': DANMAKU_FACTORY_EXEC,
|
||||||
|
'speed': DANMAKU_SPEED,
|
||||||
|
'font': DEFAULT_FONT_NAME,
|
||||||
|
'resolution': VIDEO_RESOLUTION,
|
||||||
|
},
|
||||||
|
'clip': {
|
||||||
|
'each_sec': VIDEO_CLIP_EACH_SEC,
|
||||||
|
'overflow_sec': VIDEO_CLIP_OVERFLOW_SEC,
|
||||||
|
},
|
||||||
|
'ffmpeg': {
|
||||||
|
'exec': FFMPEG_EXEC,
|
||||||
|
'nvidia_gpu': FFMPEG_USE_NVIDIA_GPU,
|
||||||
|
'intel_gpu': FFMPEG_USE_INTEL_GPU,
|
||||||
|
'bitrate': VIDEO_BITRATE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def write_config():
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
_config = get_config()
|
||||||
|
for _i in _config:
|
||||||
|
config[_i] = _config[_i]
|
||||||
|
with open("config.ini", "w", encoding="utf-8") as f:
|
||||||
|
config.write(f)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
load_config()
|
@ -1,22 +1,19 @@
|
|||||||
# 工作流
|
# 工作流
|
||||||
import datetime
|
|
||||||
import os.path
|
import os.path
|
||||||
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional, IO, AnyStr
|
import threading
|
||||||
|
from typing import Optional, IO, Union
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from PyQt5 import QtGui
|
from PyQt5 import QtGui
|
||||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QWidget, QLabel, QApplication, QFrame, QVBoxLayout, QPushButton, \
|
from PyQt5.QtWidgets import QWidget, QLabel, QApplication, QFrame, QVBoxLayout, QPushButton, \
|
||||||
QSizePolicy, QMessageBox
|
QSizePolicy, QMessageBox, QProgressBar
|
||||||
from danmaku_xml_helper import get_file_start, diff_danmaku_files
|
from danmaku_xml_helper import get_file_start, diff_danmaku_files, danmaku_to_subtitle
|
||||||
|
from config import load_config, FFMPEG_EXEC, DANMAKU_FACTORY_EXEC, FFMPEG_USE_INTEL_GPU, FFMPEG_USE_NVIDIA_GPU, \
|
||||||
DANMAKU_PATH = "workspace"
|
VIDEO_BITRATE, VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC
|
||||||
DANMAKU_FACTORY_CLI = "DFCLI.exe"
|
|
||||||
FFMPEG_TOOL = "ffmpeg.exe"
|
|
||||||
SPLIT_PATH = "split"
|
|
||||||
SPLIT_TOOL = "quick_split_video.exe"
|
|
||||||
HOME_PATH = os.path.realpath(".")
|
|
||||||
|
|
||||||
|
|
||||||
class Job:
|
class Job:
|
||||||
@ -55,11 +52,14 @@ class WorkLabel(QLabel):
|
|||||||
self.adjustSize()
|
self.adjustSize()
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None:
|
def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None:
|
||||||
if self.workVideo is None or self.finished:
|
if self.finished:
|
||||||
|
self.finished = False
|
||||||
|
elif self.workVideo is None:
|
||||||
self.parent().labelDestroy.emit(self)
|
self.parent().labelDestroy.emit(self)
|
||||||
return self.deleteLater()
|
return self.deleteLater()
|
||||||
self.workVideo = None
|
else:
|
||||||
self.workDanmaku = []
|
self.workVideo = None
|
||||||
|
self.workDanmaku = []
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
@ -149,19 +149,26 @@ class HomePage(QWidget):
|
|||||||
labelDestroy = pyqtSignal(WorkLabel)
|
labelDestroy = pyqtSignal(WorkLabel)
|
||||||
processCurTime = pyqtSignal(str)
|
processCurTime = pyqtSignal(str)
|
||||||
processSpeed = pyqtSignal(str)
|
processSpeed = pyqtSignal(str)
|
||||||
|
processCurTime2 = pyqtSignal(str)
|
||||||
|
processSpeed2 = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(HomePage, self).__init__()
|
super(HomePage, self).__init__()
|
||||||
self.layout = None
|
self.layout = None
|
||||||
self.labels: list[WorkLabel] = []
|
self.labels: list[WorkLabel] = []
|
||||||
self.worker: Optional[WorkerThread] = None
|
self.worker: WorkerThread
|
||||||
self.btn_start: Optional[QPushButton] = None
|
self.btn_start: QPushButton
|
||||||
self.showMessageBox.connect(self.on_show_message_box_info)
|
self.showMessageBox.connect(self.on_show_message_box_info)
|
||||||
self.labelDestroy.connect(self.on_label_destroy)
|
self.labelDestroy.connect(self.on_label_destroy)
|
||||||
self.processCurTime.connect(self.on_process_cur_time_change)
|
self.processCurTime.connect(self.on_process_cur_time_change)
|
||||||
self.processSpeed.connect(self.on_process_speed_change)
|
self.processSpeed.connect(self.on_process_speed_change)
|
||||||
|
self.processCurTime2.connect(self.on_process_cur_time2_change)
|
||||||
|
self.processSpeed2.connect(self.on_process_speed2_change)
|
||||||
self.process_cur_time = "-"
|
self.process_cur_time = "-"
|
||||||
self.process_speed = "-"
|
self.process_speed = "-"
|
||||||
|
self.process_cur_time2 = ""
|
||||||
|
self.process_speed2 = ""
|
||||||
|
self.cur_clip_duration = 0
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
@ -225,21 +232,46 @@ class HomePage(QWidget):
|
|||||||
self.btn_start.setDisabled(True)
|
self.btn_start.setDisabled(True)
|
||||||
self.btn_start.setText("正在处理")
|
self.btn_start.setText("正在处理")
|
||||||
|
|
||||||
def on_process_cur_time_change(self, s: str)->None:
|
def on_process_cur_time_change(self, s: str) -> None:
|
||||||
|
if self.process_cur_time == s:
|
||||||
|
return
|
||||||
self.process_cur_time = s
|
self.process_cur_time = s
|
||||||
self.update_btn_process_text()
|
self.update_btn_process_text()
|
||||||
|
|
||||||
def on_process_speed_change(self, s: str)->None:
|
def on_process_speed_change(self, s: str) -> None:
|
||||||
|
if self.process_speed == s:
|
||||||
|
return
|
||||||
self.process_speed = s
|
self.process_speed = s
|
||||||
self.update_btn_process_text()
|
self.update_btn_process_text()
|
||||||
|
|
||||||
|
def on_process_cur_time2_change(self, s: str) -> None:
|
||||||
|
if self.process_cur_time2 == s:
|
||||||
|
return
|
||||||
|
self.process_cur_time2 = s
|
||||||
|
self.update_btn_process_text()
|
||||||
|
|
||||||
|
def on_process_speed2_change(self, s: str) -> None:
|
||||||
|
if self.process_speed2 == s:
|
||||||
|
return
|
||||||
|
self.process_speed2 = s
|
||||||
|
self.update_btn_process_text()
|
||||||
|
|
||||||
def update_btn_process_text(self):
|
def update_btn_process_text(self):
|
||||||
self.btn_start.setText("{}@{}".format(self.process_cur_time, self.process_speed))
|
_t = []
|
||||||
|
if self.process_cur_time != "" and self.process_speed != "":
|
||||||
|
_t.append("{}@{}".format(self.process_cur_time, self.process_speed))
|
||||||
|
if self.process_cur_time2 != "" and self.process_speed2 != "":
|
||||||
|
_t.append("{}@{}".format(self.process_cur_time2, self.process_speed2))
|
||||||
|
if len(_t) == 0:
|
||||||
|
self.btn_start.setText("Working")
|
||||||
|
else:
|
||||||
|
self.btn_start.setText("|".join(_t))
|
||||||
|
|
||||||
def on_worker_stop(self):
|
def on_worker_stop(self):
|
||||||
self.btn_start.setDisabled(False)
|
self.btn_start.setDisabled(False)
|
||||||
self.btn_start.setText("开始")
|
self.btn_start.setText("开始")
|
||||||
self.worker = None
|
self.worker = None
|
||||||
|
self.handle_do()
|
||||||
|
|
||||||
|
|
||||||
class WorkerThread(QThread):
|
class WorkerThread(QThread):
|
||||||
@ -248,89 +280,257 @@ class WorkerThread(QThread):
|
|||||||
self.app = app
|
self.app = app
|
||||||
self.label = label
|
self.label = label
|
||||||
|
|
||||||
|
def get_video_real_duration(self, filename) -> float:
|
||||||
|
ffmpeg_process = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-i", filename, "-c", "copy", "-f",
|
||||||
|
"null", "-"
|
||||||
|
], **subprocess_args(True))
|
||||||
|
_duration_str = self.handle_ffmpeg_output(ffmpeg_process.stdout)
|
||||||
|
ffmpeg_process.wait()
|
||||||
|
return duration_str_to_float(_duration_str)
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self.label.start_running()
|
self.label.start_running()
|
||||||
job = self.label.get_job()
|
job = self.label.get_job()
|
||||||
if job.type == Job.DANMAKU_ENCODE:
|
if job.type == Job.DANMAKU_ENCODE:
|
||||||
self.run_danmaku_encode(job)
|
self.run_danmaku_encode(job)
|
||||||
elif job.type == Job.PURE_SPLIT:
|
elif job.type == Job.PURE_SPLIT:
|
||||||
self.run_pure_split(job)
|
self.quick_split_video(job.video)
|
||||||
self.label.stop_running()
|
self.label.stop_running()
|
||||||
|
|
||||||
def run_danmaku_encode(self, job: Job):
|
def run_danmaku_encode(self, job: Job):
|
||||||
os.chdir(DANMAKU_PATH)
|
|
||||||
base_danmaku = job.danmaku.pop(0)
|
base_danmaku = job.danmaku.pop(0)
|
||||||
time_shift = 0
|
time_shift = 0
|
||||||
base_start_ts = get_file_start(base_danmaku)
|
base_start_ts = get_file_start(base_danmaku)
|
||||||
new_subtitle_name = os.path.basename(base_danmaku) + ".ass"
|
new_subtitle_name = danmaku_to_subtitle(base_danmaku, time_shift)
|
||||||
subprocess.Popen((
|
|
||||||
DANMAKU_FACTORY_CLI,
|
|
||||||
"-r", "1280x720", "-s", "10", "-f", "5",
|
|
||||||
"-S", "40", "-N", "\"Sarasa Term SC\"", "--showmsgbox", "FALSE",
|
|
||||||
"-O", "255", "-L", "1", "-D", "0",
|
|
||||||
"-o", "ass", new_subtitle_name, "-i", base_danmaku, "-t", str(time_shift)
|
|
||||||
), **subprocess_args()).wait()
|
|
||||||
print(new_subtitle_name)
|
|
||||||
job.subtitles.append(new_subtitle_name)
|
job.subtitles.append(new_subtitle_name)
|
||||||
for danmaku in job.danmaku:
|
for danmaku in job.danmaku:
|
||||||
time_shift = diff_danmaku_files(base_danmaku, danmaku)
|
time_shift = diff_danmaku_files(base_danmaku, danmaku)
|
||||||
new_subtitle_name = os.path.basename(danmaku) + ".ass"
|
new_subtitle_name = danmaku_to_subtitle(danmaku, time_shift)
|
||||||
subprocess.Popen((
|
|
||||||
DANMAKU_FACTORY_CLI,
|
|
||||||
"-r", "1280x720", "-s", "10", "-f", "5",
|
|
||||||
"-S", "40", "-N", "\"Sarasa Term SC\"", "--showmsgbox", "FALSE",
|
|
||||||
"-O", "255", "-L", "1", "-D", "0",
|
|
||||||
"-o", "ass", new_subtitle_name, "-i", danmaku, "-t", str(time_shift)
|
|
||||||
), **subprocess_args()).wait()
|
|
||||||
print(new_subtitle_name)
|
|
||||||
job.subtitles.append(new_subtitle_name)
|
job.subtitles.append(new_subtitle_name)
|
||||||
# 压制
|
# 压制
|
||||||
base_start = datetime.datetime.fromtimestamp(base_start_ts)
|
split_files = self.multi_gpu_encode_video_with_subtitles(job.video, job.subtitles, base_start_ts)
|
||||||
new_file_name = base_start.strftime("%Y%m%d_%H%M.flv")
|
for file in split_files:
|
||||||
encode_process = subprocess.Popen([
|
self.quick_split_video(file)
|
||||||
FFMPEG_TOOL, "-hide_banner", "-progress", "-", "-v", "0", "-y",
|
|
||||||
"-i", job.video, "-vf", ",".join("subtitles=%s" % i for i in job.subtitles) + ",hwupload_cuda",
|
|
||||||
"-c:a", "copy", "-c:v", "h264_nvenc", "-f", "mp4",
|
|
||||||
"-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
|
||||||
"-b:v", "2.5M", "-bufsize:v", "5M", "-rc:v", "vbr_hq", "-bf:v", "3",
|
|
||||||
"-qmin", "10", "-qmax", "52", "-crf", "16",
|
|
||||||
# "-t", "10",
|
|
||||||
os.path.join(HOME_PATH, SPLIT_PATH, new_file_name)
|
|
||||||
], **subprocess_args(True))
|
|
||||||
self.handle_ffmpeg_output(encode_process.stdout)
|
|
||||||
encode_process.wait()
|
|
||||||
for _f in job.subtitles:
|
for _f in job.subtitles:
|
||||||
if os.path.isfile(_f):
|
if os.path.isfile(_f):
|
||||||
os.remove(_f)
|
os.remove(_f)
|
||||||
os.chdir(HOME_PATH)
|
|
||||||
os.chdir(SPLIT_PATH)
|
|
||||||
split_process = subprocess.Popen([
|
|
||||||
SPLIT_TOOL, new_file_name
|
|
||||||
], **subprocess_args(True))
|
|
||||||
self.handle_ffmpeg_output(split_process.stdout)
|
|
||||||
split_process.wait()
|
|
||||||
os.chdir(HOME_PATH)
|
|
||||||
|
|
||||||
def run_pure_split(self, job: Job):
|
def encode_video_with_subtitles(self, orig_filename: str, subtitles: list[str], new_filename: str):
|
||||||
os.chdir(SPLIT_PATH)
|
if FFMPEG_USE_NVIDIA_GPU:
|
||||||
split_process = subprocess.Popen([
|
print("[+]Use Nvidia NvEnc Acceleration")
|
||||||
SPLIT_TOOL, job.video
|
encode_process = subprocess.Popen([
|
||||||
], **subprocess_args(True))
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
self.handle_ffmpeg_output(split_process.stdout)
|
"-i", orig_filename, "-vf",
|
||||||
split_process.wait()
|
",".join("subtitles=%s" % i for i in subtitles) + ",hwupload_cuda",
|
||||||
os.chdir(HOME_PATH)
|
"-c:a", "copy", "-c:v", "h264_nvenc",
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune:v", "hq",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename
|
||||||
|
], stdout=subprocess.PIPE)
|
||||||
|
elif FFMPEG_USE_INTEL_GPU:
|
||||||
|
if platform.system().lower() == "windows":
|
||||||
|
print("[+]Use Intel QSV Acceleration")
|
||||||
|
encode_process = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
|
"-hwaccel", "qsv", "-i", orig_filename, "-vf",
|
||||||
|
",".join("subtitles=%s" % i for i in subtitles),
|
||||||
|
"-c:a", "copy", "-c:v", "h264_qsv",
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune:v", "hq",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename
|
||||||
|
], stdout=subprocess.PIPE)
|
||||||
|
else:
|
||||||
|
print("[+]Use Intel VAAPI Acceleration")
|
||||||
|
encode_process = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
|
"-hwaccel", "vaapi", "-i", orig_filename, "-vf",
|
||||||
|
",".join("subtitles=%s" % i for i in subtitles) + ",hwupload",
|
||||||
|
"-c:a", "copy", "-c:v", "h264_vaapi",
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune:v", "hq",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename
|
||||||
|
], stdout=subprocess.PIPE)
|
||||||
|
else:
|
||||||
|
print("[+]Use CPU Encode")
|
||||||
|
encode_process = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
|
"-i", orig_filename, "-vf",
|
||||||
|
",".join("subtitles=%s" % i for i in subtitles),
|
||||||
|
"-c:a", "copy", "-c:v", "h264",
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename
|
||||||
|
], stdout=subprocess.PIPE)
|
||||||
|
self.handle_ffmpeg_output(encode_process.stdout)
|
||||||
|
return encode_process.wait()
|
||||||
|
|
||||||
def handle_ffmpeg_output(self, stderr: IO[bytes]) -> None:
|
def multi_gpu_encode_video_with_subtitles(self, orig_filename: str, subtitles: list[str], base_ts: float):
|
||||||
|
new_filename = base_ts_to_filename(base_ts)
|
||||||
|
if not (FFMPEG_USE_NVIDIA_GPU and FFMPEG_USE_INTEL_GPU):
|
||||||
|
print("[!]Not Enabled Both GPU")
|
||||||
|
self.encode_video_with_subtitles(orig_filename, subtitles, new_filename)
|
||||||
|
return [new_filename]
|
||||||
|
duration = self.get_video_real_duration(orig_filename)
|
||||||
|
print("[>]Duration:", duration)
|
||||||
|
if duration > (VIDEO_CLIP_EACH_SEC * 5):
|
||||||
|
# qsv 压制前2段,剩余交由nvenc压制
|
||||||
|
new_filename0 = base_ts_to_filename(base_ts)
|
||||||
|
new_filename1 = base_ts_to_filename(base_ts + VIDEO_CLIP_EACH_SEC * 2)
|
||||||
|
print("[+]Use Intel QSV Acceleration")
|
||||||
|
encode_process0 = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
|
"-hwaccel", "qsv", "-t", str(VIDEO_CLIP_EACH_SEC * 2 + (VIDEO_CLIP_OVERFLOW_SEC * 0.8)), "-i",
|
||||||
|
orig_filename, "-vf",
|
||||||
|
",".join("subtitles=%s" % i for i in subtitles),
|
||||||
|
"-c:a", "copy", "-c:v", "h264_qsv",
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune:v", "hq",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename0
|
||||||
|
], **subprocess_args(True))
|
||||||
|
print("[+]Use Nvidia NvEnc Acceleration")
|
||||||
|
encode_process1 = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
|
"-ss", str(VIDEO_CLIP_EACH_SEC * 2), "-copyts", "-i", orig_filename, "-vf",
|
||||||
|
",".join("subtitles=%s" % i for i in subtitles),
|
||||||
|
"-c:a", "copy", "-c:v", "h264_nvenc", "-ss", str(VIDEO_CLIP_EACH_SEC * 2),
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune:v", "hq",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename1
|
||||||
|
], **subprocess_args(True))
|
||||||
|
threading.Thread(target=self.handle_ffmpeg_output, args=(encode_process0.stdout, True,)).start()
|
||||||
|
threading.Thread(target=self.handle_ffmpeg_output, args=(encode_process1.stdout,)).start()
|
||||||
|
encode_process0.wait()
|
||||||
|
encode_process1.wait()
|
||||||
|
return [new_filename0, new_filename1]
|
||||||
|
elif duration > (VIDEO_CLIP_EACH_SEC * 3):
|
||||||
|
# 至少也要能切2片,才用双GPU加速
|
||||||
|
new_filename0 = base_ts_to_filename(base_ts, True)
|
||||||
|
new_filename1 = base_ts_to_filename(base_ts + VIDEO_CLIP_EACH_SEC)
|
||||||
|
print("[+]Use Intel QSV Acceleration")
|
||||||
|
encode_process0 = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
|
"-hwaccel", "qsv", "-t", str(VIDEO_CLIP_EACH_SEC + (VIDEO_CLIP_OVERFLOW_SEC * 0.8)), "-i",
|
||||||
|
orig_filename, "-vf",
|
||||||
|
",".join("subtitles=%s" % i for i in subtitles),
|
||||||
|
"-c:a", "copy", "-c:v", "h264_qsv",
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune:v", "hq",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename0
|
||||||
|
], **subprocess_args(True))
|
||||||
|
print("[+]Use Nvidia NvEnc Acceleration")
|
||||||
|
encode_process1 = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
|
||||||
|
"-ss", str(VIDEO_CLIP_EACH_SEC), "-copyts", "-i", orig_filename, "-vf",
|
||||||
|
",".join("subtitles=%s" % i for i in subtitles),
|
||||||
|
"-c:a", "copy", "-c:v", "h264_nvenc", "-ss", str(VIDEO_CLIP_EACH_SEC),
|
||||||
|
"-f", "mp4", "-preset:v", "fast", "-profile:v", "high", "-level", "4.1",
|
||||||
|
"-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune:v", "hq",
|
||||||
|
"-qmin", "10", "-qmax", "32", "-crf", "16",
|
||||||
|
# "-t", "10",
|
||||||
|
new_filename1
|
||||||
|
], **subprocess_args(True))
|
||||||
|
threading.Thread(target=self.handle_ffmpeg_output, args=(encode_process0.stdout, True,)).start()
|
||||||
|
threading.Thread(target=self.handle_ffmpeg_output, args=(encode_process1.stdout,)).start()
|
||||||
|
encode_process0.wait()
|
||||||
|
encode_process1.wait()
|
||||||
|
return [new_filename1]
|
||||||
|
else:
|
||||||
|
self.encode_video_with_subtitles(orig_filename, subtitles, new_filename)
|
||||||
|
return [new_filename]
|
||||||
|
|
||||||
|
def quick_split_video(self, file):
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
raise FileNotFoundError(file)
|
||||||
|
file_name = os.path.split(file)[-1]
|
||||||
|
_create_dt = os.path.splitext(file_name)[0]
|
||||||
|
create_dt = datetime.strptime(_create_dt, "%Y%m%d_%H%M")
|
||||||
|
duration = self.get_video_real_duration(file)
|
||||||
|
current_sec = 0
|
||||||
|
while current_sec < duration:
|
||||||
|
current_dt = (create_dt + timedelta(seconds=current_sec)).strftime("%Y%m%d_%H%M_")
|
||||||
|
print("CUR_DT", current_dt)
|
||||||
|
print("BIAS_T", current_sec)
|
||||||
|
split_process = subprocess.Popen([
|
||||||
|
FFMPEG_EXEC, "-y", "-hide_banner", "-progress", "-", "-loglevel", "error",
|
||||||
|
"-ss", str(current_sec),
|
||||||
|
"-i", file_name, "-c", "copy", "-f", "mp4",
|
||||||
|
"-t", str(VIDEO_CLIP_EACH_SEC + VIDEO_CLIP_OVERFLOW_SEC),
|
||||||
|
"{}.mp4".format(current_dt)
|
||||||
|
], **subprocess_args(True))
|
||||||
|
self.handle_ffmpeg_output(split_process.stdout)
|
||||||
|
current_sec += VIDEO_CLIP_EACH_SEC
|
||||||
|
|
||||||
|
def handle_ffmpeg_output(self, stdout: Optional[IO[bytes]], second=False) -> str:
|
||||||
|
out_time = "0:0:0.0"
|
||||||
|
if stdout is None:
|
||||||
|
return out_time
|
||||||
|
speed = "0"
|
||||||
while True:
|
while True:
|
||||||
line = stderr.readline()
|
line = stdout.readline()
|
||||||
if line == b"":
|
if line == b"":
|
||||||
break
|
break
|
||||||
|
if line.strip() == b"progress=end":
|
||||||
|
# 处理完毕
|
||||||
|
break
|
||||||
if line.startswith(b"out_time="):
|
if line.startswith(b"out_time="):
|
||||||
cur_time = line.replace(b"out_time=", b"").decode()
|
out_time = line.replace(b"out_time=", b"").decode().strip()
|
||||||
self.app.processCurTime.emit(cur_time.strip())
|
if second:
|
||||||
|
self.app.processCurTime2.emit(out_time)
|
||||||
|
else:
|
||||||
|
self.app.processCurTime.emit(out_time)
|
||||||
if line.startswith(b"speed="):
|
if line.startswith(b"speed="):
|
||||||
speed = line.replace(b"speed=", b"").decode()
|
speed = line.replace(b"speed=", b"").decode().strip()
|
||||||
self.app.processSpeed.emit(speed.strip())
|
if second:
|
||||||
|
self.app.processSpeed2.emit(speed)
|
||||||
|
else:
|
||||||
|
self.app.processSpeed.emit(speed)
|
||||||
|
if second:
|
||||||
|
self.app.processSpeed2.emit("")
|
||||||
|
self.app.processCurTime2.emit("")
|
||||||
|
else:
|
||||||
|
self.app.processSpeed.emit("")
|
||||||
|
self.app.processCurTime.emit("")
|
||||||
|
return out_time
|
||||||
|
|
||||||
|
|
||||||
|
class NvencWorkerThread(QThread):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class IntelWorkerThread(QThread):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class SplitWorkerThread(QThread):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def duration_str_to_float(duration_str) -> float:
|
||||||
|
_duration = datetime.strptime(duration_str, "%H:%M:%S.%f") - datetime(1900, 1, 1)
|
||||||
|
return _duration.total_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
def base_ts_to_filename(start_ts: float, is_mp4=False) -> str:
|
||||||
|
base_start = datetime.fromtimestamp(start_ts)
|
||||||
|
if is_mp4:
|
||||||
|
return base_start.strftime("%Y%m%d_%H%M.mp4")
|
||||||
|
else:
|
||||||
|
return base_start.strftime("%Y%m%d_%H%M.flv")
|
||||||
|
|
||||||
|
|
||||||
# Create a set of arguments which make a ``subprocess.Popen`` (and
|
# Create a set of arguments which make a ``subprocess.Popen`` (and
|
||||||
@ -384,34 +584,38 @@ def subprocess_args(include_stdout=True):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def check_exec(name: Union[os.PathLike[str], str]) -> bool:
|
||||||
|
if is_windows():
|
||||||
|
check_process = subprocess.Popen([
|
||||||
|
"where.exe", name
|
||||||
|
], stdout=subprocess.PIPE)
|
||||||
|
check_process.wait()
|
||||||
|
return len(check_process.stdout.readlines()) > 0
|
||||||
|
elif is_linux():
|
||||||
|
check_process = subprocess.Popen([
|
||||||
|
"which", name
|
||||||
|
])
|
||||||
|
check_process.wait()
|
||||||
|
return check_process.returncode == 0
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_windows() -> bool:
|
||||||
|
return platform.system().lower() == "windows"
|
||||||
|
|
||||||
|
|
||||||
|
def is_linux() -> bool:
|
||||||
|
return platform.system().lower() == "linux"
|
||||||
|
|
||||||
|
|
||||||
def check_all_prerequisite():
|
def check_all_prerequisite():
|
||||||
if not os.path.isdir(DANMAKU_PATH):
|
if not check_exec(DANMAKU_FACTORY_EXEC):
|
||||||
os.mkdir(DANMAKU_PATH)
|
|
||||||
if not os.path.isdir(SPLIT_PATH):
|
|
||||||
os.mkdir(SPLIT_PATH)
|
|
||||||
os.chdir(DANMAKU_PATH)
|
|
||||||
validate_process = subprocess.Popen([
|
|
||||||
"where.exe", DANMAKU_FACTORY_CLI
|
|
||||||
], **subprocess_args(True))
|
|
||||||
if len(validate_process.stdout.readlines()) == 0:
|
|
||||||
input("弹幕处理工具不存在")
|
input("弹幕处理工具不存在")
|
||||||
exit(1)
|
exit(1)
|
||||||
os.chdir(HOME_PATH)
|
if not check_exec(FFMPEG_EXEC):
|
||||||
os.chdir(SPLIT_PATH)
|
|
||||||
validate_process = subprocess.Popen([
|
|
||||||
"where.exe", SPLIT_TOOL
|
|
||||||
], **subprocess_args(True))
|
|
||||||
if len(validate_process.stdout.readlines()) == 0:
|
|
||||||
input("视频分割工具不存在")
|
|
||||||
exit(1)
|
|
||||||
os.chdir(HOME_PATH)
|
|
||||||
validate_process = subprocess.Popen([
|
|
||||||
"where.exe", FFMPEG_TOOL
|
|
||||||
], **subprocess_args(True))
|
|
||||||
if len(validate_process.stdout.readlines()) == 0:
|
|
||||||
input("FFMPEG工具不存在")
|
input("FFMPEG工具不存在")
|
||||||
exit(1)
|
exit(1)
|
||||||
os.chdir(HOME_PATH)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -422,4 +626,5 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
load_config()
|
||||||
main()
|
main()
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
from hashlib import md5
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from config import DANMAKU_FACTORY_EXEC, VIDEO_RESOLUTION, DANMAKU_SPEED, DEFAULT_FONT_NAME
|
||||||
|
|
||||||
|
|
||||||
class NoDanmakuException(Exception):
|
class NoDanmakuException(Exception):
|
||||||
...
|
...
|
||||||
@ -39,6 +43,19 @@ def diff_danmaku_files(base_file: Union[os.PathLike[str], str], file: Union[os.P
|
|||||||
return get_file_start(file) - get_file_start(base_file)
|
return get_file_start(file) - get_file_start(base_file)
|
||||||
|
|
||||||
|
|
||||||
|
def danmaku_to_subtitle(file: Union[os.PathLike[str], str], time_shift: float):
|
||||||
|
new_subtitle_name = md5(file.encode("utf-8")).hexdigest() + ".ass"
|
||||||
|
process = subprocess.Popen((
|
||||||
|
DANMAKU_FACTORY_EXEC, "--ignore-warnings",
|
||||||
|
"-r", str(VIDEO_RESOLUTION), "-s", str(DANMAKU_SPEED), "-f", "5",
|
||||||
|
"-S", "40", "-N", str(DEFAULT_FONT_NAME), "--showmsgbox", "FALSE",
|
||||||
|
"-O", "255", "-L", "1", "-D", "0",
|
||||||
|
"-o", "ass", new_subtitle_name, "-i", file, "-t", str(time_shift)
|
||||||
|
))
|
||||||
|
process.wait()
|
||||||
|
return new_subtitle_name
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("base", help="以此为标准")
|
parser.add_argument("base", help="以此为标准")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user