docs(streams): document phase4 on-demand APIs and web simulator
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user