From 12504b9cf5246f393aec7bb8402d678d6cbb4922 Mon Sep 17 00:00:00 2001
From: Jerry Yan <792602257@qq.com>
Date: Tue, 3 May 2022 11:07:45 +0800
Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=EF=BC=8C=E6=9B=B4?=
 =?UTF-8?q?=E6=96=B0=E5=8F=8CGPU=E5=8A=A0=E9=80=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 config.py             | 107 +++++++++++
 danmaku_workflow.py   | 401 +++++++++++++++++++++++++++++++-----------
 danmaku_xml_helper.py |  17 ++
 3 files changed, 427 insertions(+), 98 deletions(-)
 create mode 100644 config.py

diff --git a/config.py b/config.py
new file mode 100644
index 0000000..93e6d8d
--- /dev/null
+++ b/config.py
@@ -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()
diff --git a/danmaku_workflow.py b/danmaku_workflow.py
index 78196ee..534cbd8 100644
--- a/danmaku_workflow.py
+++ b/danmaku_workflow.py
@@ -1,22 +1,19 @@
 # 工作流
-import datetime
 import os.path
+import platform
 import subprocess
 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.QtCore import Qt, QThread, pyqtSignal
 from PyQt5.QtWidgets import QWidget, QLabel, QApplication, QFrame, QVBoxLayout, QPushButton, \
-    QSizePolicy, QMessageBox
-from danmaku_xml_helper import get_file_start, diff_danmaku_files
-
-DANMAKU_PATH = "workspace"
-DANMAKU_FACTORY_CLI = "DFCLI.exe"
-FFMPEG_TOOL = "ffmpeg.exe"
-SPLIT_PATH = "split"
-SPLIT_TOOL = "quick_split_video.exe"
-HOME_PATH = os.path.realpath(".")
+    QSizePolicy, QMessageBox, QProgressBar
+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, \
+    VIDEO_BITRATE, VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC
 
 
 class Job:
@@ -55,11 +52,14 @@ class WorkLabel(QLabel):
         self.adjustSize()
 
     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)
             return self.deleteLater()
-        self.workVideo = None
-        self.workDanmaku = []
+        else:
+            self.workVideo = None
+            self.workDanmaku = []
         self.init_ui()
 
     def init_ui(self):
@@ -149,19 +149,26 @@ class HomePage(QWidget):
     labelDestroy = pyqtSignal(WorkLabel)
     processCurTime = pyqtSignal(str)
     processSpeed = pyqtSignal(str)
+    processCurTime2 = pyqtSignal(str)
+    processSpeed2 = pyqtSignal(str)
 
     def __init__(self):
         super(HomePage, self).__init__()
         self.layout = None
         self.labels: list[WorkLabel] = []
-        self.worker: Optional[WorkerThread] = None
-        self.btn_start: Optional[QPushButton] = None
+        self.worker: WorkerThread
+        self.btn_start: QPushButton
         self.showMessageBox.connect(self.on_show_message_box_info)
         self.labelDestroy.connect(self.on_label_destroy)
         self.processCurTime.connect(self.on_process_cur_time_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_speed = "-"
+        self.process_cur_time2 = ""
+        self.process_speed2 = ""
+        self.cur_clip_duration = 0
         self.init_ui()
 
     def init_ui(self):
@@ -225,21 +232,46 @@ class HomePage(QWidget):
         self.btn_start.setDisabled(True)
         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.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.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):
-        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):
         self.btn_start.setDisabled(False)
         self.btn_start.setText("开始")
         self.worker = None
+        self.handle_do()
 
 
 class WorkerThread(QThread):
@@ -248,89 +280,257 @@ class WorkerThread(QThread):
         self.app = app
         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:
         self.label.start_running()
         job = self.label.get_job()
         if job.type == Job.DANMAKU_ENCODE:
             self.run_danmaku_encode(job)
         elif job.type == Job.PURE_SPLIT:
-            self.run_pure_split(job)
+            self.quick_split_video(job.video)
         self.label.stop_running()
 
     def run_danmaku_encode(self, job: Job):
-        os.chdir(DANMAKU_PATH)
         base_danmaku = job.danmaku.pop(0)
         time_shift = 0
         base_start_ts = get_file_start(base_danmaku)
-        new_subtitle_name = os.path.basename(base_danmaku) + ".ass"
-        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)
+        new_subtitle_name = danmaku_to_subtitle(base_danmaku, time_shift)
         job.subtitles.append(new_subtitle_name)
         for danmaku in job.danmaku:
             time_shift = diff_danmaku_files(base_danmaku, danmaku)
-            new_subtitle_name = os.path.basename(danmaku) + ".ass"
-            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)
+            new_subtitle_name = danmaku_to_subtitle(danmaku, time_shift)
             job.subtitles.append(new_subtitle_name)
         # 压制
-        base_start = datetime.datetime.fromtimestamp(base_start_ts)
-        new_file_name = base_start.strftime("%Y%m%d_%H%M.flv")
-        encode_process = subprocess.Popen([
-            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()
+        split_files = self.multi_gpu_encode_video_with_subtitles(job.video, job.subtitles, base_start_ts)
+        for file in split_files:
+            self.quick_split_video(file)
         for _f in job.subtitles:
             if os.path.isfile(_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):
-        os.chdir(SPLIT_PATH)
-        split_process = subprocess.Popen([
-            SPLIT_TOOL, job.video
-        ], **subprocess_args(True))
-        self.handle_ffmpeg_output(split_process.stdout)
-        split_process.wait()
-        os.chdir(HOME_PATH)
+    def encode_video_with_subtitles(self, orig_filename: str, subtitles: list[str], new_filename: str):
+        if FFMPEG_USE_NVIDIA_GPU:
+            print("[+]Use Nvidia NvEnc Acceleration")
+            encode_process = subprocess.Popen([
+                FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-loglevel", "error", "-y",
+                "-i", orig_filename, "-vf",
+                ",".join("subtitles=%s" % i for i in subtitles) + ",hwupload_cuda",
+                "-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:
-            line = stderr.readline()
+            line = stdout.readline()
             if line == b"":
                 break
+            if line.strip() == b"progress=end":
+                # 处理完毕
+                break
             if line.startswith(b"out_time="):
-                cur_time = line.replace(b"out_time=", b"").decode()
-                self.app.processCurTime.emit(cur_time.strip())
+                out_time = line.replace(b"out_time=", b"").decode().strip()
+                if second:
+                    self.app.processCurTime2.emit(out_time)
+                else:
+                    self.app.processCurTime.emit(out_time)
             if line.startswith(b"speed="):
-                speed = line.replace(b"speed=", b"").decode()
-                self.app.processSpeed.emit(speed.strip())
+                speed = line.replace(b"speed=", b"").decode().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
@@ -384,34 +584,38 @@ def subprocess_args(include_stdout=True):
     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():
-    if not os.path.isdir(DANMAKU_PATH):
-        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:
+    if not check_exec(DANMAKU_FACTORY_EXEC):
         input("弹幕处理工具不存在")
         exit(1)
-    os.chdir(HOME_PATH)
-    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:
+    if not check_exec(FFMPEG_EXEC):
         input("FFMPEG工具不存在")
         exit(1)
-    os.chdir(HOME_PATH)
 
 
 def main():
@@ -422,4 +626,5 @@ def main():
 
 
 if __name__ == '__main__':
+    load_config()
     main()
diff --git a/danmaku_xml_helper.py b/danmaku_xml_helper.py
index fe83f8f..0fb6c46 100644
--- a/danmaku_xml_helper.py
+++ b/danmaku_xml_helper.py
@@ -1,10 +1,14 @@
 import datetime
 import os
 import argparse
+import subprocess
+from hashlib import md5
 from typing import Union
 
 from bs4 import BeautifulSoup
 
+from config import DANMAKU_FACTORY_EXEC, VIDEO_RESOLUTION, DANMAKU_SPEED, DEFAULT_FONT_NAME
+
 
 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)
 
 
+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__':
     parser = argparse.ArgumentParser()
     parser.add_argument("base", help="以此为标准")