"""Fail-closed PeerTube 0.2.0 adapter; freeze with PyInstaller as a real PE .exe. Video encoding is always NVENC. CPU image/audio processing is permitted. Only reviewed argument shapes are accepted, including explicit copy-only remux. """ import json import os import re import subprocess import sys import threading import time from pathlib import Path HERE = Path(sys.executable if getattr(sys, 'frozen', False) else __file__).resolve().parent REAL = Path(os.environ.get('PT_FFMPEG_EXE', str(HERE / 'ffmpeg.exe'))) PROBE = Path(os.environ.get('PT_FFPROBE_EXE', str(HERE / 'ffprobe.exe'))) STATE = Path(os.environ.get('PT_STATE_DIR', r'C:\pt-worker\state')) DEVICE = os.environ.get('PT_NVENC_DEVICE', '0') TARGET = os.environ.get('PT_NVENC_TARGET', 'h264_nvenc') QUERIES = ('-version', '-formats', '-encoders', '-filters', '-codecs', '-hwaccels') FLAGS = {'-y', '-n', '-nostdin', '-hide_banner', '-an', '-vn', '-shortest', '-copyts'} VALUES = {'-i', '-vcodec', '-c:v', '-codec:v', '-acodec', '-c:a', '-codec:a', '-c', '-vf', '-filter:v', '-pix_fmt', '-preset', '-preset:v', '-b_strategy', '-bf', '-tune', '-crf', '-maxrate', '-maxrate:v', '-bufsize', '-bufsize:v', '-b:v', '-vb', '-b:a', '-ab', '-q:a', '-g', '-g:v', '-r', '-r:v', '-threads', '-f', '-movflags', '-map', '-map_metadata', '-max_muxing_queue_size', '-channel_layout', '-ac', '-ar', '-t', '-loop', '-hls_time', '-hls_list_size', '-hls_playlist_type', '-hls_segment_filename', '-hls_segment_type', '-hls_flags', '-bsf:a', '-loglevel', '-v', '-sc_threshold'} def event(kind, **fields): try: STATE.mkdir(parents=True, exist_ok=True) with (STATE / 'events.jsonl').open('a', encoding='utf8') as f: f.write(json.dumps({'time': time.time(), 'kind': kind, **fields}) + '\n') except OSError: pass def probe(filename): p = subprocess.run([str(PROBE), '-v', 'error', '-show_streams', '-show_format', '-of', 'json', filename], capture_output=True, timeout=30) if p.returncode: raise ValueError('probe failed') return json.loads(p.stdout) def parse(args): if len(args) < 2 or args[-1].startswith('-'): raise ValueError('missing output') entries, i = [], 0 while i < len(args) - 1: key = args[i] if key in FLAGS: entries.append((key, None)); i += 1 elif key in VALUES and i + 1 < len(args) - 1: entries.append((key, args[i + 1])); i += 2 else: raise ValueError('unknown option or extra output') return entries def rewrite(args, input_probe=None): if len(args) == 1 and args[0] in QUERIES: return args, None if TARGET not in ('h264_nvenc', 'hevc_nvenc') or not DEVICE.isdigit() or (STATE / 'gpu-unavailable').exists(): raise ValueError('GPU disabled') entries = parse(args) # No codec aliases, extra output streams, or global software encoders can escape validation. video = [(k, v) for k, v in entries if k in ('-vcodec', '-c:v', '-codec:v')] generic = [v for k, v in entries if k == '-c'] audio = [v for k, v in entries if k in ('-acodec', '-c:a', '-codec:a')] if any(v not in ('copy', 'aac', 'libfdk_aac', 'libmp3lame') for v in audio): raise ValueError('unknown audio encoder') if generic: if generic != ['copy'] or video or audio: raise ValueError('unreviewed global codec') return args, None if len(video) > 1: raise ValueError('multiple video encoders') if '-vn' in args: if video: raise ValueError('contradictory video options') return args, None if video and video[0][1] == 'copy': return args, None if len(video) != 1 or video[0][1] != 'libx264': raise ValueError('unknown video encoder') inputs = [v for k, v in entries if k == '-i'] if not inputs or len(inputs) > 2: raise ValueError('unreviewed inputs') data = input_probe if input_probe is not None else probe(inputs[0]) stream = next(s for s in data['streams'] if s['codec_type'] == 'video') codec = stream['codec_name'] still = codec in ('png', 'mjpeg', 'webp') and '-loop' in args if not still and codec not in ('h264', 'hevc', 'vp9'): raise ValueError('source has no reviewed NVDEC path') if not still and stream.get('pix_fmt') not in ('yuv420p', 'yuv420p10le', 'nv12', 'p010le'): raise ValueError('unsupported pixel format') if stream.get('color_transfer') in ('smpte2084', 'arib-std-b67'): raise ValueError('HDR needs reviewed tone mapping') if stream.get('tags', {}).get('rotate') or any(s.get('rotation', 0) for s in stream.get('side_data_list', [])): raise ValueError('rotation needs reviewed GPU path') width, height = stream['width'], stream['height'] w, h = width // 2 * 2, height // 2 * 2 filters = [v for k, v in entries if k in ('-vf', '-filter:v')] if len(filters) > 1: raise ValueError('multiple filters') if filters: value = filters[0] if value == 'scale=trunc(iw/2)*2:trunc(ih/2)*2' and still: pass else: match = re.fullmatch(r'scale=w=(-2|\d+):h=(-2|\d+)', value) if not match: raise ValueError('unreviewed filter') w, h = map(int, match.groups()) if w == -2 and h > 0: w = max(2, round(width * h / height / 2) * 2) elif h == -2 and w > 0: h = max(2, round(height * w / width / 2) * 2) if w <= 0 or h <= 0 or w % 2 or h % 2 or w > 7680 or h > 4320: raise ValueError('invalid dimensions') out = ['-init_hw_device', f'cuda=nv:{DEVICE}', '-filter_hw_device', 'nv'] first = True for key, value in entries: if key == '-i' and first: if not still: out += ['-hwaccel', 'cuda', '-hwaccel_device', DEVICE, '-hwaccel_output_format', 'cuda', '-noautorotate'] first = False if key in ('-pix_fmt', '-preset', '-preset:v', '-b_strategy', '-bf', '-tune', '-crf', '-vf', '-filter:v'): continue if key in ('-vcodec', '-c:v', '-codec:v'): value = TARGET out.append(key) if value is not None: out.append(value) prefix = 'format=nv12,hwupload_cuda,' if still else '' out += ['-vf', prefix + f'scale_cuda=w={w}:h={h}:format=nv12', '-preset', 'p4', '-gpu', DEVICE, '-bf', '2', '-rc', 'vbr'] rate = next((v for k, v in entries if k in ('-maxrate:v', '-maxrate')), None) if rate and not any(k in ('-b:v', '-vb') for k, _ in entries): out += ['-b:v', rate] out.append(args[-1]) uuid = None try: uuid = json.loads((STATE / 'inputs' / (Path(inputs[0]).name + '.json')).read_text())['uuid'] except (OSError, ValueError, KeyError): pass return out, {'job_uuid': uuid, 'source_codec': codec, 'target_codec': 'hevc' if TARGET == 'hevc_nvenc' else 'h264', 'encoder': TARGET, 'source_width': width, 'source_height': height, 'target_width': w, 'target_height': h, 'duration': float(data.get('format', {}).get('duration', 0) or 0), 'static_image_upload': still} def encoder_util(): try: p = subprocess.run(['nvidia-smi.exe', '-i', DEVICE, '--query-gpu=utilization.encoder', '--format=csv,noheader,nounits'], capture_output=True, text=True, timeout=3) return float(p.stdout.strip()) if p.returncode == 0 else None except (OSError, ValueError, subprocess.SubprocessError): return None def main(): try: args, info = rewrite(sys.argv[1:]) if not REAL.is_file(): raise ValueError('ffmpeg missing') if info is None: sys.exit(subprocess.call([str(REAL), *args])) start = time.monotonic() # Do not persist ffmpeg command lines, media paths, stderr, or signed URLs. p = subprocess.Popen([str(REAL), *args], stdout=sys.stdout, stderr=subprocess.PIPE, text=True, encoding='utf8', errors='replace') progress = {} def consume(): for line in p.stderr: sys.stderr.write(line); sys.stderr.flush() for k, v in re.findall(r'(frame|fps|speed)=\s*([\d.]+)', line): progress[k] = float(v) reader = threading.Thread(target=consume, daemon=True); reader.start() samples = [] while p.poll() is None: value = encoder_util() if value is not None: samples.append(value) time.sleep(.2) reader.join(timeout=2) seconds = time.monotonic() - start maximum = max(samples) if samples else None # Short jobs can end inside the utilization averaging interval. They still # explicitly use NVENC; boot/driver self-test provides mandatory GPU proof. no_signal = seconds >= 3 and (maximum is None or maximum == 0) rc = p.returncode or (78 if no_signal else 0) event('encode', **info, seconds=seconds, returncode=rc, ffmpeg_returncode=p.returncode, engine_busy_max=maximum, engine_samples=len(samples), engine_counters_readable=bool(samples), frames=progress.get('frame'), ffmpeg_fps=progress.get('fps'), ffmpeg_speed=progress.get('speed'), realtime_factor=info['duration'] / seconds if info['duration'] else None) if rc: event('gpu_failure', reason='nvenc_transcode_failed' if p.returncode else 'no_encoder_signal') sys.exit(rc) except (ValueError, OSError, subprocess.SubprocessError, KeyError, StopIteration): event('gpu_failure', reason='unreviewed_or_unavailable_nvenc') print('PT-WORKER: NVENC adapter refused command; drain required', file=sys.stderr) sys.exit(78) if __name__ == '__main__': main()