171 lines
6.4 KiB
Python
171 lines
6.4 KiB
Python
|
|
"""
|
||
|
|
Video/audio processing functionality using FFmpeg.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import subprocess
|
||
|
|
from colorama import Fore, Style
|
||
|
|
|
||
|
|
from .utils import detect_hardware_acceleration, get_hwaccel_encoder
|
||
|
|
|
||
|
|
|
||
|
|
class StreamProcessor:
|
||
|
|
"""Handles video/audio processing using FFmpeg."""
|
||
|
|
|
||
|
|
def __init__(self, os_type: str, ffmpeg_path: str, config: dict):
|
||
|
|
"""
|
||
|
|
Initialize the stream processor.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
os_type: Operating system type ('windows' or 'linux')
|
||
|
|
ffmpeg_path: Path to FFmpeg executable
|
||
|
|
config: Configuration dictionary with FFmpeg settings
|
||
|
|
"""
|
||
|
|
self.os_type = os_type
|
||
|
|
self.ffmpeg_path = ffmpeg_path
|
||
|
|
self.quality = config.get('quality', 'best')
|
||
|
|
self.only_raw = config.get('onlyRaw', False)
|
||
|
|
self.ffmpeg_threads = config.get('ffmpeg_threads', 0)
|
||
|
|
self.ffmpeg_audio_codec = config.get('ffmpeg_audio_codec', 'aac')
|
||
|
|
self.ffmpeg_audio_samplerate = config.get('ffmpeg_audio_samplerate', 48000)
|
||
|
|
self.ffmpeg_audio_bitrate = config.get('ffmpeg_audio_bitrate', '192k')
|
||
|
|
self.ffmpeg_error_recovery = config.get('ffmpeg_error_recovery', True)
|
||
|
|
self.ffmpeg_faststart = config.get('ffmpeg_faststart', True)
|
||
|
|
self.ffmpeg_progress = config.get('ffmpeg_progress', False)
|
||
|
|
self.hwaccel_type = detect_hardware_acceleration(
|
||
|
|
config.get('ffmpeg_hwaccel', 'auto'),
|
||
|
|
os_type
|
||
|
|
)
|
||
|
|
|
||
|
|
def process_raw_stream(self, raw_path: str, output_path: str) -> None:
|
||
|
|
"""
|
||
|
|
Process raw .ts file into mp4/mp3 using ffmpeg.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
raw_path: Path to the raw .ts file
|
||
|
|
output_path: Path for the processed output file
|
||
|
|
"""
|
||
|
|
if not os.path.exists(raw_path):
|
||
|
|
print(f'{Fore.YELLOW}⚠ Raw file not found, skipping processing{Style.RESET_ALL}')
|
||
|
|
return
|
||
|
|
|
||
|
|
if self.only_raw:
|
||
|
|
print(f'{Fore.CYAN}Keeping raw .ts file (onlyRaw mode){Style.RESET_ALL}')
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f'{Fore.YELLOW}Processing raw stream file...{Style.RESET_ALL}')
|
||
|
|
|
||
|
|
# Build ffmpeg command based on quality
|
||
|
|
if self.quality == 'audio_only':
|
||
|
|
self._process_audio(raw_path, output_path)
|
||
|
|
else:
|
||
|
|
self._process_video(raw_path, output_path)
|
||
|
|
|
||
|
|
print(f'{Fore.GREEN}✓ Stream processed successfully{Style.RESET_ALL}')
|
||
|
|
|
||
|
|
def _process_audio(self, raw_path: str, output_path: str) -> None:
|
||
|
|
"""Process audio-only stream."""
|
||
|
|
# Audio-only conversion with modern AAC encoding
|
||
|
|
cmd = [
|
||
|
|
self.ffmpeg_path,
|
||
|
|
'-i', raw_path,
|
||
|
|
'-vn', # No video
|
||
|
|
'-c:a', self.ffmpeg_audio_codec,
|
||
|
|
'-ar', str(self.ffmpeg_audio_samplerate),
|
||
|
|
'-ac', '2', # Stereo
|
||
|
|
'-b:a', self.ffmpeg_audio_bitrate,
|
||
|
|
]
|
||
|
|
|
||
|
|
# Add threading for faster encoding
|
||
|
|
if self.ffmpeg_threads > 0:
|
||
|
|
cmd.extend(['-threads', str(self.ffmpeg_threads)])
|
||
|
|
|
||
|
|
# Add faststart for better streaming compatibility
|
||
|
|
if self.ffmpeg_faststart and output_path.endswith(('.mp4', '.m4a')):
|
||
|
|
cmd.extend(['-movflags', '+faststart'])
|
||
|
|
|
||
|
|
cmd.append(output_path)
|
||
|
|
|
||
|
|
# Run FFmpeg
|
||
|
|
if self.ffmpeg_progress:
|
||
|
|
subprocess.call(cmd)
|
||
|
|
else:
|
||
|
|
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||
|
|
|
||
|
|
def _process_video(self, raw_path: str, output_path: str) -> None:
|
||
|
|
"""Process video stream."""
|
||
|
|
cmd = [
|
||
|
|
self.ffmpeg_path,
|
||
|
|
'-y', # Overwrite output file
|
||
|
|
]
|
||
|
|
|
||
|
|
# Add hardware acceleration if enabled
|
||
|
|
if self.hwaccel_type and self.hwaccel_type != 'none':
|
||
|
|
print(f'{Fore.CYAN}Using hardware acceleration: {self.hwaccel_type}{Style.RESET_ALL}')
|
||
|
|
cmd.extend(['-hwaccel', 'auto'])
|
||
|
|
|
||
|
|
cmd.extend([
|
||
|
|
'-i', raw_path,
|
||
|
|
'-analyzeduration', '2147483647',
|
||
|
|
'-probesize', '2147483647',
|
||
|
|
])
|
||
|
|
|
||
|
|
# Threading support
|
||
|
|
if self.ffmpeg_threads >= 0:
|
||
|
|
cmd.extend(['-threads', str(self.ffmpeg_threads)])
|
||
|
|
|
||
|
|
# Error recovery options for corrupted streams
|
||
|
|
if self.ffmpeg_error_recovery:
|
||
|
|
cmd.extend([
|
||
|
|
'-fflags', '+genpts',
|
||
|
|
'-avoid_negative_ts', 'make_zero',
|
||
|
|
'-err_detect', 'ignore_err'
|
||
|
|
])
|
||
|
|
|
||
|
|
# Stream copy (fast, no re-encoding)
|
||
|
|
cmd.extend([
|
||
|
|
'-c:v', 'copy',
|
||
|
|
'-c:a', 'copy',
|
||
|
|
'-start_at_zero',
|
||
|
|
'-copyts',
|
||
|
|
])
|
||
|
|
|
||
|
|
# Add faststart for MP4 files
|
||
|
|
if self.ffmpeg_faststart and output_path.endswith('.mp4'):
|
||
|
|
cmd.extend(['-movflags', '+faststart'])
|
||
|
|
|
||
|
|
cmd.append(output_path)
|
||
|
|
|
||
|
|
# Run FFmpeg
|
||
|
|
if self.ffmpeg_progress:
|
||
|
|
subprocess.call(cmd)
|
||
|
|
else:
|
||
|
|
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||
|
|
|
||
|
|
def build_chat_output_args(self) -> str:
|
||
|
|
"""
|
||
|
|
Build FFmpeg output arguments for chat rendering with hardware acceleration support.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: Complete FFmpeg output arguments string with "{save_path}" placeholder
|
||
|
|
"""
|
||
|
|
if self.hwaccel_type and self.hwaccel_type != 'none':
|
||
|
|
encoder = get_hwaccel_encoder(self.hwaccel_type)
|
||
|
|
print(f'{Fore.CYAN}Using hardware-accelerated encoder for chat: {encoder}{Style.RESET_ALL}')
|
||
|
|
|
||
|
|
if 'nvenc' in encoder:
|
||
|
|
result = f'-c:v {encoder} -preset p4 -cq 18 -pix_fmt yuv420p "{{save_path}}"'
|
||
|
|
elif 'qsv' in encoder:
|
||
|
|
result = f'-c:v {encoder} -global_quality 18 -pix_fmt yuv420p "{{save_path}}"'
|
||
|
|
elif 'amf' in encoder:
|
||
|
|
result = f'-c:v {encoder} -qp_i 18 -pix_fmt yuv420p "{{save_path}}"'
|
||
|
|
elif 'vaapi' in encoder:
|
||
|
|
result = f'-c:v {encoder} -qp 18 -pix_fmt yuv420p "{{save_path}}"'
|
||
|
|
else:
|
||
|
|
result = f'-c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p "{{save_path}}"'
|
||
|
|
else:
|
||
|
|
# Default software encoding
|
||
|
|
result = f'-c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p "{{save_path}}"'
|
||
|
|
|
||
|
|
print(f'{Fore.CYAN}DEBUG - Generated output_args: {result}{Style.RESET_ALL}')
|
||
|
|
return result
|