import subprocess
import os
import glob
from db import update_operation, update_file_media_info, get_operation_by_id, mark_backup_deleted, get_file_by_path, calculate_and_save_stats
from mediascan import get_media_info_with_ffprobe
from thumbnails import invalidate_thumbs
from notifications import notify
def detect_available_accelerators():
available = ["CPU"]
# VAAPI — проверяем через vainfo на конкретных устройствах
if _find_vaapi_device() is not None:
available.append("VAAPI")
# Intel QSV — требует intel-media-sdk/oneVPL и сборки ffmpeg с QSV
if _has_ffmpeg_hwaccel("qsv") and _has_ffmpeg_encoder("h264_qsv"):
available.append("QSV")
# NVENC — наличие NVIDIA устройства или nvidia-smi
if os.path.exists("/dev/nvidia0") or _cmd_succeeds(["nvidia-smi", "-L"]):
available.append("NVENC")
# AMF — AMD GPU + ffmpeg скомпилирован с AMF (на Linux AMF экспериментальный)
if _has_amd_gpu() and _has_ffmpeg_encoder("h264_amf"):
available.append("AMF")
return available
def _cmd_succeeds(cmd):
try:
result = subprocess.run(cmd, capture_output=True, timeout=5)
return result.returncode == 0
except Exception:
return False
def _find_vaapi_device():
"""Возвращает путь к первому рабочему VAAPI render-устройству или None."""
import glob as _glob
for device in sorted(_glob.glob("/dev/dri/renderD*")):
result = subprocess.run(
["vainfo", "--display", "drm", "--device", device],
capture_output=True, timeout=5
)
if result.returncode == 0:
return device
# Fallback: vainfo без явного указания устройства
if _cmd_succeeds(["vainfo"]):
return "/dev/dri/renderD128"
return None
def _has_ffmpeg_encoder(encoder_name):
try:
result = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True, timeout=10)
return encoder_name in result.stdout
except Exception:
return False
def _has_ffmpeg_hwaccel(accel_name):
try:
result = subprocess.run(["ffmpeg", "-hide_banner", "-hwaccels"],
capture_output=True, text=True, timeout=5)
return accel_name in result.stdout
except Exception:
return False
def _has_amd_gpu():
try:
result = subprocess.run(["lspci"], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
return any(
("vga" in line or "3d controller" in line) and ("amd" in line or "radeon" in line)
for line in result.stdout.lower().splitlines()
)
except Exception:
pass
return False
def get_hardware_codec(software_codec, hardware_acceleration):
codec_map = {
"libx265": {
"NVENC": "hevc_nvenc",
"VAAPI": "hevc_vaapi",
"QSV": "hevc_qsv",
"AMF": "hevc_amf",
"CPU": "libx265"
},
"libx264": {
"NVENC": "h264_nvenc",
"VAAPI": "h264_vaapi",
"QSV": "h264_qsv",
"AMF": "h264_amf",
"CPU": "libx264"
},
"libaom-av1": {
"NVENC": None, # NVENC не поддерживает AV1 в большинстве сборок
"VAAPI": "av1_vaapi", # Intel Xe+, AMD RX 7000+
"QSV": "av1_qsv", # Intel Arc+
"AMF": "av1_amf", # AMD RX 7000+
"CPU": "libaom-av1"
},
"libvpx-vp9": {
"NVENC": None, # NVENC не поддерживает VP9
"VAAPI": "vp9_vaapi", # Только Intel VAAPI; на AMD почти не работает
"QSV": None, # QSV VP9 encode не поддерживается
"AMF": None, # AMF VP9 encode не поддерживается
"CPU": "libvpx-vp9"
}
}
# Проверка наличия кодека в таблице
if software_codec not in codec_map:
raise ValueError(f"Unsupported software codec: {software_codec}")
if hardware_acceleration not in codec_map[software_codec]:
raise ValueError(f"Unsupported hardware acceleration type: {hardware_acceleration}")
# Получение аппаратного кодека
hardware_codec = codec_map[software_codec][hardware_acceleration]
if hardware_codec is None:
return software_codec
return hardware_codec
# Ошибка CUDA-фильтра при изменении параметров видеопотока (BDRemux и т.п.)
_NVENC_CUDA_FILTER_ERROR = "Impossible to convert between the formats supported by the filter"
def get_ffmpeg_transcoding_cmd(file_path, acceleration, codec, resolution, crf, preset, cpu_used, output_file, _nvenc_full_cuda=False):
software_codec = codec
codec = get_hardware_codec(software_codec, acceleration)
if codec == software_codec:
acceleration = "CPU"
width = resolution
hw_input_flags = []
if acceleration == "VAAPI":
vaapi_device = _find_vaapi_device() or "/dev/dri/renderD128"
hw_input_flags = ["-vaapi_device", vaapi_device]
elif acceleration == "NVENC":
if _nvenc_full_cuda:
# Полный CUDA-пайплайн: decode GPU → scale GPU → encode GPU
hw_input_flags = ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"]
else:
# CPU-scale fallback: decode GPU → scale CPU → encode GPU
hw_input_flags = ["-hwaccel", "cuda"]
elif acceleration == "QSV":
hw_input_flags = ["-hwaccel", "qsv"]
# AMF: не требует hwaccel-флага на Linux — AMF SDK сам управляет устройством
if acceleration == "VAAPI":
vf = f"format=nv12,hwupload,scale_vaapi={width}:-2"
elif acceleration == "NVENC" and _nvenc_full_cuda:
vf = f"scale_cuda={width}:-2:passthrough=0:format=yuv420p"
else:
vf = f"scale={width}:-2"
nvenc_preset_map = {
"ultrafast": "p1", "superfast": "p1", "veryfast": "p2",
"faster": "p3", "fast": "p4", "medium": "p5",
"slow": "p6", "slower": "p7", "veryslow": "p7"
}
amf_preset_map = {
"ultrafast": "speed", "superfast": "speed", "veryfast": "speed",
"faster": "speed", "fast": "speed", "medium": "balanced",
"slow": "quality", "slower": "quality", "veryslow": "quality"
}
if acceleration == "NVENC":
quality_flag, quality_value = "-cq", str(crf)
elif acceleration == "QSV":
# global_quality — аналог CRF для Intel QSV (ICQ режим)
quality_flag, quality_value = "-global_quality", str(crf)
elif acceleration in ("VAAPI", "AMF"):
quality_flag, quality_value = "-qp", str(crf)
else:
quality_flag, quality_value = "-crf", str(crf)
command = ["ffmpeg"] + hw_input_flags + [
"-i", file_path,
"-map", "0:V", # main video only (excludes attached pics / cover art)
"-map", "0:a?", # all audio streams
"-map", "0:s?", # all subtitle streams (optional)
"-c:v", codec,
"-vf", vf,
"-c:a", "copy",
"-c:s", "copy",
quality_flag, quality_value
]
av1_vp9_codecs = ["libaom-av1", "av1_vaapi", "av1_amf", "av1_qsv", "libvpx-vp9", "vp9_vaapi"]
if codec in av1_vp9_codecs:
command.extend(["-cpu-used", str(cpu_used)])
elif acceleration == "NVENC":
command.extend(["-preset", nvenc_preset_map.get(str(preset), "p4")])
elif acceleration == "AMF":
command.extend(["-quality", amf_preset_map.get(str(preset), "balanced")])
elif acceleration in ("CPU", "QSV"):
command.extend(["-preset", str(preset)])
# VAAPI: пресеты не поддерживаются, не добавляем
command.append(output_file)
return command
def _run_ffmpeg_process(command, task, socketio, watch_cuda_error=False):
"""
Запускает ffmpeg, эмитит progress-события.
Возвращает (process, output_tail, cuda_error_detected).
Если watch_cuda_error=True и обнаружена ошибка CUDA-фильтра —
немедленно завершает процесс и возвращает cuda_error_detected=True.
"""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
task["process"] = process
output_tail = []
cuda_error_detected = False
for line in process.stdout:
output_tail.append(line.rstrip())
if len(output_tail) > 40:
output_tail.pop(0)
if watch_cuda_error and _NVENC_CUDA_FILTER_ERROR in line:
cuda_error_detected = True
process.terminate()
break
if "frame=" in line:
socketio.emit('progress', {
"task": {
"id": task["id"],
"file": task["file"],
"command": task["command"]
},
"message": line.strip()
})
process.wait()
return process, output_tail, cuda_error_detected
def _copy_with_progress(src, dst, file_path, socketio, chunk_size=2 * 1024 * 1024):
total = os.path.getsize(src)
copied = 0
with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
while True:
buf = fsrc.read(chunk_size)
if not buf:
break
fdst.write(buf)
copied += len(buf)
percent = int(copied / total * 100)
socketio.emit('copy-progress', {'file': file_path, 'percent': percent})
def transcode_file(transcoding_tasks, socketio, file_path, dest_path, acceleration, codec, resolution, crf, preset, cpu_used, operation_id=None, delete_original=False, user_id=None):
# Phase 1: copy original to backup location
if dest_path:
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
socketio.emit('copy-progress', {'file': file_path, 'percent': 0})
_copy_with_progress(file_path, dest_path, file_path, socketio)
base, ext = os.path.splitext(file_path)
output_file = base + '_transcoded' + ext
if os.path.exists(output_file):
os.remove(output_file)
# Для NVENC: сначала пробуем полный CUDA-пайплайн, при ошибке — CPU-scale fallback
nvenc = (acceleration == "NVENC")
command = get_ffmpeg_transcoding_cmd(
file_path, acceleration, codec, resolution, crf, preset, cpu_used, output_file,
_nvenc_full_cuda=nvenc
)
fallback_command = get_ffmpeg_transcoding_cmd(
file_path, acceleration, codec, resolution, crf, preset, cpu_used, output_file,
_nvenc_full_cuda=False
) if nvenc else None
print(" ".join(command))
task = {
"id": "id_" + str(len(transcoding_tasks) + 1),
"file": file_path,
"output": output_file,
"command": command,
"process": None
}
transcoding_tasks[task["id"]] = task
# Запуск с мониторингом CUDA-ошибки
process, output_tail, cuda_error = _run_ffmpeg_process(
command, task, socketio, watch_cuda_error=nvenc
)
# CUDA-фильтр упал — переключаемся на CPU-scale и рестартуем
if cuda_error and fallback_command:
if os.path.exists(output_file):
os.remove(output_file)
print("NVENC CUDA filter error detected — retrying with CPU-assisted scaling")
socketio.emit('progress', {
"task": {"id": task["id"], "file": task["file"], "command": fallback_command},
"message": "CUDA pipeline failed — retrying with CPU-assisted scaling..."
})
task["command"] = fallback_command
process, output_tail, _ = _run_ffmpeg_process(
fallback_command, task, socketio, watch_cuda_error=False
)
# Step 3: Replace the original file with the transcoded file
file_name = os.path.basename(file_path)
if process.returncode == 0:
os.replace(output_file, file_path)
if operation_id:
update_operation(operation_id, "completed")
fresh_info = get_media_info_with_ffprobe(file_path)
if fresh_info:
update_file_media_info(file_path, os.path.getsize(file_path), fresh_info)
db_file = get_file_by_path(file_path)
if db_file:
invalidate_thumbs(db_file["id"])
if delete_original:
op = get_operation_by_id(operation_id)
if op and op.get("backup_path") and os.path.exists(op["backup_path"]):
os.remove(op["backup_path"])
mark_backup_deleted(operation_id)
stats = calculate_and_save_stats()
notify(socketio, user_id, "success", f"Transcoding complete: {file_name}",
f"Codec: {codec} · Acceleration: {acceleration}")
socketio.emit('completed', {
"task": {
"id": task["id"],
"file": task["file"],
"command": task["command"]
},
"message": f"File {file_path} transcoded successfully",
"stats": stats
})
del transcoding_tasks[task["id"]]
else:
if task["id"] in transcoding_tasks:
if operation_id:
update_operation(operation_id, "failed")
ffmpeg_log = "\n".join(output_tail[-20:]) if output_tail else ""
notify(socketio, user_id, "error", f"Transcoding failed: {file_name}", ffmpeg_log or None)
socketio.emit('error', {
"task": {
"id": task["id"],
"file": task["file"],
"command": task["command"]
},
"message": f"Transcoding failed for {file_path}",
"ffmpeg_log": ffmpeg_log
})
del transcoding_tasks[task["id"]]
else:
if operation_id:
update_operation(operation_id, "canceled")
socketio.emit('canceled', {
"task": {
"id": task["id"],
"file": task["file"],
"command": task["command"]
},
"message": f"Transcoding canceled for {file_path}"
})
if os.path.exists(output_file):
os.remove(output_file)