From 510597be535b2fedf3c077208548497c993ed9f7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 1 May 2026 22:07:55 -0400 Subject: [PATCH 1/8] Update SimDeck agent instructions --- README.md | 11 ++- cli/XCWH264Encoder.m | 4 +- client/src/app/AppShell.tsx | 18 +++- client/src/features/stream/streamTypes.ts | 1 + .../src/features/stream/streamWorkerClient.ts | 93 ++++++++++++++++--- client/src/features/stream/useLiveStream.ts | 6 +- docs/cli/commands.md | 16 ++++ docs/cli/flags.md | 3 + docs/guide/video.md | 4 +- scripts/studio-provider-bridge.mjs | 20 ++-- server/src/api/routes.rs | 18 ++-- server/src/main.rs | 49 +++++++--- skills/simdeck/SKILL.md | 13 +-- 13 files changed, 200 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index e3e8ffb..f4b02f0 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,12 @@ simdeck studio expose "iPhone 17 Pro" The command starts or reuses the local daemon, creates an ephemeral Studio session, prints a unique `https://simdeck.djdev.me/simulator/...` URL, and keeps -the outbound bridge alive until you press Ctrl-C. It uses `auto` mode by default, -letting VideoToolbox choose the encoder. Pass `--video-codec software` when you -need to force software encoding; Studio then defaults to the `ci-software` -stream quality profile (`960` longest edge at `24` fps). Use -`--stream-quality quality|balanced|smooth|economy|ci-software` to override it. +the outbound bridge alive until you press Ctrl-C. It uses software H.264 by +default with realtime stream settings for remote viewing, and prints the active +codec/profile when it starts. Studio defaults to the `smooth` stream quality +profile (`1170` longest edge, dynamic up to `60` fps). Use +`--stream-quality quality|balanced|smooth|economy|ci-software` to override it, +or pass `--video-codec hardware` when a dedicated hardware encoder is preferable. CLI commands automatically use the same warm daemon: diff --git a/cli/XCWH264Encoder.m b/cli/XCWH264Encoder.m index cebc68d..265157a 100644 --- a/cli/XCWH264Encoder.m +++ b/cli/XCWH264Encoder.m @@ -933,8 +933,8 @@ - (BOOL)ensureCompressionSessionWithWidth:(int32_t)width height:(int32_t)height } VTSessionSetProperty(session, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(expectedFrameRate)); BOOL shortKeyframeInterval = _lowLatencyMode || _realtimeStreamMode; - VTSessionSetProperty(session, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(shortKeyframeInterval ? expectedFrameRate : expectedFrameRate * 2)); - VTSessionSetProperty(session, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(shortKeyframeInterval ? 1.0 : 2.0)); + VTSessionSetProperty(session, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(shortKeyframeInterval ? MAX(1, expectedFrameRate / 2) : expectedFrameRate * 2)); + VTSessionSetProperty(session, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(shortKeyframeInterval ? 0.5 : 2.0)); VTSessionSetProperty(session, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(averageBitRate)); if (_realtimeStreamMode) { NSArray *dataRateLimits = @[ diff --git a/client/src/app/AppShell.tsx b/client/src/app/AppShell.tsx index 395b678..237c226 100644 --- a/client/src/app/AppShell.tsx +++ b/client/src/app/AppShell.tsx @@ -111,6 +111,15 @@ function buildAuthenticatedAssetUrl(path: string, stamp: number): string { return url.toString(); } +function shouldUseRemoteStreamDefault(apiRoot: string): boolean { + if (apiRoot) { + return true; + } + return ( + new URLSearchParams(window.location.search).get("remoteStream") === "1" + ); +} + function shouldRenderNativeChrome(simulator: SimulatorMetadata): boolean { const identifier = simulator.deviceTypeIdentifier ?? ""; const name = simulator.name ?? ""; @@ -177,6 +186,7 @@ export interface AppShellProps { fixedSimulatorUDID?: string | null; hideSimulatorSelection?: boolean; pairingEnabled?: boolean; + remoteStream?: boolean; } export function AppShell({ @@ -184,6 +194,7 @@ export function AppShell({ fixedSimulatorUDID = null, hideSimulatorSelection = false, pairingEnabled = true, + remoteStream = shouldUseRemoteStreamDefault(apiRoot), }: AppShellProps = {}) { configureSimDeckClient({ apiRoot }); const [initialUiState] = useState(readPersistedUiState); @@ -370,6 +381,7 @@ export function AppShell({ streamCanvasKey, } = useLiveStream({ canvasElement: streamCanvasElement, + remote: remoteStream, simulator: selectedSimulator, }); @@ -843,9 +855,13 @@ export function AppShell({ pairingEnabled && listError === AUTH_REQUIRED_MESSAGE && !accessTokenFromLocation(); + const visibleListError = + remoteStream && hasFrame && listError === "Failed to fetch" + ? "" + : listError; const error = pairingRequired ? localError || streamError - : localError || streamError || listError; + : localError || streamError || visibleListError; const deviceTransform = `translate(${pan.x}px, ${pan.y + autoViewportOffsetY}px) scale(${effectiveZoom})`; const chromeScreenRect = computeChromeScreenRect( viewportChromeProfile, diff --git a/client/src/features/stream/streamTypes.ts b/client/src/features/stream/streamTypes.ts index b08e1a9..5ed03b7 100644 --- a/client/src/features/stream/streamTypes.ts +++ b/client/src/features/stream/streamTypes.ts @@ -1,6 +1,7 @@ import type { Size } from "../viewport/types"; export interface StreamConnectTarget { + remote?: boolean; udid: string; } diff --git a/client/src/features/stream/streamWorkerClient.ts b/client/src/features/stream/streamWorkerClient.ts index 2b420df..011e93f 100644 --- a/client/src/features/stream/streamWorkerClient.ts +++ b/client/src/features/stream/streamWorkerClient.ts @@ -12,6 +12,7 @@ const HAVE_CURRENT_DATA = 2; const WEBRTC_CONTROL_CHANNEL_LABEL = "simdeck-control"; const WEBRTC_FIRST_FRAME_TIMEOUT_MS = 10000; const WEBRTC_STALLED_FRAME_TIMEOUT_MS = 8000; +const WEBRTC_REMOTE_DISCONNECTED_GRACE_MS = 3000; let activeWebRtcControlChannel: RTCDataChannel | null = null; let activeStreamClient: StreamWorkerClient | null = null; @@ -26,8 +27,11 @@ export function sendWebRtcControlMessage(encoded: string): boolean { return true; } -export function buildStreamTarget(udid: string): StreamConnectTarget { - return { udid }; +export function buildStreamTarget( + udid: string, + options: { remote?: boolean } = {}, +): StreamConnectTarget { + return { remote: options.remote, udid }; } export function canUseWebRtc(): boolean { @@ -48,10 +52,12 @@ class WebRtcStreamClient implements StreamClientBackend { private connectGeneration = 0; private controlChannel: RTCDataChannel | null = null; private diagnostics = createWebRtcDiagnostics(); + private disconnectGraceTimeout = 0; private frameWatchdogTimeout = 0; private lastVideoFrameAt = 0; private peerConnection: RTCPeerConnection | null = null; private reconnectTimeout = 0; + private remoteMode = false; private reportedVideoConfig = false; private shouldReconnect = false; private stats: StreamStats = createEmptyStreamStats(); @@ -80,6 +86,7 @@ class WebRtcStreamClient implements StreamClientBackend { const canvasElement = this.canvas; const generation = ++this.connectGeneration; this.shouldReconnect = true; + this.remoteMode = Boolean(target.remote); this.diagnostics = createWebRtcDiagnostics(); this.reportedVideoConfig = false; this.stats = createEmptyStreamStats(); @@ -171,18 +178,37 @@ class WebRtcStreamClient implements StreamClientBackend { peerConnection.onconnectionstatechange = () => { this.diagnostics.peerConnectionState = peerConnection.connectionState; this.postDiagnostics(target, "connectionstatechange"); - if ( - generation === this.connectGeneration && - (peerConnection.connectionState === "failed" || - peerConnection.connectionState === "disconnected") - ) { - if (peerConnection.connectionState === "failed") { - void this.updateSelectedCandidatePair(peerConnection, target); + if (generation !== this.connectGeneration) { + return; + } + if (peerConnection.connectionState === "connected") { + this.clearDisconnectGraceTimeout(); + if (this.reportedVideoConfig) { + this.onMessage({ + type: "status", + status: { detail: "WebRTC media connected", state: "streaming" }, + }); + } + return; + } + if (peerConnection.connectionState === "disconnected") { + if (this.remoteMode && this.stats.renderedFrames > 0) { + this.scheduleRemoteDisconnectGrace(target, generation); + return; } this.handleConnectionError( target, generation, - new Error(`WebRTC connection ${peerConnection.connectionState}.`), + new Error("WebRTC connection disconnected."), + ); + return; + } + if (peerConnection.connectionState === "failed") { + void this.updateSelectedCandidatePair(peerConnection, target); + this.handleConnectionError( + target, + generation, + new Error("WebRTC connection failed."), ); } }; @@ -228,6 +254,7 @@ class WebRtcStreamClient implements StreamClientBackend { this.shouldReconnect = false; this.connectGeneration += 1; this.clearReconnectTimeout(); + this.clearDisconnectGraceTimeout(); this.clearFrameWatchdog(); this.closeActiveConnection(); this.onMessage({ type: "status", status: { state: "idle" } }); @@ -241,6 +268,7 @@ class WebRtcStreamClient implements StreamClientBackend { window.cancelAnimationFrame(this.animationFrame); this.animationFrame = 0; this.clearFrameWatchdog(); + this.clearDisconnectGraceTimeout(); this.cancelVideoFrameCallback(); this.video?.pause(); if (this.video) { @@ -268,13 +296,46 @@ class WebRtcStreamClient implements StreamClientBackend { } const message = error instanceof Error ? error.message : String(error); this.closeActiveConnection(); + const reconnecting = this.remoteMode && this.stats.renderedFrames > 0; this.onMessage({ type: "status", - status: { error: message, state: "error" }, + status: reconnecting + ? { + detail: "Connection interrupted. Reconnecting...", + state: "connecting", + } + : { error: message, state: "error" }, }); this.scheduleReconnect(target, generation); } + private scheduleRemoteDisconnectGrace( + target: StreamConnectTarget, + generation: number, + ) { + if (this.disconnectGraceTimeout) { + return; + } + this.onMessage({ + type: "status", + status: { + detail: "Connection interrupted. Reconnecting...", + state: "connecting", + }, + }); + this.disconnectGraceTimeout = window.setTimeout(() => { + this.disconnectGraceTimeout = 0; + if (generation !== this.connectGeneration || !this.shouldReconnect) { + return; + } + this.handleConnectionError( + target, + generation, + new Error("WebRTC connection disconnected."), + ); + }, WEBRTC_REMOTE_DISCONNECTED_GRACE_MS); + } + private scheduleReconnect(target: StreamConnectTarget, generation: number) { if ( this.reconnectTimeout || @@ -340,6 +401,14 @@ class WebRtcStreamClient implements StreamClientBackend { this.reconnectTimeout = 0; } + private clearDisconnectGraceTimeout() { + if (!this.disconnectGraceTimeout) { + return; + } + window.clearTimeout(this.disconnectGraceTimeout); + this.disconnectGraceTimeout = 0; + } + private attachDiagnostics( peerConnection: RTCPeerConnection, target: StreamConnectTarget, @@ -488,8 +557,8 @@ class WebRtcStreamClient implements StreamClientBackend { ) { this.syncCanvasSize(this.video.videoWidth, this.video.videoHeight); this.reportVideoConfig(this.video.videoWidth, this.video.videoHeight); - const now = performance.now(); const renderStartedAt = performance.now(); + const now = performance.now(); const latestRenderMs = performance.now() - renderStartedAt; this.stats.decodedFrames += 1; this.stats.renderedFrames += 1; diff --git a/client/src/features/stream/useLiveStream.ts b/client/src/features/stream/useLiveStream.ts index 2ac94e4..8f8b6cc 100644 --- a/client/src/features/stream/useLiveStream.ts +++ b/client/src/features/stream/useLiveStream.ts @@ -24,6 +24,7 @@ const CLIENT_TELEMETRY_INTERVAL_MS = 1000; interface UseLiveStreamOptions { canvasElement: HTMLCanvasElement | null; paused?: boolean; + remote?: boolean; simulator: SimulatorMetadata | null; } @@ -67,6 +68,7 @@ function buildClientTelemetryUrl(): string { export function useLiveStream({ canvasElement, paused = false, + remote = false, simulator, }: UseLiveStreamOptions): UseLiveStreamResult { const clientTelemetryIdRef = useRef(""); @@ -237,11 +239,11 @@ export function useLiveStream({ return; } - workerClient.connect(buildStreamTarget(simulator.udid)); + workerClient.connect(buildStreamTarget(simulator.udid, { remote })); return () => { workerClient.disconnect(); }; - }, [canvasElement, simulator?.isBooted, simulator?.udid, paused]); + }, [canvasElement, simulator?.isBooted, simulator?.udid, paused, remote]); useEffect(() => { if (!simulator?.udid) { diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 99a1c3c..9901622 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -37,6 +37,22 @@ simdeck ui [--port 4310] [--bind 127.0.0.1] [--advertise-host ] `--open` opens the authenticated local URL after the daemon is ready. +### `studio expose` + +Expose one local simulator through SimDeck Studio: + +```sh +simdeck studio expose [simulator] [--studio-url https://simdeck.djdev.me] + [--port 4310] [--bind 127.0.0.1] + [--video-codec auto|hardware|software] + [--low-latency] [--stream-quality ] +``` + +Studio expose defaults to software H.264, realtime stream delivery, and the +`smooth` stream quality profile. The process prints the Studio URL plus the +active codec/profile, and keeps the outbound bridge alive until Ctrl-C. +`--video-codec hardware` opts back into the hardware encoder when that is preferable. + ### `daemon start` Start or reuse the project daemon without opening the browser: diff --git a/docs/cli/flags.md b/docs/cli/flags.md index 314bbf3..fafa451 100644 --- a/docs/cli/flags.md +++ b/docs/cli/flags.md @@ -39,6 +39,9 @@ Targets a specific running SimDeck daemon for commands that support the HTTP fas | `--stream-quality` | auto/default | Optional realtime stream quality profile: `quality`, `balanced`, `smooth`, `economy`, or `ci-software`. | | `--open` | `false` | `ui` only. Open the browser after the daemon is ready. | +`studio expose` defaults to software H.264. Pass `--video-codec hardware` to +opt into the hardware encoder when that is preferable. + The public commands generate an access token automatically. Use `simdeck daemon status` to read it for direct API callers. ## `describe` diff --git a/docs/guide/video.md b/docs/guide/video.md index 631e990..a374e8a 100644 --- a/docs/guide/video.md +++ b/docs/guide/video.md @@ -79,7 +79,9 @@ A few practical guidelines: - **Start on the default for local preview.** `auto` lets VideoToolbox choose without requiring the shared hardware encoder. - **Switch to `software` when the hardware encoder stalls or is unavailable.** The encoder scales the longest edge to 1600 pixels, can climb toward 60 fps, and backs off dynamically under encode latency. -- **Use `--stream-quality ci-software` for Studio providers on virtualized CI Macs when hardware encode is unavailable.** This profile uses software H.264 at a 960-pixel longest edge, targets 24 fps, lowers bitrate pressure, and favors fresh frames over full-resolution sharpness. +- **Studio providers default to software H.264 plus `--stream-quality smooth`.** This profile uses a 1170-pixel longest edge, allows up to 60 fps, raises the bitrate budget to reduce compression artifacts, and lets multiple provider sessions share CPU cores without depending on one hardware encoder. +- **Use `--stream-quality ci-software` for denser virtualized CI Macs.** This profile uses software H.264 at a 960-pixel longest edge, targets 24 fps, lowers bitrate pressure, and favors fresh frames over full-resolution sharpness. +- **Use `simdeck studio expose --video-codec hardware` only when a dedicated hardware encoder is preferable.** The normal Studio default stays on software H.264 so future multi-simulator provider hosts can scale across CPU cores. - **Use `software --low-latency` only when you need the older extra-conservative software profile.** It caps at 15 fps, uses a single pending frame, reduces the longest edge to 1170 pixels, and backs off before software encode latency turns into seconds of stream delay. ## Tuning with metrics diff --git a/scripts/studio-provider-bridge.mjs b/scripts/studio-provider-bridge.mjs index 2233ea9..7269fa6 100644 --- a/scripts/studio-provider-bridge.mjs +++ b/scripts/studio-provider-bridge.mjs @@ -93,6 +93,9 @@ async function registerProvider() { simulatorUdid: metadata.simulator?.udid, simulatorName: metadata.simulator?.name, runtimeName: metadata.simulator?.runtimeName, + videoCodec: metadata.health?.videoCodec, + realtimeStream: metadata.health?.realtimeStream, + streamQuality: metadata.health?.streamQuality, }); registered = true; lastRegisterAt = Date.now(); @@ -131,20 +134,25 @@ async function markProviderExpired() { } async function localProviderMetadata() { + let health = null; + try { + health = await localJson("/api/health"); + } catch { + health = null; + } + try { const simulators = await localJson("/api/simulators"); const selected = simulators.simulators?.find((simulator) => simulator.isBooted) ?? simulators.simulators?.[0] ?? null; - return { ok: true, simulator: selected }; + return { health, ok: true, simulator: selected }; } catch { - try { - await localJson("/api/health"); - return { ok: true, simulator: null }; - } catch { - return { ok: false, simulator: null }; + if (health) { + return { health, ok: true, simulator: null }; } + return { health: null, ok: false, simulator: null }; } } diff --git a/server/src/api/routes.rs b/server/src/api/routes.rs index 30a3927..40bccf8 100644 --- a/server/src/api/routes.rs +++ b/server/src/api/routes.rs @@ -115,25 +115,25 @@ const STREAM_QUALITY_PROFILES: &[StreamQualityProfile] = &[ id: "quality", label: "Quality", max_edge: 1440, - fps: 30, - min_bitrate: 3_000_000, - bits_per_pixel: 4, + fps: 60, + min_bitrate: 8_000_000, + bits_per_pixel: 6, }, StreamQualityProfile { id: "balanced", label: "Balanced", max_edge: 1280, - fps: 30, - min_bitrate: 2_500_000, - bits_per_pixel: 4, + fps: 60, + min_bitrate: 6_000_000, + bits_per_pixel: 5, }, StreamQualityProfile { id: "smooth", label: "Smooth", max_edge: 1170, - fps: 30, - min_bitrate: 2_000_000, - bits_per_pixel: 3, + fps: 60, + min_bitrate: 4_000_000, + bits_per_pixel: 5, }, StreamQualityProfile { id: "economy", diff --git a/server/src/main.rs b/server/src/main.rs index 1525ffa..3e525f1 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -445,7 +445,7 @@ enum StudioCommand { bind: IpAddr, #[arg(long)] low_latency: bool, - #[arg(long, value_enum, default_value_t = VideoCodecMode::Auto)] + #[arg(long, value_enum, default_value_t = VideoCodecMode::Software)] video_codec: VideoCodecMode, #[arg(long, value_enum, conflicts_with = "low_latency")] stream_quality: Option, @@ -627,23 +627,23 @@ fn stream_quality_env_for_profile(profile: &str) -> anyhow::Result Ok(StreamQualityEnvironment { profile: "quality", max_edge: 1440, - fps: 30, - min_bitrate: 3_000_000, - bits_per_pixel: 4, + fps: 60, + min_bitrate: 8_000_000, + bits_per_pixel: 6, }), "balanced" => Ok(StreamQualityEnvironment { profile: "balanced", max_edge: 1280, - fps: 30, - min_bitrate: 2_500_000, - bits_per_pixel: 4, + fps: 60, + min_bitrate: 6_000_000, + bits_per_pixel: 5, }), "smooth" => Ok(StreamQualityEnvironment { profile: "smooth", max_edge: 1170, - fps: 30, - min_bitrate: 2_000_000, - bits_per_pixel: 3, + fps: 60, + min_bitrate: 4_000_000, + bits_per_pixel: 5, }), "economy" => Ok(StreamQualityEnvironment { profile: "economy", @@ -691,7 +691,7 @@ fn studio_stream_quality_profile( .map(|profile| profile.as_profile_id().to_owned()) .or_else(|| { (video_codec == VideoCodecMode::Software && !low_latency) - .then_some("ci-software".to_owned()) + .then_some("smooth".to_owned()) }) } @@ -1288,7 +1288,7 @@ fn expose_to_studio(options: StudioExposeOptions) -> anyhow::Result<()> { video_codec: options.video_codec, low_latency: options.low_latency, realtime_stream: true, - stream_quality_profile, + stream_quality_profile: stream_quality_profile.clone(), })?; let selected = if let Some(selector) = options.simulator.as_deref() { select_studio_simulator(&metadata.http_url, selector) @@ -1305,6 +1305,23 @@ fn expose_to_studio(options: StudioExposeOptions) -> anyhow::Result<()> { .with_context(|| format!("boot simulator {}", simulator.name))?; } } + let health = service_get_json(&metadata.http_url, "/api/health").ok(); + let active_codec = health + .as_ref() + .and_then(|value| value.get("videoCodec")) + .and_then(Value::as_str) + .unwrap_or_else(|| options.video_codec.as_env_value()); + let active_stream_quality = health + .as_ref() + .and_then(|value| value.get("streamQuality")) + .and_then(|value| value.get("profile")) + .and_then(Value::as_str) + .or(stream_quality_profile.as_deref()); + let realtime_stream = health + .as_ref() + .and_then(|value| value.get("realtimeStream")) + .and_then(Value::as_bool) + .unwrap_or(true); let bridge_script = studio_provider_bridge_script()?; println!( @@ -1314,6 +1331,14 @@ fn expose_to_studio(options: StudioExposeOptions) -> anyhow::Result<()> { .map(|simulator| simulator.name.as_str()) .unwrap_or("the selected simulator") ); + println!( + "Stream: {}{}{}", + active_codec, + if realtime_stream { ", realtime" } else { "" }, + active_stream_quality + .map(|profile| format!(", quality={profile}")) + .unwrap_or_default() + ); println!("Press Ctrl-C to stop the Studio bridge."); let status = ProcessCommand::new("node") .arg(bridge_script) diff --git a/skills/simdeck/SKILL.md b/skills/simdeck/SKILL.md index e09cbec..8b84b23 100644 --- a/skills/simdeck/SKILL.md +++ b/skills/simdeck/SKILL.md @@ -45,12 +45,13 @@ and proxies REST requests through that bridge while WebRTC media negotiates directly with the runner. For an ad-hoc local provider that can be opened from another browser or phone, run `simdeck studio expose "iPhone 17 Pro"` and keep that process running. It -prints the unique Studio simulator URL. This defaults to `auto`, letting -VideoToolbox choose the encoder. Use `--video-codec software` to force software -encoding. Studio expose uses -realtime stream settings so remote viewers drop stale frames instead of building -latency. Software H.264 Studio providers default to the `ci-software` stream -quality profile; override with `--stream-quality quality|balanced|smooth|economy|ci-software`. +prints the unique Studio simulator URL and active stream settings. This defaults +to software H.264 with realtime stream settings so remote viewers drop stale +frames instead of building latency. Studio providers default to the `smooth` +stream quality profile (1170 px, dynamic up to 60 fps, higher bitrate to reduce +artifacts); override with +`--stream-quality quality|balanced|smooth|economy|ci-software`, or pass +`--video-codec hardware` when a dedicated hardware encoder is preferable. The local viewer gets the API token automatically. LAN browsers pair with the printed code before receiving the API cookie. Direct HTTP calls need `X-SimDeck-Token` or `Authorization: Bearer `. From 6ec0045ac495c95db9699b739a950ce94ff0a8ab Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 1 May 2026 22:30:43 -0400 Subject: [PATCH 2/8] Update SimDeck agent instructions --- README.md | 2 ++ docs/guide/video.md | 1 + server/src/main.rs | 3 +-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4b02f0..009983a 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ codec/profile when it starts. Studio defaults to the `smooth` stream quality profile (`1170` longest edge, dynamic up to `60` fps). Use `--stream-quality quality|balanced|smooth|economy|ci-software` to override it, or pass `--video-codec hardware` when a dedicated hardware encoder is preferable. +The remote viewer renders live video with the browser's native video element; +the canvas is only used for input geometry. CLI commands automatically use the same warm daemon: diff --git a/docs/guide/video.md b/docs/guide/video.md index a374e8a..edcef47 100644 --- a/docs/guide/video.md +++ b/docs/guide/video.md @@ -80,6 +80,7 @@ A few practical guidelines: - **Start on the default for local preview.** `auto` lets VideoToolbox choose without requiring the shared hardware encoder. - **Switch to `software` when the hardware encoder stalls or is unavailable.** The encoder scales the longest edge to 1600 pixels, can climb toward 60 fps, and backs off dynamically under encode latency. - **Studio providers default to software H.264 plus `--stream-quality smooth`.** This profile uses a 1170-pixel longest edge, allows up to 60 fps, raises the bitrate budget to reduce compression artifacts, and lets multiple provider sessions share CPU cores without depending on one hardware encoder. +- **The remote browser renders the live stream as a native `