# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2026 L8OLOOE and contributors. See CREDITS.md and NOTICE.md. """Bounded live OCC access units, parameter-set priming and owned recording. An existing growing recording is reused and never stopped by this owner. Only SPS/PPS are read from history; historical pictures cannot become live poses. """ import queue import re import shutil import subprocess import threading import time import os from pathlib import Path from .live_decode import LiveGrayDecoder try: from ..radio.frame_occ_live import GrowingOCC,timestamped_camera_packets except ImportError: from radio.frame_occ_live import GrowingOCC,timestamped_camera_packets def parameter_sets(access_unit): parts=re.split(b'\x00\x00\x00?\x01',access_unit) return b''.join(b'\0\0\0\1'+part for part in parts if part and part[0]&31 in (7,8)) class LiveCameraSource: def __init__(self,dataset_root,*,disk_budget=2*1024**3,vrcmd='/opt/steamvr/bin/linuxarm64/vrcmd'): self.root=Path(dataset_root);self.disk_budget=disk_budget;self.vrcmd=vrcmd self.owned=False;self.created=False;self.reader=None;self.decoders={};self.parameters={} self.metadata=queue.Queue(maxsize=32);self.frames=queue.Queue(maxsize=8) self.failure=None;self.stop=threading.Event();self.thread=None self.start_size=0;self.path=None;self.dropped=0 def _command(self,command): argv=[self.vrcmd,'--cmd','0',command] if hasattr(os,'geteuid') and os.geteuid()==0:argv=['runuser','-u','steamos','--',*argv] result=subprocess.run(argv,capture_output=True,text=True,timeout=8) if result.returncode or 'response: ok' not in result.stdout: raise RuntimeError('Camera recording control failed: '+result.stdout+result.stderr) def start(self): if self.thread is not None:raise RuntimeError('Camera source already started') paths=list(self.root.glob('*.dataset/*.occ')) sizes={path:path.stat().st_size for path in paths} time.sleep(.2) growing=[path for path,size in sizes.items() if path.exists() and path.stat().st_size>size] if growing:self.path=max(growing,key=lambda path:path.stat().st_mtime_ns) else: if shutil.disk_usage(self.root).freesizes[p]] if changed: self.path=max(changed,key=lambda path:path.stat().st_mtime_ns) # Only a dataset that did not exist before our start command is ours to delete. self.created=self.path not in sizes;break time.sleep(.05) if self.path is None: self.close();raise RuntimeError('XRService did not produce a growing recording') try: # Prime at most sixteen MiB. No history pixels are decoded. prime=GrowingOCC(self.path) for kind,record in prime.iter_records(max_records=512,max_bytes=16*1024**2): for metadata,au in timestamped_camera_packets(kind,record): key=tuple(metadata['stream_key']) if key[0]!=38:continue params=parameter_sets(au) if params:self.parameters[key]=params if len(self.parameters)>=4:break self.reader=GrowingOCC(self.path);self.reader.seek_complete_tail() self.start_size=self.path.stat().st_size self.thread=threading.Thread(target=self._follow,daemon=True);self.thread.start() return self.path.parent except BaseException:self.close();raise def _latest(self,target,value): try:target.put_nowait(value) except queue.Full: try:target.get_nowait() except queue.Empty:pass target.put_nowait(value);self.dropped+=1 def _follow(self): last_growth=time.monotonic() try: while not self.stop.is_set(): if self.path.stat().st_size-self.start_size>self.disk_budget: raise RuntimeError('Live OCC growth exceeded recording budget') previous=self.reader.offset for kind,record in self.reader.iter_records(max_records=128,max_bytes=16*1024**2): for metadata,au in timestamped_camera_packets(kind,record): key=tuple(metadata['stream_key']) if key[0]!=38:continue self._latest(self.metadata,metadata) if key not in self.decoders:self.decoders[key]=LiveGrayDecoder(key,timestamped=True) params=parameter_sets(au) if params:self.parameters[key]=params self.decoders[key].feed(self.parameters.get(key,b'')+au,metadata) if self.reader.offset>previous:last_growth=time.monotonic() elif time.monotonic()-last_growth>2:raise RuntimeError('Live camera recording stopped growing') for decoder in self.decoders.values(): if decoder.failure is not None:raise RuntimeError('Live camera decoder failed') from decoder.failure while not decoder.frames.empty():self._latest(self.frames,decoder.frames.get_nowait()) self.stop.wait(.005) except (OSError,ValueError,RuntimeError) as error:self.failure=error def close(self): self.stop.set() try: if self.thread:self.thread.join(timeout=3) for decoder in self.decoders.values():decoder.close() finally: if self.owned: try:self._command('stop_tracking_recording') finally:self.owned=False;self._remove_created() def _remove_created(self): # Each owned recording grows ~2.4GB per restart cycle; on 2026-10-07 # undeleted ones filled /home to 97%. Remove only our own dataset. dataset=None if self.path is None else self.path.parent if (self.created and dataset is not None and dataset.parent==self.root and re.fullmatch(r'Deckard_[0-9_-]+\.dataset',dataset.name)): # XRService writes snapshots/metrics into the dataset after the stop # command (seen 2026-10-07): delete again until it stays gone. for _ in range(4): shutil.rmtree(dataset,ignore_errors=True) time.sleep(.75) if not dataset.exists():break self.created=False