使用handbrake进行视频压制

This commit is contained in:
Jerry Yan 2023-02-08 23:21:39 +08:00
parent c3993965a9
commit c0e5b8246f
5 changed files with 127 additions and 192 deletions

View File

@ -20,18 +20,15 @@ VIDEO_RESOLUTION = "1280x720"
# [ffmpeg] # [ffmpeg]
# exec # exec
FFMPEG_EXEC = "ffmpeg" FFMPEG_EXEC = "ffmpeg"
# hevc # [handbrake]
FFMPEG_USE_HEVC = False # exec
# nvidia_gpu HANDBRAKE_EXEC = "HandBrakeCli"
FFMPEG_USE_NVIDIA_GPU = False # preset_file
# intel_gpu HANDBRAKE_PRESET_FILE = "handbrake.json"
FFMPEG_USE_INTEL_GPU = False # preset
# vaapi HANDBRAKE_PRESET = "NvEnc"
FFMPEG_USE_VAAPI = False # encopt
# bitrate HANDBRAKE_ENCOPT = ""
VIDEO_BITRATE = "2.5M"
# gop
VIDEO_GOP = 60
# [video] # [video]
# enabled # enabled
VIDEO_ENABLED = False VIDEO_ENABLED = False
@ -94,15 +91,15 @@ def load_config():
VIDEO_CLIP_OVERFLOW_SEC = section.getfloat('overflow_sec', VIDEO_CLIP_OVERFLOW_SEC) VIDEO_CLIP_OVERFLOW_SEC = section.getfloat('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_HEVC, FFMPEG_USE_NVIDIA_GPU, FFMPEG_USE_INTEL_GPU, VIDEO_BITRATE, \ global FFMPEG_EXEC
VIDEO_GOP, FFMPEG_USE_VAAPI
FFMPEG_EXEC = section.get('exec', FFMPEG_EXEC) FFMPEG_EXEC = section.get('exec', FFMPEG_EXEC)
FFMPEG_USE_HEVC = section.getboolean('hevc', FFMPEG_USE_HEVC) if config.has_section("handbrake"):
FFMPEG_USE_NVIDIA_GPU = section.getboolean('nvidia_gpu', FFMPEG_USE_NVIDIA_GPU) section = config['handbrake']
FFMPEG_USE_INTEL_GPU = section.getboolean('intel_gpu', FFMPEG_USE_INTEL_GPU) global HANDBRAKE_EXEC, HANDBRAKE_PRESET_FILE, HANDBRAKE_PRESET, HANDBRAKE_ENCOPT
FFMPEG_USE_VAAPI = section.getboolean('vaapi', FFMPEG_USE_VAAPI) HANDBRAKE_EXEC = section.get('exec', HANDBRAKE_EXEC)
VIDEO_BITRATE = section.get('bitrate', VIDEO_BITRATE) HANDBRAKE_PRESET_FILE = section.get('preset_file', HANDBRAKE_PRESET_FILE)
VIDEO_GOP = section.getfloat('gop', VIDEO_GOP) HANDBRAKE_PRESET = section.get('preset', HANDBRAKE_PRESET)
HANDBRAKE_ENCOPT = section.get('encopt', HANDBRAKE_ENCOPT)
if config.has_section("recorder"): if config.has_section("recorder"):
global BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, VIDEO_OUTPUT_DIR global BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, VIDEO_OUTPUT_DIR
section = config['recorder'] section = config['recorder']
@ -136,12 +133,12 @@ def get_config():
}, },
'ffmpeg': { 'ffmpeg': {
'exec': FFMPEG_EXEC, 'exec': FFMPEG_EXEC,
'hevc': FFMPEG_USE_HEVC, },
'nvidia_gpu': FFMPEG_USE_NVIDIA_GPU, 'handbrake': {
'intel_gpu': FFMPEG_USE_INTEL_GPU, 'exec': HANDBRAKE_EXEC,
'vaapi': FFMPEG_USE_VAAPI, 'preset_file': HANDBRAKE_PRESET_FILE,
'bitrate': VIDEO_BITRATE, 'preset': HANDBRAKE_PRESET,
'gop': VIDEO_GOP, 'encopt': HANDBRAKE_ENCOPT,
}, },
'recorder': { 'recorder': {
'bili_dir': BILILIVE_RECORDER_DIRECTORY, 'bili_dir': BILILIVE_RECORDER_DIRECTORY,

View File

@ -4,7 +4,8 @@ import platform
import psutil import psutil
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from config import DANMAKU_EXEC, FFMPEG_EXEC, BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, VIDEO_OUTPUT_DIR from config import DANMAKU_EXEC, FFMPEG_EXEC, BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, \
VIDEO_OUTPUT_DIR, HANDBRAKE_EXEC
from util.system import check_exec from util.system import check_exec
from workflow.bilibili import IS_LIVING, IS_UPLOADING, IS_ENCODING from workflow.bilibili import IS_LIVING, IS_UPLOADING, IS_ENCODING
@ -60,6 +61,7 @@ def collect_basic_status():
}, },
'exec': { 'exec': {
'ffmpeg': check_exec(FFMPEG_EXEC), 'ffmpeg': check_exec(FFMPEG_EXEC),
'handbrake': check_exec(HANDBRAKE_EXEC),
'danmaku': check_exec(DANMAKU_EXEC), 'danmaku': check_exec(DANMAKU_EXEC),
}, },
'system': { 'system': {

View File

@ -50,6 +50,10 @@
<td>FFMPEG状态</td> <td>FFMPEG状态</td>
<td :class="collector.basic.exec.ffmpeg ? 'success' : 'warning'"></td> <td :class="collector.basic.exec.ffmpeg ? 'success' : 'warning'"></td>
</tr> </tr>
<tr>
<td>HANDBRAKE状态</td>
<td :class="collector.basic.exec.handbrake ? 'success' : 'warning'"></td>
</tr>
<tr> <tr>
<td>弹幕工具状态</td> <td>弹幕工具状态</td>
<td :class="collector.basic.exec.danmaku ? 'success' : 'warning'"></td> <td :class="collector.basic.exec.danmaku ? 'success' : 'warning'"></td>
@ -84,29 +88,30 @@
<td>命令</td> <td>命令</td>
<td>{{ config.ffmpeg.exec }}</td> <td>{{ config.ffmpeg.exec }}</td>
</tr> </tr>
</tbody>
</table>
<table class="current-config">
<thead>
<tr class="table-header">
<td colspan="2">HANDBRAKE</td>
</tr>
</thead>
<tbody>
<tr> <tr>
<td>HEVC</td> <td>命令</td>
<td :class="{warning: !config.ffmpeg.hevc, success: config.ffmpeg.hevc}"></td> <td>{{ config.handbrake.exec }}</td>
</tr> </tr>
<tr> <tr>
<td>VAAPI</td> <td>预设文件</td>
<td :class="{warning: !config.ffmpeg.vaapi, success: config.ffmpeg.vaapi}"></td> <td>{{ config.handbrake.preset_file }}</td>
</tr> </tr>
<tr> <tr>
<td>嘤伟达GPU</td> <td>预设使用</td>
<td :class="{warning: !config.ffmpeg.nvidia_gpu, success: config.ffmpeg.nvidia_gpu}"></td> <td>{{ config.handbrake.preset }}</td>
</tr> </tr>
<tr> <tr>
<td>嘤特尔GPU</td> <td>编码器参数</td>
<td :class="{warning: !config.ffmpeg.intel_gpu, success: config.ffmpeg.intel_gpu}"></td> <td>{{ config.handbrake.encopt }}</td>
</tr>
<tr>
<td>视频比特率</td>
<td>{{ config.ffmpeg.bitrate }}</td>
</tr>
<tr>
<td>GOP</td>
<td>{{ config.ffmpeg.gop }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -255,6 +260,7 @@
basic: { basic: {
exec: { exec: {
ffmpeg: false, ffmpeg: false,
handbrake: false,
danmaku: false, danmaku: false,
}, },
system: { system: {
@ -319,12 +325,12 @@
}, },
ffmpeg: { ffmpeg: {
exec: "", exec: "",
hevc: false, },
nvidia_gpu: false, handbrake: {
intel_gpu: false, exec: "",
vaapi: false, preset_file: "",
bitrate: "", preset: "",
gop: "", encopt: "",
}, },
recorder: { recorder: {
bili_dir: "", bili_dir: "",

View File

@ -1,11 +1,12 @@
import json
import os import os
import subprocess import subprocess
import warnings
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import IO from typing import IO
from config import FFMPEG_EXEC, FFMPEG_USE_HEVC, VIDEO_BITRATE, FFMPEG_USE_NVIDIA_GPU, VIDEO_CLIP_EACH_SEC, \ from config import VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC, VIDEO_OUTPUT_DIR, \
VIDEO_CLIP_OVERFLOW_SEC, \ FFMPEG_EXEC, HANDBRAKE_EXEC, HANDBRAKE_PRESET_FILE, HANDBRAKE_PRESET, HANDBRAKE_ENCOPT
FFMPEG_USE_INTEL_GPU, VIDEO_OUTPUT_DIR, VIDEO_GOP, FFMPEG_USE_VAAPI
from . import LOGGER from . import LOGGER
@ -25,143 +26,78 @@ def get_video_real_duration(filename):
def encode_video_with_subtitles(orig_filename: str, subtitles: list[str], base_ts: float): def encode_video_with_subtitles(orig_filename: str, subtitles: list[str], base_ts: float):
new_filename = base_ts_to_filename(base_ts, False) create_dt = datetime.fromtimestamp(base_ts)
new_fullpath = os.path.join(VIDEO_OUTPUT_DIR, new_filename) _duration_str = get_video_real_duration(orig_filename)
if FFMPEG_USE_HEVC: duration = duration_str_to_float(_duration_str)
if FFMPEG_USE_NVIDIA_GPU: current_sec = 0
process = get_encode_hevc_process_use_nvenc(orig_filename, subtitles, new_fullpath) _video_parts = []
elif FFMPEG_USE_VAAPI: while current_sec < duration:
process = get_encode_hevc_process_use_vaapi(orig_filename, subtitles, new_fullpath) if (current_sec + VIDEO_CLIP_OVERFLOW_SEC * 2) > duration:
elif FFMPEG_USE_INTEL_GPU: print("[-]Less than 2 overflow sec, skip")
process = get_encode_hevc_process_use_intel(orig_filename, subtitles, new_fullpath) break
else: current_dt = (create_dt + timedelta(seconds=current_sec)).strftime("%Y%m%d_%H%M_")
process = get_encode_hevc_process_use_cpu(orig_filename, subtitles, new_fullpath) process = get_encode_process_use_handbrake(orig_filename,
else: subtitles,
if FFMPEG_USE_NVIDIA_GPU: os.path.join(VIDEO_OUTPUT_DIR, "{}.mp4".format(current_dt)),
process = get_encode_process_use_nvenc(orig_filename, subtitles, new_fullpath) current_sec,
elif FFMPEG_USE_VAAPI: VIDEO_CLIP_EACH_SEC + VIDEO_CLIP_OVERFLOW_SEC)
process = get_encode_process_use_vaapi(orig_filename, subtitles, new_fullpath) handle_handbrake_output(process.stdout)
elif FFMPEG_USE_INTEL_GPU: process.wait()
process = get_encode_process_use_intel(orig_filename, subtitles, new_fullpath) _video_parts.append({
else: "base_path": VIDEO_OUTPUT_DIR,
process = get_encode_process_use_cpu(orig_filename, subtitles, new_fullpath) "file": "{}.mp4".format(current_dt),
handle_ffmpeg_output(process.stdout) })
process.wait() current_sec += VIDEO_CLIP_EACH_SEC
return [new_fullpath]
def get_encode_process_use_nvenc(orig_filename: str, subtitles: list[str], new_filename: str): def get_encode_process_use_handbrake(orig_filename: str, subtitles: list[str], new_filename: str, start_time: int, stop_time: int):
print("[+]Use Nvidia NvEnc Acceleration") print("[+]Use HandBrakeCli")
encode_process = subprocess.Popen([ encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(), HANDBRAKE_EXEC, *_common_handbrake_setting(),
"-i", orig_filename, "-vf", "--preset-import-file", HANDBRAKE_PRESET_FILE, "--preset", HANDBRAKE_PRESET,
",".join("subtitles=%s" % i for i in subtitles) + ",hwupload_cuda", "--start-at", "seconds:{}".format(start_time), "--stop-at", "seconds:{}".format(stop_time),
"-c:v", "h264_nvenc", "-i", orig_filename, "-x", HANDBRAKE_ENCOPT,
*_common_ffmpeg_params(), "--ssa-file", ",".join(i for i in subtitles),
# "-t", "10", "--ssa-offset", "{}000".format(start_time),
"--ssa-burn", ",".join("%d" % (i+1) for i in range(len(subtitles))),
"-o",
new_filename new_filename
], stdout=subprocess.PIPE) ], stdout=subprocess.PIPE)
return encode_process return encode_process
def get_encode_process_use_intel(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use Intel QSV Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-hwaccel", "qsv", "-i", orig_filename, "-vf",
",".join("subtitles=%s" % i for i in subtitles),
"-c:v", "h264_qsv",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def handle_handbrake_output(stdout: IO[bytes]):
def get_encode_process_use_vaapi(orig_filename: str, subtitles: list[str], new_filename: str): out_time = "0:0:0.0"
print("[+]Use VAAPI Acceleration") speed = "0"
encode_process = subprocess.Popen([ if stdout is None:
FFMPEG_EXEC, *_common_ffmpeg_setting(), print("[!]STDOUT is null")
"-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi", "-i", orig_filename, "-vf", return
"hwmap=mode=read+write+direct,format=nv12," + json_body = ""
"".join("subtitles=%s," % i for i in subtitles) + "hwmap", json_start = False
"-c:v", "h264_vaapi", _i = 0
*_common_ffmpeg_params(), while True:
# "-t", "10", line = stdout.readline()
new_filename if line == b"":
], stdout=subprocess.PIPE) break
return encode_process if json_start:
json_body += line.strip().decode("UTF-8")
if line.startswith(b"}"):
def get_encode_process_use_cpu(orig_filename: str, subtitles: list[str], new_filename: str): json_start = False
print("[+]Use CPU Encode") status_payload = json.loads(json_body)
encode_process = subprocess.Popen([ if status_payload["State"] == "WORKING":
FFMPEG_EXEC, *_common_ffmpeg_setting(), out_time = "ETA: {Hours:02d}:{Minutes:02d}:{Seconds:02d}".format_map(status_payload["Working"])
"-i", orig_filename, "-vf", speed = "{Rate:.2f}FPS".format_map(status_payload["Working"])
",".join("subtitles=%s" % i for i in subtitles), _i += 1
"-c:v", "h264", if _i % 300 == 150:
*_common_ffmpeg_params(), LOGGER.debug("[>]Speed:{}@{}".format(out_time, speed))
# "-t", "10", elif status_payload["State"] == "WORKDONE":
new_filename break
], stdout=subprocess.PIPE) continue
return encode_process if line.startswith(b"Progress:"):
json_start = True
json_body = "{"
def get_encode_hevc_process_use_nvenc(orig_filename: str, subtitles: list[str], new_filename: str): LOGGER.debug("[ ]Speed:{}@{}".format(out_time, speed))
print("[+]Use Nvidia NvEnc Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-i", orig_filename, "-vf",
"".join("subtitles=%s," % i for i in subtitles) + ",hwupload_cuda",
"-c:v", "hevc_nvenc",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def get_encode_hevc_process_use_vaapi(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use VAAPI Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi", "-i", orig_filename, "-vf",
"hwmap=mode=read+write+direct,format=nv12," +
"".join("subtitles=%s," % i for i in subtitles) + "hwmap",
"-c:v", "hevc_vaapi",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def get_encode_hevc_process_use_intel(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use Intel QSV Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-hwaccel", "qsv", "-i", orig_filename, "-vf",
",".join("subtitles=%s" % i for i in subtitles),
"-c:v", "hevc_qsv",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def get_encode_hevc_process_use_cpu(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use CPU Encode")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-i", orig_filename, "-vf",
",".join("subtitles=%s" % i for i in subtitles),
"-c:v", "hevc",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def handle_ffmpeg_output(stdout: IO[bytes]) -> str: def handle_ffmpeg_output(stdout: IO[bytes]) -> str:
@ -195,6 +131,7 @@ def duration_str_to_float(duration_str) -> float:
def quick_split_video(file): def quick_split_video(file):
warnings.warn("已过时", DeprecationWarning)
if not os.path.isfile(file): if not os.path.isfile(file):
raise FileNotFoundError(file) raise FileNotFoundError(file)
file_name = os.path.split(file)[-1] file_name = os.path.split(file)[-1]
@ -235,10 +172,9 @@ def _common_ffmpeg_setting():
) )
def _common_ffmpeg_params(): def _common_handbrake_setting():
return ( return (
"-f", "mp4", "-b:v", VIDEO_BITRATE, "-c:a", "aac", "--json",
"-preset:v", "fast", "-profile:v", "main", "-avoid_negative_ts", "1", "--crop-mode", "none", "--no-comb-detect", "--no-bwdif", "--no-decomb", "--no-detelecine", "--no-hqdn3d",
"-qmin", "18", "-qmax", "38", "-g:v", str(VIDEO_GOP), "--no-nlmeans", "--no-chroma-smooth", "--no-unsharp", "--no-lapsharp", "--no-deblock", "--align-av"
"-fflags", "+genpts", "-shortest"
) )

View File

@ -26,15 +26,9 @@ def do_workflow(video_file, danmaku_base_file, *danmaku_files):
print("弹幕文件", danmaku_file, "异常") print("弹幕文件", danmaku_file, "异常")
continue continue
print(result) print(result)
file_need_split = encode_video_with_subtitles(video_file, result, start_ts) _video_parts = encode_video_with_subtitles(video_file, result, start_ts)
_video_parts = []
for file in file_need_split:
_video_parts += quick_split_video(file)
# clean files # clean files
for file in result: for file in result:
if os.path.isfile(file): if os.path.isfile(file):
os.remove(file) os.remove(file)
for file in file_need_split:
if os.path.isfile(file):
os.remove(file)
return _video_parts return _video_parts