MP4 Export for Raybig on WSL

This commit is contained in:
whoisdomi
2026-08-06 14:57:40 -05:00
parent 8b3ce51325
commit a611653837
4 changed files with 384 additions and 62 deletions
+13
View File
@@ -202,6 +202,18 @@ prepare_pandad_host_artifacts() {
rm -f "${ROOT_DIR}/selfdrive/pandad/libcan_list_to_can_capnp.a"
}
prepare_acados_host_artifacts() {
# c_generated_code is regenerated byte-for-byte each run, so SCons sees no change and skips
# relinking, leaving a larch64 solver lib from a device build in place. Drop the whole tree.
local rel=""
for rel in "selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/libacados_ocp_solver_lat.so" \
"selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/libacados_ocp_solver_long.so"; do
if archive_is_aarch64_elf "${ROOT_DIR}/${rel}"; then
rm -rf "$(dirname "${ROOT_DIR}/${rel}")"
fi
done
}
python_ui_runtime_ok() {
"${PY_BIN}" - <<'PY'
import pyray # noqa: F401
@@ -286,6 +298,7 @@ if ! python_ui_runtime_ok >/dev/null 2>&1; then
prepare_common_host_artifacts
prepare_msgq_host_artifacts
prepare_pandad_host_artifacts
prepare_acados_host_artifacts
remove_if_elf "common/params_pyx.so"
remove_if_elf "common/transformations/transformations.so"
remove_if_elf "msgq/ipc_pyx.so"
+3
View File
@@ -82,6 +82,9 @@ All three entrypoints do the same thing. `./dev` is the shortest general-purpose
- `./dev sync`
- `./dev shell`
`./onroad` replays a route into a desktop UI to watch live. To record one to an mp4 instead of
watching it in real time, use `tools/clip/run.py` (see [tools/clip/README.md](clip/README.md)).
### Desktop UI shorthands
- `./c3 [jobs] [args...]`
+91
View File
@@ -0,0 +1,91 @@
# Clip
`clip.py` records a video of a driving route by replaying it into a desktop UI and capturing the result. Use it to export footage of a route without watching it live.
## Setup
Install dependencies (this pulls in `uv`, `ffmpeg`, and `xvfb`):
```bash
tools/ubuntu_setup.sh
```
To clip a route from your comma account, authenticate first:
```bash
python3 tools/lib/auth.py
```
Public routes and `--demo` need no authentication.
`replay` and the raybig UI runtime are built automatically on first run, so expect the first
clip to take a few extra minutes.
## Usage
```bash
# Clip the demo route
python3 tools/clip/run.py --demo -o output.mp4
# Clip seconds 140-260 of a route, timing in the route ID
python3 tools/clip/run.py 78511c37de32c375/00000958--8cb1d3e165/140/260 -o clip.mp4
# Same, timing as flags
python3 tools/clip/run.py 78511c37de32c375/00000958--8cb1d3e165 -s 140 -e 260 -o clip.mp4
```
Recording happens in real time, so a 120s clip takes about 120s to produce.
## Choosing a UI
`-u/--ui` selects which desktop UI to record. The default is `c3`.
```bash
python3 tools/clip/run.py --demo -u raybig -o output.mp4
```
- `c3` renders the Qt UI into a virtual Xvfb display and screen captures it with `ffmpeg`.
- `raybig` runs the raylib UI, which exports its own frames straight to `ffmpeg`. This bypasses
screen capture entirely, so it also works where a compositor hides window contents from
`x11grab` (notably WSLg).
Note that `raybig` clips do not get the title, metadata, or route timer overlays that `c3`
clips get: those are `ffmpeg` `drawtext` filters applied during screen capture, and the raylib
UI's own export path does not run them.
## Options
| Flag | Description |
| --- | --- |
| `-o, --output` | Output path, must be `.mp4` (default `output.mp4`) |
| `-s, --start` / `-e, --end` | Clip window in seconds; omit if timing is in the route ID |
| `-u, --ui` | `c3` (default) or `raybig` |
| `-f, --file-size` | Target size in MB (default 9, sized for Discord/GitHub) |
| `-q, --quality` | `high` (hevc, default) or `low` (qcam) |
| `-x, --speed` | Record at this speed multiple |
| `-t, --title` | Burn a title into the video (`c3` only) |
| `-d, --data-dir` | Use local route data instead of downloading |
| `-p, --prefix` | openpilot prefix to isolate the run |
The default 9MB target suits short clips. For anything longer than about a minute, raise it
(`-f 250`) or the bitrate drops low enough to be visibly bad.
## WSL
Recording works on WSL2 under Windows 11, which supplies a display through WSLg. Use
`-u raybig`; `-u c3` relies on screen capture and can produce black frames there.
```bash
python3 tools/clip/run.py -u raybig -f 250 -o ~/clips/foo.mp4 78511c37de32c375/00000958--8cb1d3e165/140/260
```
Running the tool rebuilds native extensions, and this repo tracks those build artifacts in git,
so the working tree ends up dirty afterwards. That does not affect running it again, it only
matters when pulling:
```bash
git checkout -- . && git pull
```
The discarded files are rebuilt on the next run. If you have source edits you want to keep,
commit or stash them instead, since `git checkout -- .` discards all unstaged changes.
+277 -62
View File
@@ -1,26 +1,29 @@
#!/usr/bin/env python3
import logging
import math
import os
import platform
import shutil
import signal
import sys
import time
from argparse import ArgumentParser, ArgumentTypeError
from collections.abc import Sequence
from pathlib import Path
from random import randint
from subprocess import Popen
from subprocess import PIPE, Popen, TimeoutExpired
from typing import Literal
from cereal.messaging import SubMaster
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params, UnknownKeyName
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.common.utils import managed_proc
from openpilot.tools.lib.route import Route
from openpilot.tools.lib.logreader import LogReader
# cereal.messaging, common.params and common.prefix load native extensions that the raybig path
# builds at runtime (prepare_raybig_runtime), so they're imported at point of use instead - at
# module scope they'd fail before that build ever runs.
DEFAULT_OUTPUT = 'output.mp4'
DEMO_START = 90
DEMO_END = 105
@@ -30,10 +33,14 @@ PIXEL_DEPTH = '24'
RESOLUTION = '2160x1080'
SECONDS_TO_WARM = 2
PROC_WAIT_SECONDS = 30*10
RECORD_TAIL_MARGIN = 5 # extra seconds recorded past the requested end, see record_raybig
OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve())
REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve())
UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve())
RAYBIG_UI = str(Path(BASEDIR, 'selfdrive/ui/ui.py').resolve())
RAYBIG_PREPARE_SCRIPT = str(Path(BASEDIR, 'scripts/launch_ui_raybig_desktop.sh').resolve())
WSLG_X11_DIR = '/mnt/wslg/.X11-unix'
logger = logging.getLogger('clip.py')
@@ -113,7 +120,8 @@ def parse_args(parser: ArgumentParser):
parser.error(f'failed to get route: {e}')
# FIXME: length isn't exactly max segment seconds, simplify to replay exiting at end of data
length = round(args.route.max_seg_number * 60)
# max_seg_number is a 0-based index, so the route has max_seg_number + 1 segments
length = round((args.route.max_seg_number + 1) * 60)
if args.start >= length:
parser.error(f'start ({args.start}s) cannot be after end of route ({length}s)')
if args.end > length:
@@ -123,6 +131,8 @@ def parse_args(parser: ArgumentParser):
def populate_car_params(lr: LogReader):
from openpilot.common.params import Params, UnknownKeyName
init_data = lr.first('initData')
assert init_data is not None
@@ -135,18 +145,42 @@ def populate_car_params(lr: LogReader):
except UnknownKeyName:
# forks of openpilot may have other Params keys configured. ignore these
logger.warning(f"unknown Params key '{key}', skipping")
except (TypeError, ValueError) as e:
# logged value doesn't cast to the param's type, e.g. an empty JSON param. ignore these
logger.warning(f"could not restore Params key '{key}', skipping: {e}")
logger.debug('persisted CarParams')
def validate_env(parser: ArgumentParser):
def wslg_available() -> bool:
return os.path.isdir(WSLG_X11_DIR)
def validate_env(parser: ArgumentParser, ui: Literal['c3', 'raybig']):
if platform.system() not in ['Linux']:
parser.exit(1, f'clip.py: error: {platform.system()} is not a supported operating system\n')
for proc in ['Xvfb', 'ffmpeg']:
use_wslg = ui == 'raybig' and wslg_available()
required_bins = ['ffmpeg']
if ui == 'raybig':
required_bins.append('ffprobe') # used to retime the export, see retime_to_wall_clock
if not use_wslg:
# Under WSLg, raybig renders against the existing live :0 display instead of a virtual Xvfb one.
required_bins.append('Xvfb')
for proc in required_bins:
if shutil.which(proc) is None:
parser.exit(1, f'clip.py: error: missing {proc} command, is it installed?\n')
for proc in [REPLAY, UI]:
if shutil.which(proc) is None:
parser.exit(1, f'clip.py: error: missing {proc} command, did you build openpilot yet?\n')
# REPLAY is not checked here; prepare_replay() builds it on demand
if ui == 'c3':
if shutil.which(UI) is None:
parser.exit(1, f'clip.py: error: missing {UI} command, did you build openpilot yet?\n')
elif ui == 'raybig':
if not os.path.isfile(RAYBIG_UI):
parser.exit(1, f'clip.py: error: missing {RAYBIG_UI}\n')
if not os.access(RAYBIG_PREPARE_SCRIPT, os.X_OK):
parser.exit(1, f'clip.py: error: missing or non-executable {RAYBIG_PREPARE_SCRIPT}\n')
def validate_output_file(output_file: str):
@@ -168,6 +202,8 @@ def validate_title(title: str):
def wait_for_frames(procs: list[Popen]):
from cereal.messaging import SubMaster
sm = SubMaster(['uiDebug'])
no_frames_drawn = True
while no_frames_drawn:
@@ -176,46 +212,45 @@ def wait_for_frames(procs: list[Popen]):
check_for_failure(procs)
def clip(
data_dir: str | None,
quality: Literal['low', 'high'],
prefix: str,
route: Route,
out: str,
start: int,
end: int,
speed: int,
target_mb: int,
title: str | None,
):
logger.info(f'clipping route {route.name.canonical_name}, start={start} end={end} quality={quality} target_filesize={target_mb}MB')
lr = get_logreader(route)
def wait_for_xvfb(display_num: int, xvfb_proc: Popen, timeout: float = 10.0):
# the UI and replay start immediately after, so wait for the display to actually exist
socket_path = f'/tmp/.X11-unix/X{display_num}'
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
check_for_failure([xvfb_proc])
if os.path.exists(socket_path):
return
time.sleep(0.05)
raise TimeoutError(f'Xvfb did not create {socket_path} within {timeout}s')
begin_at = max(start - SECONDS_TO_WARM, 0)
duration = end - start
bit_rate_kbps = int(round(target_mb * 8 * 1024 * 1024 / duration / 1000))
# TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision
display = f':{randint(99, 999)}'
def prepare_replay(jobs: int | None = None):
if shutil.which(REPLAY) is not None:
return
box_style = 'box=1:boxcolor=black@0.33:boxborderw=7'
meta_text = get_meta_text(lr, route)
overlays = [
# metadata overlay
f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=15:{box_style}:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'",
# route time overlay
f"drawtext=text='%{{eif\\:floor(({start}+t)/60)\\:d\\:2}}\\:%{{eif\\:mod({start}+t\\,60)\\:d\\:2}}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=24:{box_style}:x=w-text_w-38:y=38"
]
if title:
overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53")
logger.info('building replay (this can take a while the first time)...')
env = os.environ.copy()
env['SP_DISABLE_AUTO_DEVICE_SCONS'] = '1'
scons = Path(BASEDIR, '.venv/bin/scons')
cmd = [str(scons)] if os.access(scons, os.X_OK) else [sys.executable, '-m', 'SCons']
cmd += ['--extras', '-j', str(jobs or os.cpu_count() or 8), 'tools/replay/replay']
if Popen(cmd, cwd=BASEDIR, env=env).wait() != 0:
raise ChildProcessError('failed to build replay, see output above')
if speed > 1:
overlays += [
f"setpts=PTS/{speed}",
"fps=60",
]
ffmpeg_cmd = [
def prepare_raybig_runtime():
logger.info('preparing raybig host runtime (this can take a while the first time)...')
env = os.environ.copy()
env['SP_RAYBIG_COMPILE_ONLY'] = '1'
# Without this, the script's own cleanup trap restores/deletes the .so files it just built.
env['SP_KEEP_DESKTOP_RUNTIME_ARTIFACTS'] = '1'
result = Popen([RAYBIG_PREPARE_SCRIPT], env=env)
if result.wait() != 0:
raise ChildProcessError('failed to prepare raybig host runtime, see output above')
def make_ffmpeg_cmd(display: str, out: str, duration: int, bit_rate_kbps: int, overlays: list[str]):
return [
'ffmpeg', '-y',
'-video_size', RESOLUTION,
'-framerate', str(FRAMERATE),
@@ -237,39 +272,214 @@ def clip(
out,
]
replay_cmd = [REPLAY, '--ecam', '-c', '1', '-s', str(begin_at), '--prefix', prefix]
def record(procs: list[Popen], env: dict[str, str], display: str, out: str, duration: int,
bit_rate_kbps: int, overlays: list[str]):
# c3 has no frame export of its own, so screen capture its Xvfb display. raybig uses
# record_raybig instead.
logger.info('waiting for replay to begin (loading segments, may take a while)...')
wait_for_frames(procs)
logger.debug(f'letting UI warm up ({SECONDS_TO_WARM}s)...')
time.sleep(SECONDS_TO_WARM)
check_for_failure(procs)
ffmpeg_cmd = make_ffmpeg_cmd(display, out, duration, bit_rate_kbps, overlays)
with managed_proc(ffmpeg_cmd, env) as ffmpeg_proc:
all_procs = procs + [ffmpeg_proc]
logger.info(f'recording in progress ({duration}s)...')
ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS)
check_for_failure(all_procs)
logger.info(f'recording complete: {Path(out).resolve()}')
def probe_video(path: str) -> tuple[float, float]:
"""Return (duration_seconds, frame_rate) of a video file's first video stream."""
proc = Popen(['ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=r_frame_rate:format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path], stdout=PIPE, stderr=PIPE)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
raise ChildProcessError(f'ffprobe failed on {path}: {stderr.decode().strip()}')
values = stdout.decode().split()
rate_str = next(v for v in values if '/' in v)
numerator, denominator = rate_str.split('/')
frame_rate = float(numerator) / float(denominator)
duration = float(next(v for v in values if '/' not in v))
return duration, frame_rate
def retime_to_wall_clock(out: str, recorded_seconds: float, tolerance: float = 0.10):
# the UI tags the export with the fps it targeted, but the real rate depends on how fast this
# machine reads frames back from the GPU. frames are produced in lockstep with real-time
# replay, so re-tag to match wall clock. remuxing through raw h264 avoids a re-encode.
# tolerance is loose because recorded_seconds starts at the first onroad frame, a few seconds
# after the UI starts recording; this only needs to catch gross mismatches.
duration, frame_rate = probe_video(out)
if abs(duration - recorded_seconds) <= tolerance * recorded_seconds:
return
actual_frame_rate = frame_rate * duration / recorded_seconds
logger.info(f'retiming: {duration:.1f}s at {frame_rate:.2f}fps -> {recorded_seconds:.1f}s at {actual_frame_rate:.2f}fps')
out_path = Path(out)
raw = out_path.with_suffix('.retime.h264')
retimed = out_path.with_suffix('.retime.mp4')
try:
for cmd in (['ffmpeg', '-y', '-v', 'error', '-i', out, '-c', 'copy', '-f', 'h264', str(raw)],
['ffmpeg', '-y', '-v', 'error', '-r', f'{actual_frame_rate:.6f}', '-i', str(raw),
'-c', 'copy', '-movflags', '+faststart', str(retimed)]):
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
_, stderr = proc.communicate()
if proc.returncode != 0:
raise ChildProcessError(f'retime step failed: {stderr.decode().strip()}')
retimed.replace(out_path)
finally:
raw.unlink(missing_ok=True)
retimed.unlink(missing_ok=True)
def record_raybig(ui_proc: Popen, replay_proc: Popen, duration: int, out: str):
# the UI records from the moment its window opens, so the export leads with some offroad
# frames. tail margin covers replay not being exactly at `start` on the first onroad frame.
procs = [ui_proc, replay_proc]
logger.info('waiting for replay to begin (loading segments, may take a while)...')
wait_for_frames(procs)
logger.info(f'recording in progress ({duration}s)...')
started_at = time.monotonic()
time.sleep(SECONDS_TO_WARM + duration + RECORD_TAIL_MARGIN)
check_for_failure(procs)
logger.info('stopping recording...')
# SIGINT closes the window gracefully, flushing the UI's own ffmpeg pipe (close_ffmpeg() in
# system/ui/lib/application.py). SIGTERM, which managed_proc uses, has no handler and would
# skip that. close_ffmpeg() can take up to 60s, so wait longer than that before killing.
ui_proc.send_signal(signal.SIGINT)
recorded_seconds = time.monotonic() - started_at
try:
ui_proc.wait(timeout=90)
except TimeoutExpired:
logger.warning('UI did not exit cleanly after SIGINT; the recording may be truncated or corrupt')
ui_proc.kill()
ui_proc.wait()
if ui_proc.returncode not in (0, -signal.SIGINT):
logger.warning(f'UI exited with code {ui_proc.returncode} after SIGINT; the recording may be incomplete')
retime_to_wall_clock(out, recorded_seconds)
logger.info(f'recording complete: {Path(out).resolve()}')
def clip(
data_dir: str | None,
quality: Literal['low', 'high'],
prefix: str,
route: Route,
out: str,
start: int,
end: int,
speed: int,
target_mb: int,
title: str | None,
ui: Literal['c3', 'raybig'],
):
logger.info(f'clipping route {route.name.canonical_name}, start={start} end={end} quality={quality} target_filesize={target_mb}MB')
Path(out).resolve().parent.mkdir(parents=True, exist_ok=True)
lr = get_logreader(route)
begin_at = max(start - SECONDS_TO_WARM, 0)
duration = end - start
bit_rate_kbps = int(round(target_mb * 8 * 1024 * 1024 / duration / 1000))
# raybig exports its own frames (RECORD, below) rather than being screen captured, so under
# WSLg it can just render against the live desktop instead of needing its own Xvfb.
use_wslg = ui == 'raybig' and wslg_available()
if use_wslg:
display = os.environ.get('DISPLAY', ':0')
else:
# TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision
display_num = randint(99, 999)
display = f':{display_num}'
xvfb_cmd = ['Xvfb', display, '-terminate', '-screen', '0', f'{RESOLUTION}x{PIXEL_DEPTH}']
box_style = 'box=1:boxcolor=black@0.33:boxborderw=7'
meta_text = get_meta_text(lr, route)
overlays = [
# metadata overlay
f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=15:{box_style}:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'",
# route time overlay
f"drawtext=text='%{{eif\\:floor(({start}+t)/60)\\:d\\:2}}\\:%{{eif\\:mod({start}+t\\,60)\\:d\\:2}}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=24:{box_style}:x=w-text_w-38:y=38"
]
if title:
overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53")
if speed > 1:
overlays += [
f"setpts=PTS/{speed}",
"fps=60",
]
# cache enough of the ~60s segments to cover the clip, else replay runs dry partway through
# and loops back on itself, repeating earlier footage
segments_to_cache = math.ceil((end - begin_at) / 60) + 1
replay_cmd = [REPLAY, '--ecam', '-c', str(segments_to_cache), '-s', str(begin_at), '--prefix', prefix]
if data_dir:
replay_cmd.extend(['--data_dir', data_dir])
if quality == 'low':
replay_cmd.append('--qcam')
replay_cmd.append(route.name.canonical_name)
ui_cmd = [UI, '-platform', 'xcb']
xvfb_cmd = ['Xvfb', display, '-terminate', '-screen', '0', f'{RESOLUTION}x{PIXEL_DEPTH}']
prepare_replay()
if ui == 'raybig':
prepare_raybig_runtime()
ui_cmd = [sys.executable, RAYBIG_UI]
else:
ui_cmd = [UI, '-platform', 'xcb']
# imported here, after prepare_raybig_runtime() has built the native extensions it needs
from openpilot.common.prefix import OpenpilotPrefix
with OpenpilotPrefix(prefix, shared_download_cache=True):
populate_car_params(lr)
env = os.environ.copy()
env['DISPLAY'] = display
if ui == 'raybig':
env['BIG'] = '1'
env.setdefault('PRIME_TYPE', '0')
# use the real replayed drive stats instead of raybig's desktop fake-data demo
env['SP_RAYBIG_FAKE_DRIVE_STATS'] = '0'
pythonpath_extra = f"{BASEDIR}{os.pathsep}{Path(BASEDIR, 'starpilot/third_party')}"
env['PYTHONPATH'] = f"{pythonpath_extra}{os.pathsep}{env['PYTHONPATH']}" if env.get('PYTHONPATH') else pythonpath_extra
# the raylib UI pipes its own frames to ffmpeg (system/ui/lib/application.py). no drawtext
# overlays on this path, unlike c3.
env['RECORD'] = '1'
env['RECORD_OUTPUT'] = out
env['RECORD_BITRATE'] = f'{bit_rate_kbps}k'
if speed > 1:
env['RECORD_SPEED'] = str(speed)
# the UI defaults to 60fps and tags the export as such, but the per-frame GPU readback
# can't sustain that and the clip plays fast. ask for a rate it can actually hit.
env['FPS'] = str(FRAMERATE)
with managed_proc(xvfb_cmd, env) as xvfb_proc, managed_proc(ui_cmd, env) as ui_proc, managed_proc(replay_cmd, env) as replay_proc:
procs = [xvfb_proc, ui_proc, replay_proc]
logger.info('waiting for replay to begin (loading segments, may take a while)...')
wait_for_frames(procs)
logger.debug(f'letting UI warm up ({SECONDS_TO_WARM}s)...')
time.sleep(SECONDS_TO_WARM)
check_for_failure(procs)
with managed_proc(ffmpeg_cmd, env) as ffmpeg_proc:
procs.append(ffmpeg_proc)
logger.info(f'recording in progress ({duration}s)...')
ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS)
check_for_failure(procs)
logger.info(f'recording complete: {Path(out).resolve()}')
if use_wslg:
logger.info('WSLg detected: rendering against the live desktop display.')
with managed_proc(ui_cmd, env) as ui_proc, managed_proc(replay_cmd, env) as replay_proc:
record_raybig(ui_proc, replay_proc, duration, out)
else:
with managed_proc(xvfb_cmd, env) as xvfb_proc:
wait_for_xvfb(display_num, xvfb_proc)
with managed_proc(ui_cmd, env) as ui_proc, managed_proc(replay_cmd, env) as replay_proc:
record_raybig(ui_proc, replay_proc, duration, out)
else:
with managed_proc(xvfb_cmd, env) as xvfb_proc:
wait_for_xvfb(display_num, xvfb_proc)
with managed_proc(ui_cmd, env) as ui_proc, managed_proc(replay_cmd, env) as replay_proc:
record([xvfb_proc, ui_proc, replay_proc], env, display, out, duration, bit_rate_kbps, overlays)
def main():
p = ArgumentParser(prog='clip.py', description='clip your openpilot route.', epilog='comma.ai')
validate_env(p)
route_group = p.add_mutually_exclusive_group(required=True)
route_group.add_argument('route', nargs='?', type=validate_route, help=f'The route (e.g. {DEMO_ROUTE} or {DEMO_ROUTE}/{DEMO_START}/{DEMO_END})')
route_group.add_argument('--demo', help='use the demo route', action='store_true')
@@ -282,7 +492,11 @@ def main():
p.add_argument('-x', '--speed', help='record the clip at this speed multiple', type=int, default=1)
p.add_argument('-s', '--start', help='start clipping at <start> seconds', type=int)
p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title)
p.add_argument('-u', '--ui', help='desktop UI to record. raybig exports its own frames, so it also works where screen capture '
'cannot see the window (e.g. WSLg), but gets no title/metadata overlays',
choices=['c3', 'raybig'], default='c3')
args = parse_args(p)
validate_env(p, args.ui)
exit_code = 1
try:
clip(
@@ -296,6 +510,7 @@ def main():
speed=args.speed,
target_mb=args.file_size,
title=args.title,
ui=args.ui,
)
exit_code = 0
except KeyboardInterrupt as e: