Show P1/A1 cameras as a refreshing picture

SnapshotView fetches one frame at a time through the backend, pauses
while the tab is hidden and backs off after failures. PrinterCard picks
it when /stream answers kind "snapshot".
This commit is contained in:
2026-10-29 16:11:10 +00:00
parent 9bd9728b21
commit 6b1c7c6db3
6 changed files with 139 additions and 8 deletions
+7 -1
View File
@@ -1,7 +1,13 @@
/**
* Where a printer's camera can be watched, from `GET /printers/:id/stream`.
*/
export interface PrinterStream {
export type PrinterStream =
| WebrtcStream
// A P1/A1: no stream to negotiate, a picture to fetch again and again.
| { kind: "snapshot"; snapshotPath: string; intervalMs: number };
export interface WebrtcStream {
kind?: "webrtc";
/** The go2rtc stream name to negotiate for. */
cameraId: string;
/**
+7 -1
View File
@@ -56,6 +56,11 @@ async function signalOffer(printerId: string, sdp: string): Promise<string> {
return answer.sdp;
}
/** One camera frame (P1/A1) as an image, through the backend. */
function fetchFrame(printerId: string): Promise<Blob> {
return apiFetch<Blob>(`/printers/${printerId}/snapshot`, { responseType: "blob" });
}
// Which farms exist, and for each whether its gateway is connected and what
// discovery has found. Polled, because gateway presence has no push channel;
// ten seconds is quick enough to notice an unplugged box without hammering.
@@ -119,7 +124,7 @@ watch(
printers,
(list) => {
for (const printer of list ?? []) {
if (!printer.cameraStream || printer.id in streams.value) continue;
if (!(printer.cameraStream || printer.cameraSnapshot) || printer.id in streams.value) continue;
// Marked before the request resolves, so a re-render in the meantime
// does not fire a second lookup for the same printer. `null` renders
// no player, which is also the right thing to show while this is in
@@ -149,6 +154,7 @@ watch(
:printer="current(printer)"
:stream="streams[printer.id]"
:signal="(sdp: string) => signalOffer(printer.id, sdp)"
:fetch-frame="() => fetchFrame(printer.id)"
:send="(command: PrinterCommand) => sendCommand(printer.id, command)"
/>
</div>