File size: 13,123 Bytes
8aedc84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
import enum
import logging
from datetime import datetime
from typing import Any, Literal, TypedDict

from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)

# ============= ENUMS =============


class ParticipantRole(enum.StrEnum):
    """Participant roles in a video room"""

    PRODUCER = "producer"  # Camera/video source
    CONSUMER = "consumer"  # Video viewer/receiver


class MessageType(enum.StrEnum):
    """WebSocket message types for video streaming"""

    # === CONNECTION & LIFECYCLE ===
    JOINED = "joined"  # Confirmation of successful room join
    ERROR = "error"  # Error notifications

    # === CONNECTION HEALTH ===
    HEARTBEAT = "heartbeat"  # Client ping for connection health
    HEARTBEAT_ACK = "heartbeat_ack"  # Server response to heartbeat

    # === VIDEO STREAMING ===
    VIDEO_CONFIG_UPDATE = "video_config_update"  # Video configuration update
    STREAM_STARTED = "stream_started"  # Stream started notification
    STREAM_STOPPED = "stream_stopped"  # Stream stopped notification
    RECOVERY_TRIGGERED = "recovery_triggered"  # Recovery policy triggered
    EMERGENCY_STOP = "emergency_stop"  # Emergency stop command

    # === STATUS & MONITORING ===
    STATUS_UPDATE = "status_update"  # General status updates
    STREAM_STATS = "stream_stats"  # Stream statistics
    PARTICIPANT_JOINED = "participant_joined"  # Participant joined room
    PARTICIPANT_LEFT = "participant_left"  # Participant left room

    # === WEBRTC SIGNALING ===
    WEBRTC_OFFER = "webrtc_offer"  # WebRTC offer forwarded between participants
    WEBRTC_ANSWER = "webrtc_answer"  # WebRTC answer forwarded between participants
    WEBRTC_ICE = "webrtc_ice"  # WebRTC ICE candidate forwarded between participants


class RecoveryPolicy(enum.StrEnum):
    """Frame recovery policies for handling video interruptions"""

    FREEZE_LAST_FRAME = "freeze_last_frame"  # Reuse last valid frame
    CONNECTION_INFO = "connection_info"  # Show informative status frame
    BLACK_SCREEN = "black_screen"  # Black screen with same dimensions
    FADE_TO_BLACK = "fade_to_black"  # Gradually fade last frame to black
    OVERLAY_STATUS = "overlay_status"  # Show last frame with overlay


class VideoEncoding(enum.StrEnum):
    """Supported video encodings"""

    JPEG = "jpeg"
    H264 = "h264"
    VP8 = "vp8"
    VP9 = "vp9"


class RawWebRTCMessageType(enum.StrEnum):
    """Raw WebRTC signaling message types from client API"""

    OFFER = "offer"
    ANSWER = "answer"
    ICE = "ice"


# ============= CORE DATA STRUCTURES (TypedDict) =============


class VideoConfigDict(TypedDict, total=False):
    """Video configuration dictionary"""

    encoding: str | None
    resolution: dict[str, int] | None  # {"width": int, "height": int}
    framerate: int | None
    bitrate: int | None
    quality: int | None


class StreamStatsDict(TypedDict):
    """Stream statistics structure"""

    stream_id: str
    duration_seconds: float
    frame_count: int
    total_bytes: int
    average_fps: float
    average_bitrate: float


class ParticipantInfoDict(TypedDict):
    """Information about room participants"""

    producer: str | None
    consumers: list[str]
    total: int


# ============= WEBRTC DATA STRUCTURES =============


class RTCSessionDescriptionDict(TypedDict):
    """RTCSessionDescription structure"""

    type: Literal["offer", "answer"]
    sdp: str


class RTCIceCandidateDict(TypedDict, total=False):
    """RTCIceCandidate structure (from candidate.toJSON())"""

    candidate: str
    sdpMLineIndex: int | None
    sdpMid: str | None
    usernameFragment: str | None


# ============= BASE MESSAGE STRUCTURES =============


class BaseWebSocketMessage(TypedDict):
    """Base WebSocket message structure"""

    type: str
    timestamp: str | None


# ============= CONNECTION & LIFECYCLE MESSAGES =============


class JoinedMessageDict(BaseWebSocketMessage):
    """Confirmation of successful room join"""

    type: Literal[MessageType.JOINED]
    room_id: str
    role: Literal[ParticipantRole.PRODUCER, ParticipantRole.CONSUMER]


class ErrorMessageDict(BaseWebSocketMessage):
    """Error notification message"""

    type: Literal[MessageType.ERROR]
    message: str
    code: str | None


# ============= CONNECTION HEALTH MESSAGES =============


class HeartbeatMessageDict(BaseWebSocketMessage):
    """Heartbeat ping from client"""

    type: Literal[MessageType.HEARTBEAT]


class HeartbeatAckMessageDict(BaseWebSocketMessage):
    """Heartbeat acknowledgment from server"""

    type: Literal[MessageType.HEARTBEAT_ACK]


# ============= VIDEO STREAMING MESSAGES =============


class VideoConfigUpdateMessageDict(BaseWebSocketMessage):
    """Video configuration update message"""

    type: Literal[MessageType.VIDEO_CONFIG_UPDATE]
    config: VideoConfigDict
    source: str | None


class StreamStartedMessageDict(BaseWebSocketMessage):
    """Stream started notification"""

    type: Literal[MessageType.STREAM_STARTED]
    config: VideoConfigDict
    participant_id: str


class StreamStoppedMessageDict(BaseWebSocketMessage):
    """Stream stopped notification"""

    type: Literal[MessageType.STREAM_STOPPED]
    participant_id: str
    reason: str | None


class RecoveryTriggeredMessageDict(BaseWebSocketMessage):
    """Recovery policy triggered message"""

    type: Literal[MessageType.RECOVERY_TRIGGERED]
    policy: Literal[
        RecoveryPolicy.FREEZE_LAST_FRAME,
        RecoveryPolicy.CONNECTION_INFO,
        RecoveryPolicy.BLACK_SCREEN,
        RecoveryPolicy.FADE_TO_BLACK,
        RecoveryPolicy.OVERLAY_STATUS,
    ]
    reason: str


class EmergencyStopMessageDict(BaseWebSocketMessage):
    """Emergency stop message"""

    type: Literal[MessageType.EMERGENCY_STOP]
    reason: str
    source: str | None


# ============= STATUS & MONITORING MESSAGES =============


class StreamStatsMessageDict(BaseWebSocketMessage):
    """Stream statistics message"""

    type: Literal[MessageType.STREAM_STATS]
    stats: StreamStatsDict


class StatusUpdateMessageDict(BaseWebSocketMessage):
    """General status update message"""

    type: Literal[MessageType.STATUS_UPDATE]
    status: str
    data: dict[str, Any] | None


class ParticipantJoinedMessageDict(BaseWebSocketMessage):
    """Participant joined room notification"""

    type: Literal[MessageType.PARTICIPANT_JOINED]
    room_id: str
    participant_id: str
    role: Literal[ParticipantRole.PRODUCER, ParticipantRole.CONSUMER]


class ParticipantLeftMessageDict(BaseWebSocketMessage):
    """Participant left room notification"""

    type: Literal[MessageType.PARTICIPANT_LEFT]
    room_id: str
    participant_id: str
    role: Literal[ParticipantRole.PRODUCER, ParticipantRole.CONSUMER]


# ============= WEBRTC SIGNALING MESSAGES =============


class WebRTCOfferMessageDict(BaseWebSocketMessage):
    """WebRTC offer message"""

    type: Literal[MessageType.WEBRTC_OFFER]
    offer: RTCSessionDescriptionDict  # RTCSessionDescription
    from_producer: str


class WebRTCAnswerMessageDict(BaseWebSocketMessage):
    """WebRTC answer message"""

    type: Literal[MessageType.WEBRTC_ANSWER]
    answer: RTCSessionDescriptionDict  # RTCSessionDescription
    from_consumer: str


class WebRTCIceMessageDict(BaseWebSocketMessage):
    """WebRTC ICE candidate message"""

    type: Literal[MessageType.WEBRTC_ICE]
    candidate: RTCIceCandidateDict  # RTCIceCandidate
    from_producer: str | None
    from_consumer: str | None


# ============= RAW WEBRTC SIGNALING (from client WebRTC API) =============


class RawWebRTCOfferDict(TypedDict, total=False):
    """Raw WebRTC offer from client WebRTC API"""

    type: Literal[RawWebRTCMessageType.OFFER]
    sdp: str
    target_consumer: str | None  # For producer targeting specific consumer


class RawWebRTCAnswerDict(TypedDict, total=False):
    """Raw WebRTC answer from client WebRTC API"""

    type: Literal[RawWebRTCMessageType.ANSWER]
    sdp: str
    target_producer: str | None  # For consumer responding to specific producer


class RawWebRTCIceDict(TypedDict, total=False):
    """Raw WebRTC ICE candidate from client WebRTC API"""

    type: Literal[RawWebRTCMessageType.ICE]
    candidate: RTCIceCandidateDict
    target_consumer: str | None  # For producer sending to specific consumer
    target_producer: str | None  # For consumer sending to specific producer


# ============= MESSAGE GROUPS (Union Types) =============

# Connection lifecycle messages
ConnectionLifecycleMessage = JoinedMessageDict | ErrorMessageDict

# Connection health messages
ConnectionHealthMessage = HeartbeatMessageDict | HeartbeatAckMessageDict

# Video streaming messages
VideoStreamingMessage = (
    VideoConfigUpdateMessageDict
    | StreamStartedMessageDict
    | StreamStoppedMessageDict
    | RecoveryTriggeredMessageDict
    | EmergencyStopMessageDict
)

# Status and monitoring messages
StatusMonitoringMessage = (
    StreamStatsMessageDict
    | StatusUpdateMessageDict
    | ParticipantJoinedMessageDict
    | ParticipantLeftMessageDict
)

# WebRTC signaling messages (WebSocket forwarding)
WebRTCSignalingMessage = (
    WebRTCOfferMessageDict | WebRTCAnswerMessageDict | WebRTCIceMessageDict
)

# Raw WebRTC signaling messages (from client WebRTC API)
RawWebRTCSignalingMessage = RawWebRTCOfferDict | RawWebRTCAnswerDict | RawWebRTCIceDict

# All WebSocket messages
WebSocketMessageDict = (
    ConnectionLifecycleMessage
    | ConnectionHealthMessage
    | VideoStreamingMessage
    | StatusMonitoringMessage
    | WebRTCSignalingMessage
)

# ============= PYDANTIC MODELS (API INPUT/OUTPUT ONLY) =============


class VideoConfig(BaseModel):
    """Video processing configuration"""

    encoding: VideoEncoding | None = Field(default=VideoEncoding.VP8)
    resolution: dict[str, int] | None = Field(default={"width": 640, "height": 480})
    framerate: int | None = Field(default=30, ge=1, le=120)
    bitrate: int | None = Field(default=1000000, ge=100000)
    quality: int | None = Field(default=80, ge=1, le=100)


class RecoveryConfig(BaseModel):
    """Video frame recovery configuration"""

    frame_timeout_ms: int = Field(default=100, ge=10, le=1000)
    max_frame_reuse_count: int = Field(default=3, ge=1, le=10)
    recovery_policy: RecoveryPolicy = RecoveryPolicy.FREEZE_LAST_FRAME
    fallback_policy: RecoveryPolicy = RecoveryPolicy.CONNECTION_INFO
    show_hold_indicators: bool = True
    info_frame_bg_color: tuple[int, int, int] = (20, 30, 60)
    info_frame_text_color: tuple[int, int, int] = (200, 200, 200)
    fade_intensity: float = Field(default=0.7, ge=0.0, le=1.0)
    overlay_opacity: float = Field(default=0.3, ge=0.0, le=1.0)


class CreateRoomRequest(BaseModel):
    """Request to create a new video room"""

    room_id: str | None = None
    workspace_id: str | None = None  # Optional - will be generated if not provided
    name: str | None = None
    config: VideoConfig | None = None
    recovery_config: RecoveryConfig | None = None
    max_consumers: int = Field(default=10, ge=1, le=100)


class WebRTCSignalRequest(BaseModel):
    """WebRTC signaling request"""

    client_id: str = Field(..., min_length=1, max_length=100)
    message: dict[str, Any]  # Raw WebRTC signaling message


class JoinMessage(BaseModel):
    """Message to join a video room"""

    participant_id: str = Field(..., min_length=1, max_length=100)
    role: ParticipantRole


class ParticipantInfo(BaseModel):
    """Information about room participants"""

    producer: str | None
    consumers: list[str]
    total: int


class StreamStats(BaseModel):
    """Video stream statistics"""

    stream_id: str
    duration_seconds: float
    frame_count: int
    total_bytes: int
    average_fps: float
    average_bitrate: float


class RoomInfo(BaseModel):
    """Basic room information"""

    id: str
    workspace_id: str
    participants: ParticipantInfo
    frame_count: int
    config: VideoConfig
    has_producer: bool
    active_consumers: int


class RoomState(BaseModel):
    """Detailed room state"""

    room_id: str
    workspace_id: str
    participants: ParticipantInfo
    frame_count: int
    last_frame_time: datetime | None
    current_config: VideoConfig
    timestamp: str


# ============= PYDANTIC MODELS FOR RAW WEBRTC SIGNALING =============


class RawWebRTCOffer(BaseModel):
    """Raw WebRTC offer from client WebRTC API"""

    type: Literal[RawWebRTCMessageType.OFFER]
    sdp: str
    target_consumer: str | None = None  # For producer targeting specific consumer


class RawWebRTCAnswer(BaseModel):
    """Raw WebRTC answer from client WebRTC API"""

    type: Literal[RawWebRTCMessageType.ANSWER]
    sdp: str
    target_producer: str | None = None  # For consumer responding to specific producer


class RawWebRTCIce(BaseModel):
    """Raw WebRTC ICE candidate from client WebRTC API"""

    type: Literal[RawWebRTCMessageType.ICE]
    candidate: RTCIceCandidateDict
    target_consumer: str | None = None  # For producer sending to specific consumer
    target_producer: str | None = None  # For consumer sending to specific producer