- Volatile frames are held in memory with a byte-bounded queue instead of the /dev/shm ring scanned on every put; workers keep writing to the directory, which is trimmed on a timer. A volatile upload no longer spools to disk. ARTIFACT_VOLATILE_BYTES=0 turns the ring off. - A device sends camera frames as binary messages on its socket, held and published in one step and credited to its device node; refused frames are logged and dropped. Its snapshot carries demand and the uplink (binary, or http through the portal). - One demand registry for every socket's wanted media, the tunnel's included, plus what running flows read, mapped onto on-demand ports and published as device_demand; at most 8 media names per socket. - Per-message deflate off in serve and dev; a device may fetch the last 8 digests of a message; published dashboards cached per store revision; a revoked device socket is closed with 1008. - Media tiles want bytes only while on screen; the Devices tab previews a wired camera; the device node warns when a flow reads an on-demand port.
144 lines
3.8 KiB
TypeScript
144 lines
3.8 KiB
TypeScript
import { useRef } from "react"
|
|
|
|
import { useOnScreen } from "@/hooks/useOnScreen"
|
|
import { isRef, useArtifactUrl } from "@/lib/media"
|
|
import { cn } from "@/lib/utils"
|
|
import { useBoundValue } from "./dataContext"
|
|
import { config, text } from "./ui/core/config"
|
|
import type { WidgetProps } from "./widgets"
|
|
|
|
/**
|
|
* What a message's bytes look like: a frame, a clip, a segment.
|
|
*
|
|
* Media never travels as a message — the reference does. The bytes come down
|
|
* the socket in front of it where the engine is holding the frame in memory,
|
|
* and are fetched from the artifact store otherwise; either way this tile only
|
|
* asks for what it is drawing. A camera serving its own stream is still played
|
|
* from source: point `stream_url` at it and the browser does the work.
|
|
*/
|
|
export function MediaWidget({ widget }: WidgetProps) {
|
|
const cfg = config(widget)
|
|
return (
|
|
<MediaView
|
|
message={text(cfg.message)}
|
|
stream={text(cfg.stream_url)}
|
|
dtype={text(cfg.dtype)}
|
|
fit={text(cfg.fit) === "contain" ? "contain" : "cover"}
|
|
autoplay={Boolean(cfg.autoplay)}
|
|
label={widget.title}
|
|
/>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The tile's body, for anywhere a message's media is shown.
|
|
*
|
|
* Frames are asked for only while the tile is on screen, in a tab being
|
|
* looked at: a camera is only sent while somebody watches it, and a tile
|
|
* scrolled away or behind another tab is nobody. A clip keeps playing either
|
|
* way.
|
|
*/
|
|
export function MediaView({
|
|
message,
|
|
stream = "",
|
|
dtype = "",
|
|
fit = "cover",
|
|
autoplay = false,
|
|
label,
|
|
}: {
|
|
message: string
|
|
stream?: string
|
|
dtype?: string
|
|
fit?: "cover" | "contain"
|
|
autoplay?: boolean
|
|
label?: string
|
|
}) {
|
|
const box = useRef<HTMLDivElement>(null)
|
|
const shown = useOnScreen(box)
|
|
const live = useBoundValue(message || undefined)
|
|
const value = live?.value
|
|
const ref = isRef(value) ? value : null
|
|
const kind = (ref?.media_type ?? dtype).split("/")[0]
|
|
const playing = kind === "audio" || kind === "video"
|
|
const active = shown || playing
|
|
const url = useArtifactUrl(
|
|
active ? ref : null,
|
|
active ? message || undefined : undefined,
|
|
)
|
|
|
|
const objectFit = fit === "contain" ? "object-contain" : "object-cover"
|
|
const alt = label || ref?.name || message
|
|
|
|
return (
|
|
<div
|
|
ref={box}
|
|
className="flex min-h-0 w-full flex-1 flex-col justify-center"
|
|
>
|
|
{body()}
|
|
</div>
|
|
)
|
|
|
|
function body() {
|
|
// A camera serving its own stream is played from source: the engine
|
|
// carries references at whatever rate the flow publishes them, which is
|
|
// not video.
|
|
if (stream && kind !== "audio") {
|
|
return (
|
|
<img
|
|
src={stream}
|
|
alt={alt}
|
|
className={cn("h-full w-full rounded-md", objectFit)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (!message && !stream) {
|
|
return <p className="text-muted-foreground">Pick a message.</p>
|
|
}
|
|
if (!ref) {
|
|
return <p className="text-muted-foreground">Nothing published yet.</p>
|
|
}
|
|
if (!url) {
|
|
return <p className="text-muted-foreground">Loading…</p>
|
|
}
|
|
|
|
if (kind === "audio") {
|
|
return (
|
|
<audio
|
|
// Keyed on the digest so a new clip replaces the element rather than
|
|
// leaving the last one's playhead on it.
|
|
key={ref.digest}
|
|
src={url}
|
|
controls
|
|
autoPlay={autoplay}
|
|
className="w-full"
|
|
>
|
|
<track kind="captions" />
|
|
</audio>
|
|
)
|
|
}
|
|
|
|
if (kind === "video") {
|
|
return (
|
|
<video
|
|
key={ref.digest}
|
|
src={url}
|
|
controls
|
|
autoPlay={autoplay}
|
|
className={cn("h-full w-full rounded-md", objectFit)}
|
|
>
|
|
<track kind="captions" />
|
|
</video>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<img
|
|
src={url}
|
|
alt={alt}
|
|
className={cn("h-full w-full rounded-md", objectFit)}
|
|
/>
|
|
)
|
|
}
|
|
}
|