Add opt-in FFmpeg continuity slates with per-viewer localization

This commit is contained in:
2026-10-23 22:35:40 +02:00
parent d097aad032
commit 5f6b0b5ff6
22 changed files with 1407 additions and 30 deletions
+71
View File
@@ -0,0 +1,71 @@
"""Run the isolated synthetic C# harness and measure its own source FFmpeg only.
No Jellyfin configuration changes, restarts, API keys or receiver connections.
"""
import argparse
import json
import os
from pathlib import Path
import signal
import subprocess
import time
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--harness', required=True)
parser.add_argument('--ffmpeg', default='/usr/lib/jellyfin-ffmpeg/ffmpeg')
parser.add_argument('--output', required=True)
parser.add_argument('--long', action='store_true')
args = parser.parse_args()
root = Path(args.output).resolve()
if root.exists():
raise ValueError('Choose a new output directory')
samples, previous = [], {}
with Path(str(root) + '.log').open('x') as log:
process = subprocess.Popen([args.harness, '--long-continuity-e2e' if args.long else '--continuity-e2e', args.ffmpeg, str(root)], stdout=log,
stderr=subprocess.STDOUT, start_new_session=True)
deadline = time.monotonic() + 240
try:
while process.poll() is None:
if time.monotonic() > deadline:
raise TimeoutError('Synthetic harness deadline exceeded')
now = time.monotonic()
for proc in Path('/proc').glob('[0-9]*'):
try:
if not str((proc / 'cwd').resolve()).startswith(str(root / 'cache' / 'source-')):
continue
value = (proc / 'stat').read_text()
fields = value[value.rfind(')') + 2:].split()
cpu = (int(fields[11]) + int(fields[12])) / os.sysconf('SC_CLK_TCK')
rss = int(fields[21]) * os.sysconf('SC_PAGE_SIZE') / 1048576
if proc.name in previous:
before, at = previous[proc.name]
samples.append({'one_core_percent': round((cpu-before)/(now-at)*100, 1),
'rss_mib': round(rss, 1)})
previous[proc.name] = (cpu, now)
except OSError:
pass
time.sleep(1)
finally:
if process.poll() is None:
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait()
report = {'harness_exit_code': process.returncode, 'samples': samples,
'scope': 'Only synthetic plugin source encoding; excludes fixture and player decoders'}
(root / 'cpu-sample.json').write_text(json.dumps(report, indent=2))
if process.returncode == 0 and args.long:
subprocess.run(['python3', str(Path(__file__).with_name('verify-continuity-recording.py')), '--output', str(root), '--ffmpeg', args.ffmpeg], check=True)
print(Path(str(root) + '.log').read_text()[-3500:])
print(json.dumps({'exit_code': process.returncode, 'cpu_samples': len(samples),
'maximum_one_core_percent': max((s['one_core_percent'] for s in samples), default=None),
'maximum_rss_mib': max((s['rss_mib'] for s in samples), default=None)}))
raise SystemExit(0 if process.returncode == 0 else 1)
if __name__ == '__main__':
main()
@@ -0,0 +1,58 @@
"""Verify actual rendered stages in the synthetic long-trial recordings.
Copies of H.264 packets alone are insufficient evidence that clients saw a
retry slate or a recovered channel. Compare decoded text pixels, not file names.
"""
import argparse
import json
from pathlib import Path
import subprocess
def pixels(ffmpeg, path, second=0):
command = [ffmpeg, '-hide_banner', '-loglevel', 'error', '-nostdin', '-threads', '1', '-filter_threads', '1',
'-ss', str(second), '-i', str(path), '-frames:v', '1', '-vf',
'crop=1920:260:0:430,scale=640:86:flags=area', '-pix_fmt', 'gray', '-f', 'rawvideo', 'pipe:1']
output = subprocess.run(command, capture_output=True, check=True, timeout=20).stdout
if len(output) != 640 * 86:
raise RuntimeError('Incomplete video frame')
return output
def difference(first, second):
return sum(abs(a-b) for a, b in zip(first, second)) / len(first)
def verify(root, ffmpeg):
result = json.loads((root / 'result.json').read_text())
if result['FailureSeconds'] != 75:
raise ValueError('Use the long trial with the documented stage times')
errors = {}
for language in ('pl', 'en'):
waiting = pixels(ffmpeg, root / (language + '-waiting-slate.ts'))
retrying = pixels(ffmpeg, root / (language + '-retrying-slate.ts'))
observed_waiting = pixels(ffmpeg, root / (language + '.mkv'), 1)
observed_retrying = pixels(ffmpeg, root / (language + '.mkv'), 70)
live = pixels(ffmpeg, root / (language + '.mkv'), 39)
recovered = pixels(ffmpeg, root / (language + '.mkv'), 140)
metrics = {'waiting_error': difference(waiting, observed_waiting),
'retrying_error': difference(retrying, observed_retrying),
'live_difference': difference(retrying, live),
'recovered_difference': difference(retrying, recovered)}
assert metrics['waiting_error'] < 0.5 and metrics['retrying_error'] < 0.5, metrics
assert metrics['live_difference'] > 15 and metrics['recovered_difference'] > 15, metrics
other = 'en' if language == 'pl' else 'pl'
assert difference(observed_retrying, pixels(ffmpeg, root / (other + '-retrying-slate.ts'))) > 2
errors[language] = {key: round(value, 4) for key, value in metrics.items()}
result.update(RenderedWaitingVerified=True, RenderedRetryingVerified=True, RenderedLiveRecoveryVerified=True,
RenderedPerViewerLanguageVerified=True, VisualErrorMetrics=errors)
(root / 'result.json').write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps({'rendered_stages_verified': True, 'metrics': errors}))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--ffmpeg', default='/usr/lib/jellyfin-ffmpeg/ffmpeg')
args = parser.parse_args()
verify(args.output, args.ffmpeg)