// Camera capture, reactive (SolidJS) layer. A camera streams into a texture id,
// so a viewfinder is just . Opening the camera IS
// the permission request (SDL semantics): it resolves once the stream is
// configured and rejects if the user denies access.
//
// The imperative primitive lives in the `flux:camera` module; import { open,
// listCameras, scanImage } from "flux:camera" for non-reactive use.
import { createSignal, onCleanup } from "@solidjs/signals"
import { listCameras, open } from "flux:camera"
import type { TextureId } from "flux:gpu"
import { on } from "srt:events"
export type CameraFacing = "front" | "back" | "unknown"
export type CameraInfo = {
id: number
name: string
facing: CameraFacing
}
export type BarcodeFormat = "qr"
export type BarcodeResult = {
data: string
format: BarcodeFormat
}
export type CameraOptions = {
/** Explicit device id from flux:camera listCameras(); takes precedence over facing. */
camera?: number
/** Pick the first camera with this facing (falls back to the first camera). */
facing?: "front" | "back"
/** Size hint; the device picks the closest supported mode. */
width?: number
height?: number
/** Decode these barcode formats from the stream (delivered via the barcode signal). */
scan?: BarcodeFormat[]
}
let devicesAccessor: (() => CameraInfo[]) | undefined
/**
* Current camera list as a reactive accessor: re-enumerates on hotplug. Also
* kicks off the camera subsystem (required before hotplug events fire), which
* starts asynchronously: the list is empty until the initial device events
* arrive - moments later normally, never if the platform's capture backend is
* wedged. App-lifetime: there is one camera subsystem, so no cleanup is
* needed.
*
* Coverage caveat (SDL 3.4.8): only Android delivers both add and remove. On
* Linux you get add events but not remove (removal is broken upstream); on
* macOS/Windows there is no camera hotplug at all.
*/
export function cameraDevices(): CameraInfo[] {
if (!devicesAccessor) {
let [devices, setDevices] = createSignal(listCameras())
on("cameraDeviceChange", () => setDevices(listCameras()))
devicesAccessor = devices
}
return devicesAccessor()
}
/**
* Low-level fallback: subscribe to camera hotplug events with a callback. Prefer
* `cameraDevices()` unless the reactive accessor does not fit. Re-enumerate with
* `listCameras()` (from flux:camera) inside the callback to get the new set.
* Returns an unsubscribe function.
*/
export function onDeviceChange(callback: (event: { added: boolean }) => void): () => void {
return on("cameraDeviceChange", callback)
}
/** A live camera as reactive accessors. */
export type CameraStream = {
/** Texture id once the stream is up, undefined while opening; render with . */
texture(): TextureId | undefined
/** Actual stream size, undefined while opening. */
width(): number | undefined
height(): number | undefined
/** The most recently decoded barcode (requires the scan option). */
barcode(): BarcodeResult | undefined
/** Set if opening failed (e.g. permission denied). */
error(): Error | undefined
}
/**
* Opens a camera and exposes it as reactive signals: read texture() in JSX and
* it appears once the stream is configured. Closes automatically when the
* reactive owner is disposed (e.g. the component unmounts). For imperative use,
* call open() from "flux:camera" directly.
*/
export function createCamera(options: CameraOptions = {}): CameraStream {
let [texture, setTexture] = createSignal(undefined)
let [width, setWidth] = createSignal(undefined)
let [height, setHeight] = createSignal(undefined)
let [barcode, setBarcode] = createSignal(undefined)
let [error, setError] = createSignal(undefined)
let session: Awaited> | undefined
let disposed = false
open(options)
.then((cam) => {
if (disposed) {
cam.close()
return
}
session = cam
if (options.scan) cam.onBarcode((result) => setBarcode(result))
setTexture(cam.texture)
setWidth(cam.width)
setHeight(cam.height)
})
.catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
onCleanup(() => {
disposed = true
if (session) {
session.close()
session = undefined
}
})
return { texture, width, height, barcode, error }
}