import argparse from pathlib import Path import sys import time ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from localcast.sensor_fusion.spout_output import ( CultCacheRenderFrameSource, OpenGLSpoutPointRenderer, RenderFrameFileSource, SpoutOutputConfig, write_cult_status, write_status, ) def main() -> None: parser = argparse.ArgumentParser(description="Publish render-frame packets as a named Spout texture for OBS.") parser.add_argument("--frame-json", default=str(ROOT / "calibration" / "runs" / "synthetic-render-frame.json")) parser.add_argument("--frame-cache", default=str(ROOT / "calibration" / "runs" / "visual-state.msgpack")) parser.add_argument("--sender-name", default="LocalCastBridge Point Cloud") parser.add_argument("--width", type=int, default=1920) parser.add_argument("--height", type=int, default=1080) parser.add_argument("--fps", type=float, default=60.0) parser.add_argument("--point-scale", type=float, default=1.0) parser.add_argument("--demo-if-missing", action="store_true") parser.add_argument("--demo-point-count", type=int, default=4096) parser.add_argument("--status", default=str(ROOT / "calibration" / "runs" / "stream-spout-status.json")) parser.add_argument("--status-cache", default=str(ROOT / "calibration" / "runs" / "visual-stream-status.msgpack")) parser.add_argument("--source", choices=["cultcache", "json"], default="cultcache") parser.add_argument("--duration", type=float, help="Optional run duration in seconds for smoke tests.") args = parser.parse_args() frame_path = Path(args.frame_json) frame_cache_path = Path(args.frame_cache) status_path = Path(args.status) status_cache_path = Path(args.status_cache) config = SpoutOutputConfig( sender_name=args.sender_name, width=args.width, height=args.height, fps=args.fps, point_scale=args.point_scale, demo_point_count=args.demo_point_count, ) if args.source == "cultcache": source = CultCacheRenderFrameSource(frame_cache_path, demo_if_missing=args.demo_if_missing, demo_point_count=args.demo_point_count) frame_source_name = str(frame_cache_path) else: source = RenderFrameFileSource(frame_path, demo_if_missing=args.demo_if_missing, demo_point_count=args.demo_point_count) frame_source_name = str(frame_path) frame_interval = 1.0 / max(1.0, args.fps) start = time.monotonic() last_status = 0.0 with OpenGLSpoutPointRenderer(config) as renderer: while True: now = time.monotonic() if args.duration is not None and now - start >= args.duration: break frame = source.current_frame(now, config) ok = renderer.render(frame) if now - last_status >= 1.0: write_status( status_path, sender_name=config.sender_name, frames_sent=renderer.frames_sent, point_count=len(frame.points), frame_path=Path(frame_source_name), last_error=None if ok else "SpoutGL sendTexture returned false", ) write_cult_status( status_cache_path, sender_name=config.sender_name, frames_sent=renderer.frames_sent, point_count=len(frame.points), frame_source=frame_source_name, last_error=None if ok else "SpoutGL sendTexture returned false", ) last_status = now sleep_for = frame_interval - (time.monotonic() - now) if sleep_for > 0: time.sleep(sleep_for) write_status( status_path, sender_name=config.sender_name, frames_sent=renderer.frames_sent, point_count=0, frame_path=Path(frame_source_name), last_error="stopped", ) write_cult_status( status_cache_path, sender_name=config.sender_name, frames_sent=renderer.frames_sent, point_count=0, frame_source=frame_source_name, last_error="stopped", ) if __name__ == "__main__": main()