Files
GigaMux/encode_dvr_high.py

297 lines
11 KiB
Python
Raw Normal View History

2025-08-17 21:27:09 -07:00
import os
import subprocess
from pathlib import Path
import json
import logging
from datetime import datetime
import shutil
# ANSI color codes
class Colors:
PURPLE = '\033[95m'
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
ENDC = '\033[0m'
def get_gpu_selection():
while True:
print(f"\n{Colors.BLUE}Select GPU slot:{Colors.ENDC}")
print("0 - First GPU")
print("1 - Second GPU")
print("2 - Third GPU")
gpu = input(f"{Colors.YELLOW}Enter GPU number (0-2):{Colors.ENDC} ").strip()
if gpu in ['0', '1', '2']:
return gpu
print(f"{Colors.RED}Invalid selection. Please try again.{Colors.ENDC}")
2026-01-18 11:12:40 -07:00
# Custom file handler that silently handles I/O errors (for network shares)
class SafeFileHandler(logging.FileHandler):
"""File handler that silently handles I/O errors during flush"""
def flush(self):
"""Override flush to silently handle I/O errors"""
try:
super().flush()
except (OSError, IOError):
# Silently ignore I/O errors (network share issues)
pass
except Exception:
# Silently ignore all other errors during flush
pass
def emit(self, record):
"""Override emit to handle errors gracefully"""
try:
super().emit(record)
except (OSError, IOError):
# Silently ignore I/O errors - we'll fall back to console output
self.handleError(record)
except Exception:
# Handle other errors
self.handleError(record)
def handleError(self, record):
"""Override to prevent error messages from being printed"""
# Don't print "--- Logging error ---" messages
pass
2025-08-17 21:27:09 -07:00
# Set up logging
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"encode_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
2026-01-18 11:12:40 -07:00
# Configure logging with custom handler that handles network share errors
handler = SafeFileHandler(log_file, mode='w', encoding='utf-8')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(handler)
# Remove default handlers to avoid duplicate output
logger.handlers = [handler]
2025-08-17 21:27:09 -07:00
def get_file_info(input_file):
cmd = [
'ffprobe',
'-v', 'error',
'-show_entries', 'format=duration,size:stream=codec_name,width,height,r_frame_rate,channels,channel_layout',
'-of', 'json',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True)
return json.loads(result.stdout)
def get_audio_labels(input_file):
cmd = [
'ffprobe',
'-v', 'error',
'-select_streams', 'a',
'-show_entries', 'stream=index:stream_tags=title',
'-of', 'json',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True)
info = json.loads(result.stdout)
labels = []
for stream in info.get('streams', []):
title = stream.get('tags', {}).get('title', None)
labels.append(title)
return labels
def format_size(size_bytes):
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024
return f"{size_bytes:.2f} TB"
2026-01-18 11:12:40 -07:00
def safe_log_info(message, print_msg=None):
"""Safely log info message, ensuring console output even if logging fails"""
try:
logging.info(message)
except (OSError, IOError) as e:
# Logging failed (likely network share issue) - print to console
if print_msg is None:
print(f"{Colors.YELLOW}[Logging failed: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.YELLOW}[Logging failed: {e}]{Colors.ENDC}")
return
except Exception as e:
# Other logging errors
if print_msg is None:
print(f"{Colors.YELLOW}[Logging error: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.YELLOW}[Logging error: {e}]{Colors.ENDC}")
return
# Always print to console if message provided
if print_msg is not None:
print(print_msg)
def safe_log_error(message, print_msg=None):
"""Safely log error message, ensuring console output even if logging fails"""
try:
logging.error(message)
except (OSError, IOError) as e:
# Logging failed (likely network share issue) - print to console
if print_msg is None:
print(f"{Colors.RED}[Logging failed: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.RED}[Logging failed: {e}]{Colors.ENDC}")
return
except Exception as e:
# Other logging errors
if print_msg is None:
print(f"{Colors.RED}[Logging error: {e}] {message}{Colors.ENDC}")
else:
print(print_msg)
print(f"{Colors.RED}[Logging error: {e}]{Colors.ENDC}")
return
# Always print to console if message provided
if print_msg is not None:
print(print_msg)
else:
print(f"{Colors.RED}{message}{Colors.ENDC}")
2025-08-17 21:27:09 -07:00
def encode_dvr(input_file, output_dir, gpu):
input_path = Path(input_file)
output_path = Path(output_dir) / f"{input_path.stem}{input_path.suffix}"
# Get file info for logging
file_info = get_file_info(str(input_path))
input_size = int(file_info['format']['size'])
duration = float(file_info['format']['duration'])
2026-01-18 11:12:40 -07:00
safe_log_info(f"Processing file: {input_path}")
safe_log_info(f"Input size: {format_size(input_size)}")
safe_log_info(f"Duration: {duration:.2f} seconds")
2025-08-17 21:27:09 -07:00
print(f"\n{Colors.BLUE}Processing file: {input_path}{Colors.ENDC}")
print(f"Input size: {format_size(input_size)}")
print(f"Duration: {duration:.2f} seconds")
# Log stream information
for i, stream in enumerate(file_info.get('streams', [])):
stream_type = 'Video' if stream.get('codec_name', '').startswith('h') else 'Audio'
2026-01-18 11:12:40 -07:00
safe_log_info(f"Stream {i} ({stream_type}):")
2025-08-17 21:27:09 -07:00
for key, value in stream.items():
2026-01-18 11:12:40 -07:00
safe_log_info(f" {key}: {value}")
2025-08-17 21:27:09 -07:00
# Skip if output file already exists
if output_path.exists():
output_size = output_path.stat().st_size
2026-01-18 11:12:40 -07:00
safe_log_info(f"Skipping {input_path} - output already exists: {output_path}")
safe_log_info(f"Output size: {format_size(output_size)}")
2025-08-17 21:27:09 -07:00
print(f"{Colors.YELLOW}Skipping {input_path} - output already exists{Colors.ENDC}")
return
# Get audio labels
audio_labels = get_audio_labels(str(input_path))
2026-01-18 11:12:40 -07:00
safe_log_info(f"Audio labels: {audio_labels}")
2025-08-17 21:27:09 -07:00
# FFmpeg command with NVIDIA HEVC encoder and maximum quality
cmd = [
'ffmpeg',
'-v', 'verbose', # Enable verbose output
'-i', str(input_path),
'-c:v', 'hevc_nvenc',
'-gpu', gpu,
'-preset', 'p7',
'-tune', 'hq',
'-rc', 'vbr', # Variable bitrate mode
'-rc-lookahead', '32', # Increase lookahead for better VBR decisions
'-spatial-aq', '1', # Enable spatial AQ for better quality
'-aq-strength', '15', # Maximum allowed AQ strength
'-cq', '30',
'-b:v', '0', # Let VBR determine bitrate
'-c:a', 'copy',
'-map', '0',
]
# Add metadata for each audio stream if label exists
for idx, label in enumerate(audio_labels):
if label:
cmd += [f'-metadata:s:a:{idx}', f'title={label}']
cmd.append(str(output_path))
try:
# Run FFmpeg and capture output
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
# Log FFmpeg output in real-time
while True:
2026-01-18 11:12:40 -07:00
try:
output = process.stderr.readline()
if output == '' and process.poll() is not None:
break
if output:
text = output.strip()
if text.startswith('frame=') or ' fps=' in text:
safe_log_info(f"Progress: {text}", f"{Colors.PURPLE}Progress: {text}{Colors.ENDC}")
else:
safe_log_info(f"FFmpeg: {text}", f"{Colors.GREEN}FFmpeg: {text}{Colors.ENDC}")
except (OSError, IOError) as e:
# I/O error reading from pipe - log it
safe_log_error(f"I/O error reading FFmpeg output: {e}")
2025-08-17 21:27:09 -07:00
break
2026-01-18 11:12:40 -07:00
except Exception as e:
# Unexpected error
safe_log_error(f"Unexpected error processing FFmpeg output: {e}")
2025-08-17 21:27:09 -07:00
if process.returncode == 0:
# Get output file info
output_info = get_file_info(str(output_path))
output_size = int(output_info['format']['size'])
compression_ratio = input_size / output_size if output_size > 0 else 0
2026-01-18 11:12:40 -07:00
safe_log_info(f"Successfully encoded: {output_path}", f"{Colors.GREEN}Successfully encoded: {output_path}{Colors.ENDC}")
safe_log_info(f"Output size: {format_size(output_size)}")
safe_log_info(f"Compression ratio: {compression_ratio:.2f}x", f"Compression ratio: {compression_ratio:.2f}x")
2025-08-17 21:27:09 -07:00
else:
2026-01-18 11:12:40 -07:00
# Convert Windows error code to signed integer if needed
return_code = process.returncode
if return_code > 2147483647: # If it's a large unsigned int, convert to signed
return_code = return_code - 4294967296
safe_log_error(f"FFmpeg process failed with return code {return_code}",
f"{Colors.RED}FFmpeg process failed with return code {return_code}{Colors.ENDC}")
2025-08-17 21:27:09 -07:00
except subprocess.CalledProcessError as e:
2026-01-18 11:12:40 -07:00
safe_log_error(f"Error encoding {input_path}: {e}", f"{Colors.RED}Error encoding {input_path}: {e}{Colors.ENDC}")
except Exception as e:
safe_log_error(f"Unexpected error encoding {input_path}: {type(e).__name__}: {e}",
f"{Colors.RED}Unexpected error encoding {input_path}: {e}{Colors.ENDC}")
2025-08-17 21:27:09 -07:00
if __name__ == "__main__":
# Get GPU selection
gpu = get_gpu_selection()
2026-01-18 11:12:40 -07:00
safe_log_info(f"Selected GPU: {gpu}")
2025-08-17 21:27:09 -07:00
input_dir = "input"
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
# Get list of files to process
files = [f for f in os.listdir(input_dir) if f.endswith(('.mp4', '.DVR.mp4'))]
total_files = len(files)
if total_files == 0:
2026-01-18 11:12:40 -07:00
safe_log_info("No files to process in input directory", f"{Colors.YELLOW}No files to process in input directory{Colors.ENDC}")
2025-08-17 21:27:09 -07:00
else:
2026-01-18 11:12:40 -07:00
safe_log_info(f"Found {total_files} files to process", f"{Colors.BLUE}Found {total_files} files to process{Colors.ENDC}")
2025-08-17 21:27:09 -07:00
for i, file in enumerate(files, 1):
input_file = os.path.join(input_dir, file)
2026-01-18 11:12:40 -07:00
safe_log_info(f"Processing file {i}/{total_files}: {file}")
2025-08-17 21:27:09 -07:00
print(f"\n{Colors.BLUE}Processing file {i}/{total_files}: {file}{Colors.ENDC}")
encode_dvr(input_file, output_dir, gpu)