Capture saved replay screens only for explicit mirror mode

This commit is contained in:
firestar5683
2026-09-20 12:54:23 -07:00
parent 054d204d4f
commit 87c871ca07
6 changed files with 117 additions and 13 deletions
+10 -5
View File
@@ -52,6 +52,7 @@ class DemoSession:
pid = owner.get('pid')
running = type(pid) is int and pid > 1 and self.ticks(pid) == owner.get('start_ticks') and owner.get('start_ticks') is not None
result = {key: owner.get(key) for key in ('request_id', 'alias', 'route', 'pid', 'out', 'muted')}
result['screen_mirror'] = owner.get('screen_mirror') is True
result.update(running=bool(running), generation_invoked=False)
if owner.get('out'):
out = Path(owner['out'])
@@ -65,17 +66,20 @@ class DemoSession:
return result
def start(self, data):
if (not isinstance(data, dict) or set(data) != {'alias', 'request_id', 'muted'}
required = {'alias', 'request_id', 'muted'}
if (not isinstance(data, dict) or not required <= set(data) or set(data) - required - {'screen_mirror'}
or not isinstance(data['request_id'], str) or not re.fullmatch(r'[a-zA-Z0-9-]{8,80}', data['request_id'])
or type(data['muted']) is not bool):
raise ValueError('Saved demo requires an alias, request ID and explicit output choice')
or type(data['muted']) is not bool or type(data.get('screen_mirror', False)) is not bool):
raise ValueError('Saved demo requires an alias, request ID, explicit output choice and optional screen_mirror boolean')
data = {**data, 'screen_mirror': data.get('screen_mirror', False)}
if not self.offroad():
raise ValueError('Saved demo playback requires the vehicle to be offroad')
selected = entry(self.root, data['alias'])
with self.lock:
current = self.status()
if current['running']:
if current['request_id'] == data['request_id'] and current['alias'] == data['alias'] and current['muted'] == data['muted']:
if (current['request_id'] == data['request_id'] and current['alias'] == data['alias']
and current['muted'] == data['muted'] and current['screen_mirror'] == data['screen_mirror']):
return current
raise ValueError('A saved demo is already running; stop that owned session first')
if read(self.owner_path).get('request_id') == data['request_id']:
@@ -91,8 +95,9 @@ class DemoSession:
'--score-archive', selected['archive']]
if selected.get('curve_plan'): command += ['--curve-plan', selected['curve_plan']]
if data['muted']: command.append('--muted')
if data['screen_mirror']: command.append('--screen-mirror')
env = os.environ.copy()
for key in ('ZMQ', 'OPENPILOT_PREFIX', 'OPENPILOT_ZMQ_NAMESPACE', 'PARAMS_ROOT'):
for key in ('ZMQ', 'OPENPILOT_PREFIX', 'OPENPILOT_ZMQ_NAMESPACE', 'PARAMS_ROOT', 'ROADSCORE_MIRROR_DIR'):
env.pop(key, None)
env['PYTHONPATH'] = ':'.join((str(self.root/'prototype'), '/data/openpilot', '/data/roadscore-feasibility/venv/lib/python3.12/site-packages'))
with (out/'launcher.log').open('wb') as log:
@@ -67,14 +67,14 @@ def start_deadline(path, session, now):
except (OSError, ValueError):return None
def native_environment(project, root, out, session, inherited=None):
def native_environment(project, root, out, session, inherited=None, *, screen_mirror=False):
env = dict(os.environ if inherited is None else inherited)
for key in ('ZMQ','PARAMS_ROOT','OPENPILOT_ZMQ_NAMESPACE','ROADSCORE_PCM_RETURN','ROADSCORE_RESIDENT','ROADSCORE_GENERATION_SEED','ROADSCORE_SEED_ORIGIN'):
for key in ('ZMQ','PARAMS_ROOT','OPENPILOT_ZMQ_NAMESPACE','ROADSCORE_PCM_RETURN','ROADSCORE_RESIDENT','ROADSCORE_GENERATION_SEED','ROADSCORE_SEED_ORIGIN','ROADSCORE_MIRROR_DIR'):
env.pop(key, None)
env.update(PYTHONDONTWRITEBYTECODE='1',OPENPILOT_PREFIX='roadscore_replay',BASEDIR=str(project),PWD=str(project),NOBOARD='1',SIMULATION='1',SKIP_FW_QUERY='1',BIG='0',ROADSCORE_PREPARED_SHOWCASE='1',ROADSCORE_SHOWCASE_SESSION=session,ROADSCORE_REPLAY_UI_CONTROLS='1',ROADSCORE_CLEAN_DEMO_UI='1',ROADSCORE_OVERLAY='1',ROADSCORE_STATUS_FILE=str(out/'status.json'),ROADSCORE_UI_AUDIT=str(out/'ui_audit.jsonl'),ROADSCORE_OVERLAY_CAPTURE=str(out/'overlay.png'),ROADSCORE_AUDIO_DRAIN_FILE=str(out/'audio_drained.json'),ROADSCORE_PRESENTATION_POLICY='conservative-v4',SP_ALLOW_DESKTOP_FAKE_WIFI='0',SP_ALLOW_DESKTOP_FAKE_BLUETOOTH='0',SP_ONROAD_NAV_DEMO='0',SP_ONROAD_CEM_DEMO='0')
paths=[HERE,project,project/'starpilot/third_party',*project.glob('*_repo'),Path('/data/roadscore-feasibility/venv/lib/python3.12/site-packages')]
env['PYTHONPATH']=':'.join(map(str,paths))
env['ROADSCORE_MIRROR_DIR']=str(out/'mirror')
if screen_mirror:env['ROADSCORE_MIRROR_DIR']=str(out/'mirror')
env['ROADSCORE_REPLAY_PRIME']='1'
return env
@@ -132,6 +132,7 @@ def main():
p.add_argument('--project-root',type=Path,default=Path('/data/openpilot'))
p.add_argument('--score-archive',type=Path);p.add_argument('--curve-plan',type=Path)
p.add_argument('--out',type=Path);p.add_argument('--muted',action='store_true');p.add_argument('--headless',action='store_true')
p.add_argument('--screen-mirror',action='store_true',help='Capture the native UI for the legacy screen-mirror viewer')
p.add_argument('--check',action='store_true');p.add_argument('--hold-start',action='store_true')
p.add_argument('--start-timeout',type=float,default=120.);p.add_argument('--duration',type=float,default=float('inf'))
a=p.parse_args()
@@ -163,7 +164,7 @@ def main():
for path in (py,replay,project/'selfdrive/ui/ui.py',HERE/'mac_showcase.py'):
if not path.is_file():raise FileNotFoundError(path)
args=replay_arguments(meta,local)
checked=dict(route=route,archive=str(archive),seconds=duration,local_cache=str(local),generation_invoked=False,requires_chestnut=False,replay_args=args)
checked=dict(route=route,archive=str(archive),seconds=duration,local_cache=str(local),generation_invoked=False,requires_chestnut=False,replay_args=args,screen_mirror=a.screen_mirror)
if a.check:
print(json.dumps(checked,indent=2));return
from native_ownership import verify_offroad,DisplayOwner
@@ -187,7 +188,7 @@ def main():
if any(path.name!='launcher.log' for path in out.iterdir()):raise ValueError('Prepared output directory must contain only its supervisor launcher.log')
else:out.mkdir(parents=True)
previous=install_current(root,out,session)
env=native_environment(project,root,out,session)
env=native_environment(project,root,out,session,screen_mirror=a.screen_mirror)
env['ROADSCORE_FORCE_MUTE']='1' if muted else '0'
Path('/dev/shm/msgq_roadscore_replay').mkdir(exist_ok=True)
launch={**checked,'mode':'prepared-interactive-showcase','session_id':session,'muted':muted,'core_sha256':hashlib.sha256((archive/'dry.wav').read_bytes()).hexdigest(),'previous_current':str(previous) if previous else None,'curve_plan':str(a.curve_plan) if a.curve_plan else None}
+4 -2
View File
@@ -114,7 +114,7 @@ def mirror_main():
signal.signal(signal.SIGTERM,interrupt)
try:
launched=True
peer.call('demo_start',{'alias':args.route,'request_id':request_id,'muted':args.muted})
peer.call('demo_start',{'alias':args.route,'request_id':request_id,'muted':args.muted,'screen_mirror':True})
url=f'http://127.0.0.1:{server.server_port}'
print('RoadScore saved demo. No model generation. Controls and mirrored screen: '+url,flush=True)
if not args.no_browser:subprocess.Popen(['/usr/bin/open',url],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
@@ -231,8 +231,10 @@ def native_pair_main():
try:
launched=True
peer.call('demo_start',{'alias':args.route,'request_id':request_id,'muted':args.muted})
env=os.environ.copy()
env.pop('ROADSCORE_MIRROR_DIR',None)
with (out/'mac.log').open('wb') as log:
process=subprocess.Popen(command,cwd=project,stdin=subprocess.DEVNULL,stdout=log,
process=subprocess.Popen(command,cwd=project,env=env,stdin=subprocess.DEVNULL,stdout=log,
stderr=subprocess.STDOUT,start_new_session=True,close_fds=True)
print('Preparing saved RoadScore on comma and native Mac UI. No model generation.',flush=True)
deadline=time.monotonic()+100
+29
View File
@@ -4,6 +4,7 @@ from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from demo_catalog import entry
from demo_session import DemoSession, write
@@ -45,12 +46,40 @@ class SavedDemoTests(unittest.TestCase):
command,options=self.calls[0]
self.assertTrue(command[1].endswith('/native_prepared_showcase.py'))
self.assertIn('--hold-start',command);self.assertIn('--muted',command)
self.assertNotIn('--screen-mirror',command)
self.assertFalse(current['screen_mirror'])
self.assertTrue(options['close_fds']);self.assertTrue(options['start_new_session'])
with self.assertRaises(ValueError):self.service.start({**self.request,'request_id':'different-request'})
with self.assertRaises(ValueError):self.service.stop({'request_id':'different-request'})
self.service.stop({'request_id':self.request['request_id']})
self.assertEqual(self.stops,[123])
def test_capture_is_explicit_and_part_of_launch_identity(self):
with patch.dict('os.environ',{'ROADSCORE_MIRROR_DIR':'/inherited/mirror'}):
current=self.service.start({**self.request,'screen_mirror':True})
self.assertTrue(current['screen_mirror'])
command,options=self.calls[0]
self.assertIn('--screen-mirror',command)
self.assertNotIn('ROADSCORE_MIRROR_DIR',options['env'])
self.assertEqual(self.service.start({**self.request,'screen_mirror':True})['pid'],123)
with self.assertRaises(ValueError):self.service.start(self.request)
self.assertEqual(len(self.calls),1)
def test_optional_capture_defaults_to_false_without_inherited_activation(self):
with patch.dict('os.environ',{'ROADSCORE_MIRROR_DIR':'/inherited/mirror'}):
current=self.service.start(self.request)
self.assertFalse(current['screen_mirror'])
self.assertEqual(self.service.start({**self.request,'screen_mirror':False})['pid'],123)
self.assertNotIn('ROADSCORE_MIRROR_DIR',self.calls[0][1]['env'])
with self.assertRaises(ValueError):self.service.start({**self.request,'screen_mirror':True})
def test_capture_api_rejects_nonbooleans_and_unknown_fields_before_spawning(self):
for value in (None,0,1,'true','false',[],{}):
with self.subTest(value=value),self.assertRaises(ValueError):
self.service.start({**self.request,'screen_mirror':value})
with self.assertRaises(ValueError):self.service.start({**self.request,'mirror':True})
self.assertEqual(self.calls,[])
def test_matching_ready_session_is_required_for_release(self):
current=self.service.start(self.request);out=Path(current['out'])
request={'request_id':self.request['request_id'],'session_id':'prepared-session'}
@@ -63,6 +63,19 @@ class NativePreparedTests(unittest.TestCase):
self.assertIn('--presentation-root',args);self.assertIn('bluealsa:AA:BB',args)
self.assertTrue(args[1].endswith('/mac_showcase.py'))
self.assertFalse(any('ace_worker' in item or 'power_worker' in item for item in args))
def test_continuous_capture_is_absent_even_with_inherited_mirror_directory(self):
from screen_mirror import ScreenMirror
inherited={'ROADSCORE_MIRROR_DIR':'/old/session/mirror'}
for choice in ({}, {'screen_mirror':False}):
env=native_environment(Path(self.tmp.name),self.root,self.out,'session-123',inherited,**choice)
self.assertNotIn('ROADSCORE_MIRROR_DIR',env)
self.assertIsNone(ScreenMirror.from_environ(env))
self.assertEqual(inherited['ROADSCORE_MIRROR_DIR'],'/old/session/mirror')
def test_explicit_legacy_mirror_uses_only_this_owned_output(self):
env=native_environment(Path(self.tmp.name),self.root,self.out,'session-123',
{'ROADSCORE_MIRROR_DIR':'/old/session/mirror'},screen_mirror=True)
self.assertEqual(env['ROADSCORE_MIRROR_DIR'],str(self.out/'mirror'))
self.assertEqual(env['ROADSCORE_REPLAY_PRIME'],'1')
def test_native_replay_uses_only_given_local_cache(self):
meta={'native_replay_args':['fixture','--data_dir','old','--start','149']}
args=replay_arguments(meta,Path('/local/playback'))
+55 -1
View File
@@ -3,10 +3,11 @@ from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from paired_showcase import DemoPeer, control_server, local_start_deadline, mac_command, read_ready
from paired_showcase import DemoPeer, control_server, local_start_deadline, mac_command, read_ready, mirror_main, native_pair_main
class NativePairTests(unittest.TestCase):
@@ -48,6 +49,34 @@ class NativePairTests(unittest.TestCase):
'http://192.168.1.2:8082',Path('/owned/mac'),12.,fullscreen=True)
self.assertIn('--fullscreen',command)
def test_normal_pair_never_requests_or_inherits_screen_capture(self):
calls=[]
def call(action,data=None):
calls.append((action,data))
if action=='status':return {'offroad':True}
if action=='demo_ready':return dict(request_id='paired-test-request',route='private/route',running=True,prepared=True,ready_session_id='native-session')
if action=='demo_play':return dict(request_id='paired-test-request',session_id='native-session',start_at_wall=101.,server_wall=100.)
return {}
peer=SimpleNamespace(call=call,peer=SimpleNamespace(base_url='http://192.168.8.156:8082'))
process=Mock();process.poll.return_value=None
def local_ready(path,route):
path.parent.mkdir(exist_ok=True)
return {'session_id':'mac-session'}
with tempfile.TemporaryDirectory() as folder:
root=Path(folder)/'roadscore';(root/'assets').mkdir(parents=True);(root/'prototype').mkdir()
(root/'assets/demo_catalog.json').write_text(json.dumps({'paired_comma':peer.peer.base_url}))
with patch('paired_showcase.HERE',root/'prototype'),patch('paired_showcase.DemoPeer',return_value=peer), \
patch('paired_showcase.signal.signal'),patch('paired_showcase.subprocess.Popen',return_value=process) as spawn, \
patch('paired_showcase.stop_mac'),patch('paired_showcase.read_ready',side_effect=local_ready), \
patch('paired_showcase.uuid.uuid4',return_value=SimpleNamespace(hex='paired-test-request')), \
patch('demo_catalog.entry',return_value={'route':'private/route','archive':'/protected/core'}), \
patch('mac_replay_ownership.preflight'),patch.dict('os.environ',{'ROADSCORE_MIRROR_DIR':'/inherited/mirror'}), \
patch('sys.argv',['paired_showcase','--muted','--duration','0.000000001']):
native_pair_main()
self.assertNotIn('ROADSCORE_MIRROR_DIR',spawn.call_args.kwargs['env'])
payload=next(data for action,data in calls if action=='demo_start')
self.assertNotIn('screen_mirror',payload)
class MirrorControlTests(unittest.TestCase):
def setUp(self):
@@ -84,4 +113,29 @@ class MirrorControlTests(unittest.TestCase):
with self.assertRaises(ValueError):DemoPeer('http://unrelated.example:8082')
class LegacyMirrorLaunchTests(unittest.TestCase):
def test_legacy_viewer_explicitly_requests_native_capture(self):
calls=[]
def call(action,data=None):
calls.append((action,data))
if action=='status':return {'offroad':True}
if action=='demo_ready':return {'request_id':'mirror-test-request','running':True,'prepared':True,'ready_session_id':'native-session'}
return {}
peer=SimpleNamespace(call=call)
server=Mock(server_port=56976)
with tempfile.TemporaryDirectory() as folder:
root=Path(folder);(root/'assets').mkdir();(root/'prototype').mkdir()
(root/'assets/demo_catalog.json').write_text(json.dumps({'paired_comma':'http://192.168.8.156:8082'}))
with patch('paired_showcase.HERE',root/'prototype'),patch('paired_showcase.DemoPeer',return_value=peer), \
patch('paired_showcase.control_server',return_value=server),patch('paired_showcase.signal.signal'), \
patch('paired_showcase.uuid.uuid4',return_value=SimpleNamespace(hex='mirror-test-request')), \
patch('demo_catalog.entry',return_value={'route':'private/route'}), \
patch('sys.argv',['paired_showcase','--screen-mirror','--no-browser','--muted','--duration','0']):
mirror_main()
payload=next(data for action,data in calls if action=='demo_start')
self.assertEqual(payload,{'alias':'route1','request_id':'mirror-test-request','muted':True,'screen_mirror':True})
self.assertIn(('demo_stop',{'request_id':'mirror-test-request'}),calls)
server.shutdown.assert_called_once();server.server_close.assert_called_once()
if __name__=='__main__':unittest.main()