`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.
77 lines
2.3 KiB
Vue
77 lines
2.3 KiB
Vue
<script setup lang="ts">
|
|
import { PrinterCard } from "@continuum/ui";
|
|
import { useTelemetryStore } from "~/stores/telemetry";
|
|
|
|
interface Printer {
|
|
id: string;
|
|
name: string;
|
|
model: string;
|
|
farmId: string;
|
|
/**
|
|
* Set only for a printer whose camera the gateway can actually restream,
|
|
* which in a mixed fleet is the minority. Its presence is what decides
|
|
* whether to ask for a stream URL at all - see the lookup below.
|
|
*/
|
|
cameraStream?: string | null;
|
|
}
|
|
|
|
const telemetry = useTelemetryStore();
|
|
const config = useRuntimeConfig();
|
|
|
|
const { data: printers } = await useFetch<Printer[]>(`${config.public.apiBase}/printers`, {
|
|
headers: useRequestHeaders(["cookie"]),
|
|
});
|
|
|
|
// Looked up per printer, because the answer is per printer, but only for the
|
|
// printers the list already says have a camera. Asking for all of them would
|
|
// mean one request per machine in the fleet, most of them 404 by design.
|
|
const streams = ref<Record<string, PrinterStream | null>>({});
|
|
|
|
// Read here, in setup, rather than inside the loop below: Nuxt's request
|
|
// helpers are only valid while a component is setting up.
|
|
const streamHeaders = useRequestHeaders(["cookie"]) as Record<string, string>;
|
|
|
|
watch(
|
|
printers,
|
|
(list) => {
|
|
for (const printer of list ?? []) {
|
|
if (!printer.cameraStream || 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
|
|
// flight.
|
|
streams.value[printer.id] = null;
|
|
void fetchPrinterStream(config.public.apiBase, printer.id, streamHeaders).then((stream) => {
|
|
streams.value[printer.id] = stream;
|
|
});
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="fleet">
|
|
<h1>Fleet overview</h1>
|
|
<div class="fleet__grid">
|
|
<PrinterCard
|
|
v-for="printer in printers ?? []"
|
|
:key="printer.id"
|
|
:printer="printer"
|
|
:live="telemetry.printers[printer.id]"
|
|
:stream="streams[printer.id]"
|
|
/>
|
|
</div>
|
|
<p v-if="!printers?.length">No printers registered yet.</p>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.fleet__grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|