From f66b5ad15dc90606df4b1c27690fc0056a5cbe1d Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Mon, 19 Jan 2026 13:20:00 +0000 Subject: [PATCH] docs(streams): document phase4 on-demand APIs and web simulator --- Backend/README.md | 21 +++++ Backend/docs/openapi.ts | 182 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/Backend/README.md b/Backend/README.md index c871ff3..f070c88 100644 --- a/Backend/README.md +++ b/Backend/README.md @@ -122,6 +122,20 @@ Motion realtime events: - Linked clients receive `motion:detected` as soon as camera starts event. - Linked clients receive `motion:ended` when camera ends event. +### On-Demand Streams (Phase 4) +| Endpoint | Purpose | +| --- | --- | +| `POST /streams/request` | Client device requests a linked camera to start a live stream | +| `POST /streams/:streamSessionId/accept` | Camera device accepts and transitions stream session to `streaming` | +| `POST /streams/:streamSessionId/end` | Requester/camera ends an existing stream session | +| `GET /streams/:streamSessionId/playback-token` | Obtain short-lived playback token for active stream | +| `GET /streams/me/list` | List stream sessions for the current device | + +Stream realtime events: +- Client receives `stream:requested` after request creation. +- Client receives `stream:started` when camera accepts. +- Both devices receive `stream:ended` when session is closed. + ### API Docs OpenAPI docs are generated from Zod/OpenAPI definitions: @@ -130,6 +144,13 @@ OpenAPI docs are generated from Zod/OpenAPI definitions: | `GET /openapi.json` | OpenAPI 3 spec (JSON) | | `GET /docs` | Swagger UI | +### Web Mobile Simulator +Use `GET /sim/mobile-sim.html` to run a browser simulator that behaves like the mobile app: +- Register as `camera` or `client` +- Connect Socket.IO with bearer device token +- Camera: process incoming `start_stream` commands, start/end motion events +- Client: create links, request streams, and fetch playback tokens + ### Admin Dashboard Access `/admin` with Basic auth to: diff --git a/Backend/docs/openapi.ts b/Backend/docs/openapi.ts index 7627476..c74bf8c 100644 --- a/Backend/docs/openapi.ts +++ b/Backend/docs/openapi.ts @@ -734,6 +734,187 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: 'post', + path: '/streams/request', + summary: 'Client device requests an on-demand stream from a linked camera', + tags: ['Streams'], + security: [{ bearerDeviceToken: [] }], + request: { + body: { + content: { + 'application/json': { + schema: z.object({ + cameraDeviceId: z.string().uuid(), + reason: z.enum(['on_demand', 'motion_follow_up']).optional(), + metadata: z.record(z.string(), z.unknown()).optional(), + }), + }, + }, + }, + }, + responses: { + 201: { + description: 'Stream request created', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + streamSession: z.object({ + id: z.string().uuid(), + ownerUserId: z.string().uuid(), + cameraDeviceId: z.string().uuid(), + requesterDeviceId: z.string().uuid(), + status: z.string(), + reason: z.string(), + }), + command: DeviceCommandSchema, + }), + }, + }, + }, + }, +}); + +registry.registerPath({ + method: 'post', + path: '/streams/{streamSessionId}/accept', + summary: 'Camera device accepts a pending on-demand stream request', + tags: ['Streams'], + security: [{ bearerDeviceToken: [] }], + request: { + params: z.object({ streamSessionId: z.string().uuid() }), + body: { + content: { + 'application/json': { + schema: z.object({ + streamKey: z.string().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), + }), + }, + }, + }, + }, + responses: { + 200: { + description: 'Stream session accepted', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + streamSession: z.object({ + id: z.string().uuid(), + status: z.string(), + streamKey: z.string().nullable(), + startedAt: z.string().datetime().nullable(), + }), + }), + }, + }, + }, + }, +}); + +registry.registerPath({ + method: 'post', + path: '/streams/{streamSessionId}/end', + summary: 'Requester or camera ends an active stream session', + tags: ['Streams'], + security: [{ bearerDeviceToken: [] }], + request: { + params: z.object({ streamSessionId: z.string().uuid() }), + body: { + content: { + 'application/json': { + schema: z.object({ + reason: z.enum(['completed', 'cancelled', 'failed']).optional(), + }), + }, + }, + }, + }, + responses: { + 200: { + description: 'Stream session ended', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + streamSession: z.object({ + id: z.string().uuid(), + status: z.string(), + endedAt: z.string().datetime().nullable(), + }), + }), + }, + }, + }, + }, +}); + +registry.registerPath({ + method: 'get', + path: '/streams/{streamSessionId}/playback-token', + summary: 'Get short-lived playback token for active stream session', + tags: ['Streams'], + security: [{ bearerDeviceToken: [] }], + request: { + params: z.object({ streamSessionId: z.string().uuid() }), + }, + responses: { + 200: { + description: 'Playback token response', + content: { + 'application/json': { + schema: z.object({ + streamSessionId: z.string().uuid(), + streamKey: z.string(), + status: z.string(), + playbackToken: z.string(), + expiresInSeconds: z.number().int(), + }), + }, + }, + }, + }, +}); + +registry.registerPath({ + method: 'get', + path: '/streams/me/list', + summary: 'List stream sessions for current device', + tags: ['Streams'], + security: [{ bearerDeviceToken: [] }], + request: { + query: z.object({ + status: z.string().optional(), + limit: z.coerce.number().int().min(1).max(100).default(25), + }), + }, + responses: { + 200: { + description: 'Stream sessions list', + content: { + 'application/json': { + schema: z.object({ + count: z.number().int(), + streamSessions: z.array( + z.object({ + id: z.string().uuid(), + cameraDeviceId: z.string().uuid(), + requesterDeviceId: z.string().uuid(), + status: z.string(), + reason: z.string(), + streamKey: z.string().nullable(), + }), + ), + }), + }, + }, + }, + }, +}); + export function buildOpenApiDocument() { const generator = new OpenApiGeneratorV3(registry.definitions); const document = generator.generateDocument({ @@ -752,6 +933,7 @@ export function buildOpenApiDocument() { { name: 'Device Links', description: 'Client-camera authorization links' }, { name: 'Commands', description: 'Realtime command dispatch and status' }, { name: 'Events', description: 'Motion event lifecycle and user event history' }, + { name: 'Streams', description: 'On-demand live stream control lifecycle' }, ], });