65 lines
2.8 KiB
Python
65 lines
2.8 KiB
Python
"""On-demand H.264/AAC browser preview from a local MediaMTX substream."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
from urllib.parse import quote
|
|
|
|
|
|
def main():
|
|
name = os.environ.get('MTX_PATH', '')
|
|
if not re.fullmatch(r'cam_[a-f0-9]{16}_compat', name):
|
|
raise SystemExit('Invalid preview path')
|
|
data = Path(sys.argv[1]).resolve()
|
|
config = json.loads((data / 'mediamtx.yml').read_text(encoding='utf8'))
|
|
original = name.removesuffix('_compat') + '_sub'
|
|
if config['paths'].get(original, {}).get('source', 'publisher') == 'publisher':
|
|
raise SystemExit('Camera is disabled')
|
|
secret = next(u['pass'] for u in config['authInternalUsers'] if u['user'] == 'vision')
|
|
base = 'rtsp://vision:' + quote(secret, safe='') + '@127.0.0.1:18554/'
|
|
executable = shutil.which('ffmpeg')
|
|
if not executable:
|
|
raise SystemExit('Install FFmpeg to enable compatible preview')
|
|
# A bounded 768x432 / 10 fps preview keeps six lenses practical on a small
|
|
# computer. Original main/sub streams and recording retain their own codecs.
|
|
command = [executable, '-hide_banner', '-loglevel', 'error', '-nostdin',
|
|
'-threads', '1', '-filter_threads', '1', '-rtsp_transport', 'tcp',
|
|
'-timeout', '15000000', '-i', base + original,
|
|
'-map', '0:v:0', '-map', '0:a:0?', '-sn', '-dn',
|
|
'-vf', 'fps=10,scale=768:432:force_original_aspect_ratio=decrease:force_divisible_by=2',
|
|
'-c:v', 'libx264', '-threads', '1', '-preset', 'veryfast',
|
|
'-tune', 'zerolatency', '-profile:v', 'baseline', '-level:v', '3.1',
|
|
'-pix_fmt', 'yuv420p', '-crf', '26', '-maxrate', '1100k',
|
|
'-bufsize', '2200k', '-g', '20', '-keyint_min', '20', '-sc_threshold', '0',
|
|
'-c:a', 'aac', '-ar', '48000', '-ac', '1', '-b:a', '48k',
|
|
'-f', 'rtsp', '-rtsp_transport', 'tcp', base + name]
|
|
process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE, text=True, errors='replace')
|
|
|
|
def stop(_signum, _frame):
|
|
if process.poll() is None:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
|
|
signal.signal(signal.SIGTERM, stop)
|
|
signal.signal(signal.SIGINT, stop)
|
|
try:
|
|
for line in process.stderr:
|
|
# Never write connection credentials from FFmpeg diagnostics to logs.
|
|
clean = re.sub(r'rtsp://[^\s]+', '[local stream]', line.replace(secret, '[redacted]'))
|
|
print(name + ': ' + clean.rstrip(), file=sys.stderr, flush=True)
|
|
return process.wait()
|
|
finally:
|
|
stop(None, None)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|