Show a printer's camera, from the backend's address not a build-time one
`WebRTCStreamView` has existed since before this commit with no consumer: nothing rendered it, and the URL it would have used came from a `go2rtcStreamBase` in `nuxt.config.ts` defaulting to `http://localhost:1984`. That default was the real problem. A camera stream lives on a gateway box inside one farm's network, each farm has its own, and the gateway now reports its address to the backend when it connects. A compiled-in constant could only ever be correct for a single-farm install, and its localhost default pointed every player at the operator's own laptop. It is gone, replaced by `fetchPrinterStream`, which asks `GET /printers/:id/stream`. Absence is the normal case here, not an error: only Bambu's X1/H2 class publishes a camera URL a gateway can restream, so a P1 or A1 has a camera and nothing to connect to. The backend 404s for those, this resolves to null, and the card renders no player - offering one that can never load would be worse than offering nothing. The lookup also runs only for printers whose `cameraStream` the fleet list already reports, so a twenty-printer farm does not make twenty requests to learn that eighteen of them 404. `PrinterCard` takes the stream as a prop rather than fetching it, like `live` above it: `packages/ui` is plain Vue that knows nothing about the API or about Nuxt, and that is what lets these components render from a test or the desktop shell with no server behind them. Three things in `WebRTCStreamView` that would have bitten whoever first pointed it at a real printer: - The fetch status was never checked. go2rtc answers a request for a stream it does not have with an error page, and handing that to `setRemoteDescription` throws about SDP parsing - which sends you looking at the wrong layer entirely. - `connect()` is async inside a watcher with nothing catching it, so a failure left the badge on "connecting" forever and showed up only as an unhandled rejection in the console. - Two prop changes in quick succession raced: the slower request could finish last and apply its answer to a connection already replaced and closed, leaving a black video with a "connected" badge. No `iceServers`, deliberately and now commented: both ends are on the same LAN, so host candidates are enough. The same fact is the MVP's limitation - there is no relay, so this plays for someone on the printers' network and for nobody else. `nuxt typecheck` and `vue-tsc --noEmit` both clean.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Where a printer's camera can be watched, from `GET /printers/:id/stream`.
|
||||
*/
|
||||
export interface PrinterStream {
|
||||
streamUrl: string;
|
||||
cameraId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the backend where a printer's camera can be watched, or `null` if it
|
||||
* cannot.
|
||||
*
|
||||
* This replaces the build-time `go2rtcStreamBase` that used to live in
|
||||
* `nuxt.config.ts`. A single compiled-in URL can only ever be correct for a
|
||||
* one-farm deployment: the stream lives on a gateway box inside a farm's own
|
||||
* network, each farm has its own, and the gateway reports its address to the
|
||||
* backend when it connects. So the address has to be asked for at runtime,
|
||||
* per printer, rather than baked in.
|
||||
*
|
||||
* A plain function and not a `useAsyncData` wrapper despite the file it lives
|
||||
* in: the caller needs one of these per printer, discovered from a list that
|
||||
* itself arrives asynchronously, and Nuxt's data composables may only be
|
||||
* called while a component is setting up. A loop over a resolved list is not
|
||||
* that.
|
||||
*
|
||||
* **Absence is the normal case, not an error.** Only Bambu's X1/H2 class
|
||||
* publishes a camera URL a gateway can restream; a P1 or A1 reports that it
|
||||
* has a camera and gives nothing to connect to. The backend answers 404 for
|
||||
* those, so this answers `null` and the caller shows no player at all -
|
||||
* rendering one that can never load would be worse than rendering nothing.
|
||||
*
|
||||
* It is also a LAN address with no relay in front of it, so the stream plays
|
||||
* only for someone on the same network as the printers. That is the right
|
||||
* trade-off for an operator standing in the workshop, and a real limitation
|
||||
* for anyone else.
|
||||
*/
|
||||
export async function fetchPrinterStream(
|
||||
apiBase: string,
|
||||
printerId: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<PrinterStream | null> {
|
||||
try {
|
||||
return await $fetch<PrinterStream>(`${apiBase}/printers/${printerId}/stream`, { headers });
|
||||
} catch (err: unknown) {
|
||||
// A 404 means "this printer has no watchable camera", which is expected
|
||||
// often enough that treating it as a failure would put an error state on
|
||||
// most cards in a mixed fleet. Anything else is a real problem: logged
|
||||
// and then treated as "no stream", because a broken camera lookup should
|
||||
// not take the fleet overview down with it.
|
||||
const status = (err as { statusCode?: number }).statusCode;
|
||||
if (status !== 404) {
|
||||
console.warn(`could not look up the camera stream for printer ${printerId}`, err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user