debb3a5d67
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
88 lines
3.7 KiB
Python
Executable File
88 lines
3.7 KiB
Python
Executable File
"""FFmpeg 共享工具 — packages/shared 层.
|
||
|
||
仅包含与业务无关的底层原语:FFmpeg/FFprobe 二进制路径、run_ffmpeg 执行器。
|
||
业务相关的滤镜构建、视频探测等留在 apps/worker/video_processing/ffmpeg_utils.py。
|
||
|
||
application 层和 worker 层都可以引用本模块,避免跨层依赖。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
import shutil
|
||
import subprocess # nosec B404
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
||
|
||
FFMPEG_BIN: str = shutil.which("ffmpeg") or "ffmpeg"
|
||
FFPROBE_BIN: str = shutil.which("ffprobe") or "ffprobe"
|
||
|
||
# FFmpeg 执行默认超时(秒),防止 FFmpeg hang 住导致进程永久阻塞
|
||
# 默认 30 分钟,足够处理大部分短视频渲染;超长视频可单独传参覆盖
|
||
DEFAULT_FFMPEG_TIMEOUT = 1800
|
||
|
||
# ── 编码参数(集中配置,支持环境变量覆盖)────────────────────────────────────
|
||
# preset 从 medium → fast,渲染速度提升 30%+,画质几乎无损(CRF 相同时 PSNR 差异 <0.1dB)
|
||
# 可通过环境变量 FFMPEG_ENCODE_PRESET 覆盖(如 ultrafast 追求极致速度,veryslow 追求极致压缩)
|
||
FFMPEG_ENCODE_PRESET: str = os.environ.get("FFMPEG_ENCODE_PRESET", "fast")
|
||
# CRF 保持 23(libx264 默认质量),可通过 FFMPEG_ENCODE_CRF 覆盖
|
||
FFMPEG_ENCODE_CRF: str = os.environ.get("FFMPEG_ENCODE_CRF", "23")
|
||
# 编码线程数:0 = 自动检测 CPU 核心数,充分利用多核
|
||
FFMPEG_ENCODE_THREADS: str = os.environ.get("FFMPEG_ENCODE_THREADS", "0")
|
||
|
||
|
||
# ── FFmpeg 执行 ───────────────────────────────────────────────────────────────
|
||
|
||
|
||
def run_ffmpeg(
|
||
command: list[str],
|
||
*,
|
||
capture_output: bool = True,
|
||
timeout: int | None = DEFAULT_FFMPEG_TIMEOUT,
|
||
) -> tuple[str, str]:
|
||
"""执行 FFmpeg 命令(统一入口)。
|
||
|
||
Args:
|
||
command: 完整的 ffmpeg 命令列表(含 "ffmpeg" 本身)
|
||
capture_output: 是否捕获 stdout/stderr
|
||
timeout: 超时时间(秒),默认 1800s(30分钟);None 表示不设超时(不推荐)
|
||
|
||
Returns:
|
||
(stdout, stderr) 元组
|
||
|
||
Raises:
|
||
subprocess.CalledProcessError: 命令执行失败时抛出,
|
||
异常信息包含完整 stderr 以便排查。
|
||
subprocess.TimeoutExpired: 超时未完成时抛出,FFmpeg 进程会被 kill。
|
||
"""
|
||
try:
|
||
result = subprocess.run( # nosec B603
|
||
command,
|
||
check=True,
|
||
stdout=subprocess.PIPE if capture_output else None,
|
||
stderr=subprocess.PIPE if capture_output else None,
|
||
text=True,
|
||
timeout=timeout,
|
||
)
|
||
return (result.stdout or "", result.stderr or "")
|
||
except subprocess.TimeoutExpired:
|
||
logger.error(
|
||
"FFmpeg 命令超时 (%ds): command=%s",
|
||
timeout or -1,
|
||
" ".join(str(c) for c in command[:20]),
|
||
)
|
||
raise
|
||
except subprocess.CalledProcessError as e:
|
||
# 把完整 stderr 打到日志,方便排查 exit code 183 等问题
|
||
stderr_text = (e.stderr or "").strip()
|
||
logger.error(
|
||
"FFmpeg 命令失败: exit_code=%d command=%s\nstderr:\n%s",
|
||
e.returncode,
|
||
" ".join(str(c) for c in command[:20]), # 截断过长的命令
|
||
stderr_text[:5000], # 截断过长的 stderr
|
||
)
|
||
raise
|