// rnx camera drives the host-owned camera source. fixture video uses the // same hidden video, canvas frame pump, ImageBitmap transfer, and // BarcodeDetector loop as a real device camera. `do scan` remains the narrow // synthetic special case for callers that have a payload string but no media. import { createHash, randomUUID } from 'node:crypto' import { createReadStream } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' import { cameraFixturesDir } from '../../src/home-paths' import { printBridgeFailureDiagnostics } from '../bridge-diagnostics' import { createBridgeFromParsed, evalInBridge, parseBridgeCliArgs, type WsBridge, } from '../ws-bridge' import type { CameraCommand, CameraResult } from '../../src/bridge-contract' const CAMERA_FIXTURE_ROUTE = '/__camera-fixtures/' const VIDEO_EXTENSIONS = new Set(['.m4v', '.mov', '.mp4', '.webm']) function printHelp() { console.log(` rnx camera — control the simulator camera source usage: rnx camera play [--loop] [--rate ] rnx camera stop rnx camera status [--json] rnx do scan [--format ] subcommands: play replace the active camera source with a local video fixture stop clear fixture mode and restore the live camera source status report live, fixture, or idle source state scan inject a synthetic barcode result into an active scan session options: --loop loop fixture playback --rate fixture playback rate (default: 1) --format synthetic BarcodeDetector format (default: qr_code) --json print status/result as JSON --sim target a specific sim --port

WS bridge port (default: 7668) examples: rnx camera play ./fixtures/qr.mp4 --loop rnx camera play ./fixtures/document.webm --rate 2 rnx camera status rnx camera stop rnx do scan "https://example.com" --format qr_code `) } function isCameraResult(value: unknown): value is CameraResult { if (!value || typeof value !== 'object') return false const ok = Reflect.get(value, 'ok') const source = Reflect.get(value, 'source') return ( typeof ok === 'boolean' && (source === 'live' || source === 'fixture' || source === 'idle') ) } async function sendCameraCommand( bridge: WsBridge, camera: CameraCommand, ): Promise { const result: unknown = await bridge.send({ type: 'camera', camera }) if (!isCameraResult(result)) { throw new Error('camera bridge returned an invalid result') } return result } function printCameraResult(result: CameraResult, json: boolean): void { if (json) { console.log(JSON.stringify(result)) return } if (result.source === 'fixture') { console.log( ` camera source: fixture (${result.url ?? 'unknown url'}, ${ result.loop ? 'looping' : 'once' }, ${result.rate ?? 1}x)`, ) } else if (result.source === 'live') { console.log(' camera source: live') } else if (result.url) { console.log( ` camera source: idle (fixture queued: ${result.url}, ${ result.loop ? 'looping' : 'once' }, ${result.rate ?? 1}x)`, ) } else { console.log(' camera source: idle') } if (!result.ok && result.error) console.error(` camera failed: ${result.error}`) } export async function runCamera( args: string[], opts: { port?: number } = {}, ): Promise { const parsed = parseBridgeCliArgs(args, { port: opts.port, stripBooleanFlags: ['--help', '-h', '--json', '--loop'], stripValueFlags: ['--format', '--rate'], }) const positional = parsed.positional const subcommand = positional[0] if ( !subcommand || subcommand === 'help' || args.includes('--help') || args.includes('-h') ) { printHelp() return subcommand ? 0 : 1 } const bridge = createBridgeFromParsed(parsed) const json = args.includes('--json') try { if (subcommand === 'scan') { const text = positional[1] if (!text) { console.error(' usage: rnx do scan ') return 1 } const formatFlag = args.indexOf('--format') const format = formatFlag >= 0 ? args[formatFlag + 1] : 'qr_code' if (!format) { console.error(' --format requires a value') return 1 } const injected = await evalInBridge( bridge, `globalThis.__sootsimCameraInjectScan?.(${JSON.stringify(text)}, ${JSON.stringify(format)}) ?? false`, ) if (!injected) { console.error( ' no active barcode scan session. open a scanning first', ) return 1 } console.log(` injected ${format} scan: ${text}`) return 0 } if (subcommand === 'status') { const result = await sendCameraCommand(bridge, { action: 'status' }) printCameraResult(result, json) return result.ok ? 0 : 1 } if (subcommand === 'stop') { const result = await sendCameraCommand(bridge, { action: 'stop' }) printCameraResult(result, json) return result.ok ? 0 : 1 } if (subcommand === 'play') { const input = positional[1] if (!input) { console.error(' usage: rnx camera play [--loop] [--rate ]') return 1 } const sourcePath = path.resolve(input) const extension = path.extname(sourcePath).toLowerCase() if (!VIDEO_EXTENSIONS.has(extension)) { console.error( ` unsupported camera fixture extension ${extension || '(none)'}. use mp4, webm, mov, or m4v`, ) return 1 } let sourceStat try { sourceStat = await fs.stat(sourcePath) } catch { console.error(` camera fixture not found: ${sourcePath}`) return 1 } if (!sourceStat.isFile() || sourceStat.size === 0) { console.error(` camera fixture must be a non-empty file: ${sourcePath}`) return 1 } const rateFlag = args.indexOf('--rate') const rawRate = rateFlag >= 0 ? args[rateFlag + 1] : undefined const rate = rawRate === undefined ? 1 : Number(rawRate) if (!Number.isFinite(rate) || rate <= 0) { console.error(' --rate must be a positive finite number') return 1 } const fixtureDirectory = cameraFixturesDir() await fs.mkdir(fixtureDirectory, { recursive: true }) const digest = createHash('sha256') await new Promise((resolve, reject) => { const input = createReadStream(sourcePath) input.on('data', (chunk) => digest.update(chunk)) input.once('error', reject) input.once('end', resolve) }) const filename = `${digest.digest('hex')}${extension}` const servedPath = path.join(fixtureDirectory, filename) let staged = false try { const servedStat = await fs.lstat(servedPath) if (!servedStat.isFile()) { throw new Error(`camera fixture cache entry is not a file: ${servedPath}`) } staged = true } catch (error) { if ( !error || typeof error !== 'object' || Reflect.get(error, 'code') !== 'ENOENT' ) { throw error } } if (!staged) { const temporaryPath = path.join( fixtureDirectory, `.${filename}.${process.pid}-${randomUUID()}.tmp`, ) await fs.copyFile(sourcePath, temporaryPath) try { await fs.link(temporaryPath, servedPath) } catch (error) { if ( !error || typeof error !== 'object' || Reflect.get(error, 'code') !== 'EEXIST' ) { throw error } } finally { await fs.unlink(temporaryPath).catch(() => {}) } } const url = `http://127.0.0.1:${parsed.wsPort}${CAMERA_FIXTURE_ROUTE}${filename}` const result = await sendCameraCommand(bridge, { action: 'play', url, loop: args.includes('--loop'), rate, }) printCameraResult(result, json) return result.ok ? 0 : 1 } console.error(` unknown camera subcommand: ${subcommand}`) printHelp() return 1 } catch (error) { console.error(` camera failed: ${error instanceof Error ? error.message : error}`) await printBridgeFailureDiagnostics(bridge) return 1 } finally { bridge.close() } }