初始版本

This commit is contained in:
Jerry Yan 2022-04-15 14:33:01 +08:00
parent 09b2956573
commit e72eeb1f0e
6 changed files with 124 additions and 7 deletions

View File

@ -20,6 +20,11 @@ VIDEO_BITRATE = "2.5M"
# [video] # [video]
# title # title
VIDEO_TITLE = "【永恒de草薙直播录播】直播于 {}" VIDEO_TITLE = "【永恒de草薙直播录播】直播于 {}"
# [clip]
# each_sec
VIDEO_CLIP_EACH_SEC = 6000
# overflow_sec
VIDEO_CLIP_OVERFLOW_SEC = 5
# [recorder] # [recorder]
# bili_dir # bili_dir
BILILIVE_RECORDER_DIRECTORY = "./" BILILIVE_RECORDER_DIRECTORY = "./"
@ -48,6 +53,11 @@ def load_config():
section = config['video'] section = config['video']
global VIDEO_TITLE global VIDEO_TITLE
VIDEO_TITLE = section.get('title', 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"): if config.has_section("ffmpeg"):
section = config['ffmpeg'] section = config['ffmpeg']
global FFMPEG_EXEC, FFMPEG_USE_GPU, VIDEO_BITRATE global FFMPEG_EXEC, FFMPEG_USE_GPU, VIDEO_BITRATE
@ -78,6 +88,10 @@ def get_config():
'video': { 'video': {
'title': VIDEO_TITLE, 'title': VIDEO_TITLE,
}, },
'clip': {
'each_sec': VIDEO_CLIP_EACH_SEC,
'overflow_sec': VIDEO_CLIP_OVERFLOW_SEC,
},
'ffmpeg': { 'ffmpeg': {
'exec': FFMPEG_EXEC, 'exec': FFMPEG_EXEC,
'gpu': FFMPEG_USE_GPU, 'gpu': FFMPEG_USE_GPU,
@ -102,4 +116,4 @@ def write_config():
config[_i] = _config[_i] config[_i] = _config[_i]
with open("config.ini", "w", encoding="utf-8") as f: with open("config.ini", "w", encoding="utf-8") as f:
config.write(f) config.write(f)
return True return True

View File

@ -1,4 +1,5 @@
import os.path import os.path
import threading
from datetime import datetime, timedelta from datetime import datetime, timedelta
from glob import glob from glob import glob
from flask import Blueprint, jsonify, request, current_app from flask import Blueprint, jsonify, request, current_app
@ -9,12 +10,25 @@ from model import db
from model.DanmakuClip import DanmakuClip from model.DanmakuClip import DanmakuClip
from model.VideoClip import VideoClip from model.VideoClip import VideoClip
from model.Workflow import Workflow from model.Workflow import Workflow
from worker.danmaku import do_workflow
blueprint = Blueprint("api_bilirecorder", __name__, url_prefix="/api/bilirecorder") blueprint = Blueprint("api_bilirecorder", __name__, url_prefix="/api/bilirecorder")
bili_record_workflow_item: Optional[Workflow] = None 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(): def clear_item():
global bili_record_workflow_item global bili_record_workflow_item
bili_record_workflow_item = None bili_record_workflow_item = None
@ -86,7 +100,6 @@ def collect_danmaku_files(workflow: Optional[Workflow]):
commit_item() commit_item()
@blueprint.post("/") @blueprint.post("/")
def bilirecorder_event(): def bilirecorder_event():
payload = request.json payload = request.json
@ -107,7 +120,7 @@ def bilirecorder_event():
item = safe_get_item() item = safe_get_item()
item.editing = False item.editing = False
commit_item() commit_item()
clear_item() auto_submit_task()
return jsonify(item.to_dict()) return jsonify(item.to_dict())
elif payload['EventType'] == "FileClosed": elif payload['EventType'] == "FileClosed":
# 文件关闭 # 文件关闭
@ -128,6 +141,7 @@ def bilirecorder_event():
item.video_clips.append(video_clip) item.video_clips.append(video_clip)
commit_item() commit_item()
collect_danmaku_files(item) collect_danmaku_files(item)
auto_submit_task()
return jsonify(item.to_dict()) return jsonify(item.to_dict())
commit_item() commit_item()
item = safe_get_item() item = safe_get_item()

View File

@ -1,6 +1,10 @@
class NoDanmakuException(Exception): class DanmakuException(Exception):
... ...
class DanmakuFormatErrorException(Exception): class NoDanmakuException(DanmakuException):
...
class DanmakuFormatErrorException(DanmakuException):
... ...

View File

@ -1,3 +1,5 @@
import os
from . import db from . import db
@ -5,16 +7,18 @@ class DanmakuClip(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
base_path = db.Column(db.String(255)) base_path = db.Column(db.String(255))
file = 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) offset = db.Column(db.Float, nullable=False, default=0)
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id')) workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'))
workflow = db.relationship("Workflow", backref=db.backref("danmaku_clips", lazy="dynamic")) 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): def to_json(self):
return { return {
"id": self.id, "id": self.id,
"base_path": self.base_path, "base_path": self.base_path,
"file": self.file, "file": self.file,
"subtitle_file": self.subtitle_file,
"offset": self.offset, "offset": self.offset,
} }

25
worker/danmaku.py Normal file
View File

@ -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)

View File

@ -1,5 +1,10 @@
import os
import re import re
import subprocess 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): def get_video_real_duration(filename):
@ -14,3 +19,54 @@ def get_video_real_duration(filename):
result = match_result.pop() result = match_result.pop()
return result 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