Live view over WebRTC (openpanel L1, server and browser)

A device reporting a `live` port streams its camera to the browser's own
RTCPeerConnection. The flows socket carries the signalling as text, so it
crosses the portal: `live` messages on a person's socket, `signal` on a
device's (start, sdp, candidate, bye, busy). The server routes by session,
holds a device to two viewers, ends a session when either socket closes or
no answer comes within 10 s, and gives each socket an outbox of its own
that ends a session rather than drop a message. TURN credentials are
minted per session in coturn's REST scheme (STUN_URLS, TURN_URLS,
TURN_SECRET), the viewer's with the device's offer; GET /devices/{id}/ice
hands them out too. The tunnel keeps a stream's early frames instead of
dropping them past eight and forgets them when the stream or link goes.

A media tile on a device's camera, and the Devices tab's preview, play
live when tapped, pause frames meanwhile, and fall back to frames with a
short note on busy, failure or no picture within 10 s. coturn is an
optional compose service behind the `turn` profile.
This commit is contained in:
Melvin Strobl
2026-10-17 08:37:39 +02:00
parent efd883d092
commit 6a98dcd23f
20 changed files with 1535 additions and 24 deletions
+57
View File
@@ -2006,6 +2006,63 @@ export const HistoryPointSchema = {
description: 'One numeric value a message carried, and when.'
} as const;
export const IceServerSchema = {
properties: {
urls: {
items: {
type: 'string'
},
type: 'array',
title: 'Urls'
},
username: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Username'
},
credential: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Credential'
}
},
type: 'object',
required: ['urls'],
title: 'IceServer',
description: 'One STUN or TURN server, as `RTCPeerConnection` takes it.'
} as const;
export const IceServersSchema = {
properties: {
ice_servers: {
items: {
'$ref': '#/components/schemas/IceServer'
},
type: 'array',
title: 'Ice Servers'
},
ttl: {
type: 'integer',
title: 'Ttl',
default: 0
}
},
type: 'object',
title: 'IceServers'
} as const;
export const ImportArtifactSchema = {
properties: {
name: {
File diff suppressed because one or more lines are too long
+20
View File
@@ -686,6 +686,20 @@ export type HTTPValidationError = {
detail?: Array<ValidationError>;
};
/**
* One STUN or TURN server, as `RTCPeerConnection` takes it.
*/
export type IceServer = {
urls: Array<(string)>;
username?: (string | null);
credential?: (string | null);
};
export type IceServers = {
ice_servers?: Array<IceServer>;
ttl?: number;
};
export type ImportArtifact = {
name: string;
filename?: (string | null);
@@ -1879,6 +1893,12 @@ export type DevicesDeleteDeviceData = {
export type DevicesDeleteDeviceResponse = (Message);
export type DevicesReadIceData = {
deviceId: string;
};
export type DevicesReadIceResponse = (IceServers);
export type DevicesPutReportData = {
deviceId: string;
};