From e72eeb1f0efecb0e94ddaeea2c3f3a7601caa786 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 15 Apr 2022 14:33:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 16 ++++++- controller/api/bilirecorder_blueprint.py | 18 +++++++- exception/danmaku.py | 8 +++- model/DanmakuClip.py | 8 +++- worker/danmaku.py | 25 +++++++++++ workflow/video.py | 56 ++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 worker/danmaku.py diff --git a/config.py b/config.py index 0f05859..866e638 100644 --- a/config.py +++ b/config.py @@ -20,6 +20,11 @@ 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 = "./" @@ -48,6 +53,11 @@ def load_config(): 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.get('each_sec', VIDEO_CLIP_EACH_SEC) + VIDEO_CLIP_OVERFLOW_SEC = section.get('overflow_sec', VIDEO_CLIP_OVERFLOW_SEC) if config.has_section("ffmpeg"): section = config['ffmpeg'] global FFMPEG_EXEC, FFMPEG_USE_GPU, VIDEO_BITRATE @@ -78,6 +88,10 @@ def get_config(): 'video': { 'title': VIDEO_TITLE, }, + 'clip': { + 'each_sec': VIDEO_CLIP_EACH_SEC, + 'overflow_sec': VIDEO_CLIP_OVERFLOW_SEC, + }, 'ffmpeg': { 'exec': FFMPEG_EXEC, 'gpu': FFMPEG_USE_GPU, @@ -102,4 +116,4 @@ def write_config(): config[_i] = _config[_i] with open("config.ini", "w", encoding="utf-8") as f: config.write(f) - return True \ No newline at end of file + return True diff --git a/controller/api/bilirecorder_blueprint.py b/controller/api/bilirecorder_blueprint.py index f9d244f..414bfc1 100644 --- a/controller/api/bilirecorder_blueprint.py +++ b/controller/api/bilirecorder_blueprint.py @@ -1,4 +1,5 @@ import os.path +import threading from datetime import datetime, timedelta from glob import glob from flask import Blueprint, jsonify, request, current_app @@ -9,12 +10,25 @@ from model import db from model.DanmakuClip import DanmakuClip from model.VideoClip import VideoClip from model.Workflow import Workflow +from worker.danmaku import do_workflow blueprint = Blueprint("api_bilirecorder", __name__, url_prefix="/api/bilirecorder") bili_record_workflow_item: Optional[Workflow] = None +def auto_submit_task(): + global bili_record_workflow_item + if not bili_record_workflow_item.editing: + if len(bili_record_workflow_item.video_clips) > 0 and len(bili_record_workflow_item.danmaku_clips) > 0: + threading.Thread(target=do_workflow, args=( + bili_record_workflow_item.video_clips[0].full_path, + bili_record_workflow_item.danmaku_clips[0].full_path, + [clip.full_path for clip in bili_record_workflow_item.danmaku_clips[1:]] + )).start() + clear_item() + + def clear_item(): global bili_record_workflow_item bili_record_workflow_item = None @@ -86,7 +100,6 @@ def collect_danmaku_files(workflow: Optional[Workflow]): commit_item() - @blueprint.post("/") def bilirecorder_event(): payload = request.json @@ -107,7 +120,7 @@ def bilirecorder_event(): item = safe_get_item() item.editing = False commit_item() - clear_item() + auto_submit_task() return jsonify(item.to_dict()) elif payload['EventType'] == "FileClosed": # 文件关闭 @@ -128,6 +141,7 @@ def bilirecorder_event(): item.video_clips.append(video_clip) commit_item() collect_danmaku_files(item) + auto_submit_task() return jsonify(item.to_dict()) commit_item() item = safe_get_item() diff --git a/exception/danmaku.py b/exception/danmaku.py index 174beab..15b128b 100644 --- a/exception/danmaku.py +++ b/exception/danmaku.py @@ -1,6 +1,10 @@ -class NoDanmakuException(Exception): +class DanmakuException(Exception): ... -class DanmakuFormatErrorException(Exception): +class NoDanmakuException(DanmakuException): + ... + + +class DanmakuFormatErrorException(DanmakuException): ... diff --git a/model/DanmakuClip.py b/model/DanmakuClip.py index 2168c7e..6c2d83b 100644 --- a/model/DanmakuClip.py +++ b/model/DanmakuClip.py @@ -1,3 +1,5 @@ +import os + from . import db @@ -5,16 +7,18 @@ class DanmakuClip(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) base_path = db.Column(db.String(255)) file = db.Column(db.String(255)) - subtitle_file = db.Column(db.String(255)) offset = db.Column(db.Float, nullable=False, default=0) workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id')) workflow = db.relationship("Workflow", backref=db.backref("danmaku_clips", lazy="dynamic")) + @property + def full_path(self): + return os.path.abspath(os.path.join(self.base_path, self.file)) + def to_json(self): return { "id": self.id, "base_path": self.base_path, "file": self.file, - "subtitle_file": self.subtitle_file, "offset": self.offset, } diff --git a/worker/danmaku.py b/worker/danmaku.py new file mode 100644 index 0000000..f37336d --- /dev/null +++ b/worker/danmaku.py @@ -0,0 +1,25 @@ +import os.path +from datetime import datetime + +from exception.danmaku import DanmakuException +from workflow.danmaku import get_file_start, diff_danmaku_files, danmaku_to_subtitle +from workflow.video import encode_video_with_subtitles, quick_split_video + + +def do_workflow(video_file, danmaku_base_file, *danmaku_files): + if not os.path.exists(danmaku_base_file): + ... + result = [] + start_ts = get_file_start(danmaku_base_file) + base_start = datetime.fromtimestamp(start_ts) + new_file_name = base_start.strftime("%Y%m%d_%H%M.flv") + result.append(danmaku_to_subtitle(danmaku_base_file, 0)) + for file in danmaku_files: + try: + result.append(danmaku_to_subtitle(file, diff_danmaku_files(danmaku_base_file, file))) + except DanmakuException: + print("弹幕文件", file, "异常") + continue + print(result) + encode_video_with_subtitles(video_file, result, new_file_name) + quick_split_video(new_file_name) diff --git a/workflow/video.py b/workflow/video.py index 388ead3..6f0770a 100644 --- a/workflow/video.py +++ b/workflow/video.py @@ -1,5 +1,10 @@ +import os import re import subprocess +from datetime import datetime, timedelta +from typing import IO + +from config import FFMPEG_EXEC, VIDEO_BITRATE, FFMPEG_USE_GPU, VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC def get_video_real_duration(filename): @@ -14,3 +19,54 @@ def get_video_real_duration(filename): result = match_result.pop() return result + +def encode_video_with_subtitles(orig_filename: str, subtitles: list[str], new_filename: str): + encode_process = subprocess.Popen([ + FFMPEG_EXEC, "-hide_banner", "-progress", "-", "-v", "0", "-y", + "-i", orig_filename, "-vf", ",".join("subtitles=%s" % i for i in subtitles) + ",hwupload_cuda", + "-c:a", "copy", "-c:v", "h264_nvenc" if FFMPEG_USE_GPU else "h264", "-f", "mp4", + "-preset:v", "fast", "-profile:v", "high", "-level", "4.1", + "-b:v", VIDEO_BITRATE, "-rc:v", "vbr", "-tune", "hq", + "-qmin", "10", "-qmax", "32", "-crf", "16", + # "-t", "10", + new_filename + ], stdout=subprocess.PIPE) + handle_ffmpeg_output(encode_process.stdout) + + +def handle_ffmpeg_output(stderr: IO[bytes]) -> None: + while True: + line = stderr.readline() + if line == b"": + break + if line.startswith(b"out_time="): + cur_time = line.replace(b"out_time=", b"").decode() + print("CurTime", cur_time.strip()) + if line.startswith(b"speed="): + speed = line.replace(b"speed=", b"").decode() + print("Speed", speed.strip()) + + +def quick_split_video(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_str = get_video_real_duration(file) + _duration = datetime.strptime(_duration_str, "%H:%M:%S.%f") - datetime(1900, 1, 1) + duration = _duration.total_seconds() + 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", "-y", "-hide_banner", "-progress", "-", "-v", "0", + "-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) + ], stdout=subprocess.PIPE) + handle_ffmpeg_output(split_process.stdout) + current_sec += VIDEO_CLIP_EACH_SEC