blanchon commited on
Commit
bf6e620
·
1 Parent(s): 03537af
Files changed (4) hide show
  1. server/launch_with_ui.py +12 -10
  2. server/logs.txt +0 -141
  3. server/p2p&server.txt +0 -1817
  4. test-docker.sh +1 -1
server/launch_with_ui.py CHANGED
@@ -35,6 +35,18 @@ app.add_middleware(
35
  allow_headers=["*"],
36
  )
37
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  # Mount the API under /api prefix
39
  app.mount("/api", api_app)
40
 
@@ -93,16 +105,6 @@ else:
93
  }
94
 
95
 
96
- @app.get("/health")
97
- async def health_check():
98
- return {
99
- "status": "healthy",
100
- "server_running": True,
101
- "frontend_enabled": serve_frontend,
102
- "api_available": True,
103
- }
104
-
105
-
106
  if __name__ == "__main__":
107
  port = int(os.getenv("PORT", 7860))
108
  host = os.getenv("HOST", "0.0.0.0")
 
35
  allow_headers=["*"],
36
  )
37
 
38
+
39
+ # Add the health endpoint BEFORE other routes to avoid conflicts
40
+ @app.get("/health")
41
+ async def health_check():
42
+ return {
43
+ "status": "healthy",
44
+ "server_running": True,
45
+ "frontend_enabled": os.getenv("SERVE_FRONTEND", "false").lower() == "true",
46
+ "api_available": True,
47
+ }
48
+
49
+
50
  # Mount the API under /api prefix
51
  app.mount("/api", api_app)
52
 
 
105
  }
106
 
107
 
 
 
 
 
 
 
 
 
 
 
108
  if __name__ == "__main__":
109
  port = int(os.getenv("PORT", 7860))
110
  host = os.getenv("HOST", "0.0.0.0")
server/logs.txt DELETED
@@ -1,141 +0,0 @@
1
- 🤖 Starting LeRobot Arena Modular Server...
2
- Starting server...
3
- INFO: 127.0.0.1:64126 - "GET /video/rooms HTTP/1.1" 200 OK
4
- INFO: 127.0.0.1:64126 - "OPTIONS /video/rooms HTTP/1.1" 200 OK
5
- INFO: 127.0.0.1:64126 - "POST /video/rooms HTTP/1.1" 200 OK
6
- INFO: 127.0.0.1:64126 - "GET /video/rooms HTTP/1.1" 200 OK
7
- INFO: 127.0.0.1:64126 - "GET /video/rooms HTTP/1.1" 200 OK
8
- INFO: 127.0.0.1:64132 - "OPTIONS /video/rooms/webcam/webrtc/signal HTTP/1.1" 200 OK
9
- INFO: 127.0.0.1:64134 - "GET /video/rooms/webcam HTTP/1.1" 200 OK
10
- INFO: 127.0.0.1:64132 - "POST /video/rooms/webcam/webrtc/signal HTTP/1.1" 200 OK
11
- INFO: 127.0.0.1:64134 - "POST /video/rooms/webcam/webrtc/signal HTTP/1.1" 200 OK
12
- INFO: 127.0.0.1:64134 - "GET /video/rooms/webcam HTTP/1.1" 200 OK
13
- INFO: 127.0.0.1:64134 - "POST /video/rooms/webcam/webrtc/signal HTTP/1.1" 200 OK
14
- INFO: 127.0.0.1:64136 - "POST /video/rooms/webcam/webrtc/signal HTTP/1.1" 200 OK
15
- INFO: 127.0.0.1:64141 - "POST /video/rooms HTTP/1.1" 200 OK
16
- INFO: 127.0.0.1:64145 - "GET /video/rooms HTTP/1.1" 200 OK
17
- INFO: 127.0.0.1:64145 - "GET /video/rooms/b22f5bc5-caa2-47bb-b4b5-d0411654dc9d/state HTTP/1.1" 200 OK
18
- INFO: 127.0.0.1:64158 - "POST /video/rooms/b22f5bc5-caa2-47bb-b4b5-d0411654dc9d/webrtc/signal HTTP/1.1" 200 OK
19
- INFO: 127.0.0.1:64145 - "OPTIONS /video/rooms/b22f5bc5-caa2-47bb-b4b5-d0411654dc9d/webrtc/signal HTTP/1.1" 200 OK
20
- INFO: 127.0.0.1:64160 - "GET /video/rooms/b22f5bc5-caa2-47bb-b4b5-d0411654dc9d HTTP/1.1" 200 OK
21
- INFO: 127.0.0.1:64145 - "POST /video/rooms/b22f5bc5-caa2-47bb-b4b5-d0411654dc9d/webrtc/signal HTTP/1.1" 200 OK
22
- INFO: 127.0.0.1:64145 - "POST /video/rooms/b22f5bc5-caa2-47bb-b4b5-d0411654dc9d/webrtc/signal HTTP/1.1" 200 OK
23
- INFO: 127.0.0.1:64170 - "POST /robotics/rooms HTTP/1.1" 200 OK
24
- INFO: 127.0.0.1:64181 - "GET /robotics/rooms HTTP/1.1" 200 OK
25
- INFO: 127.0.0.1:64181 - "GET /robotics/rooms/53ba9baa-e462-4a1b-8812-83f1337ae9f4/state HTTP/1.1" 200 OK
26
- INFO: 127.0.0.1:64185 - "GET /robotics/rooms HTTP/1.1" 200 OK
27
- INFO: 127.0.0.1:64185 - "GET /robotics/rooms HTTP/1.1" 200 OK
28
- INFO: 127.0.0.1:64187 - "GET /robotics/rooms HTTP/1.1" 200 OK
29
- INFO: 127.0.0.1:64187 - "GET /robotics/rooms HTTP/1.1" 200 OK
30
- INFO: 127.0.0.1:64187 - "GET /robotics/rooms HTTP/1.1" 200 OK
31
- INFO: 127.0.0.1:64187 - "GET /robotics/rooms HTTP/1.1" 200 OK
32
- INFO: 127.0.0.1:64196 - "GET /robotics/rooms HTTP/1.1" 200 OK
33
- INFO: 127.0.0.1:64201 - "GET /robotics/rooms HTTP/1.1" 200 OK
34
- INFO: 127.0.0.1:64217 - "GET /robotics/rooms HTTP/1.1" 200 OK
35
- INFO: 127.0.0.1:64217 - "GET /robotics/rooms HTTP/1.1" 200 OK
36
- INFO: 127.0.0.1:64223 - "GET /robotics/rooms HTTP/1.1" 200 OK
37
- INFO: 127.0.0.1:64223 - "GET /robotics/rooms HTTP/1.1" 200 OK
38
- INFO: 127.0.0.1:64225 - "GET /robotics/rooms HTTP/1.1" 200 OK
39
- INFO: 127.0.0.1:64225 - "GET /robotics/rooms HTTP/1.1" 200 OK
40
- INFO: 127.0.0.1:64228 - "GET /robotics/rooms HTTP/1.1" 200 OK
41
- INFO: 127.0.0.1:64228 - "GET /robotics/rooms HTTP/1.1" 200 OK
42
- INFO: 127.0.0.1:64228 - "GET /robotics/rooms HTTP/1.1" 200 OK
43
- INFO: 127.0.0.1:64232 - "GET /robotics/rooms HTTP/1.1" 200 OK
44
- INFO: 127.0.0.1:64232 - "GET /robotics/rooms HTTP/1.1" 200 OK
45
- INFO: 127.0.0.1:64232 - "GET /robotics/rooms HTTP/1.1" 200 OK
46
- INFO: 127.0.0.1:64232 - "GET /robotics/rooms HTTP/1.1" 200 OK
47
- INFO: 127.0.0.1:64232 - "GET /robotics/rooms HTTP/1.1" 200 OK
48
- INFO: 127.0.0.1:64232 - "GET /robotics/rooms HTTP/1.1" 200 OK
49
- INFO: 127.0.0.1:64232 - "GET /robotics/rooms HTTP/1.1" 200 OK
50
- INFO: 127.0.0.1:64267 - "GET /robotics/rooms HTTP/1.1" 200 OK
51
- INFO: 127.0.0.1:64267 - "GET /robotics/rooms HTTP/1.1" 200 OK
52
- INFO: 127.0.0.1:64276 - "GET /robotics/rooms HTTP/1.1" 200 OK
53
- INFO: 127.0.0.1:64276 - "GET /robotics/rooms HTTP/1.1" 200 OK
54
- INFO: 127.0.0.1:64276 - "GET /robotics/rooms HTTP/1.1" 200 OK
55
- INFO: 127.0.0.1:64276 - "GET /robotics/rooms HTTP/1.1" 200 OK
56
- INFO: 127.0.0.1:64283 - "GET /robotics/rooms HTTP/1.1" 200 OK
57
- INFO: 127.0.0.1:64283 - "GET /robotics/rooms HTTP/1.1" 200 OK
58
- INFO: 127.0.0.1:64294 - "GET /robotics/rooms HTTP/1.1" 200 OK
59
- INFO: 127.0.0.1:64294 - "GET /robotics/rooms HTTP/1.1" 200 OK
60
- INFO: 127.0.0.1:64294 - "GET /robotics/rooms HTTP/1.1" 200 OK
61
- INFO: 127.0.0.1:64294 - "GET /robotics/rooms HTTP/1.1" 200 OK
62
- INFO: 127.0.0.1:64294 - "GET /robotics/rooms HTTP/1.1" 200 OK
63
- INFO: 127.0.0.1:64294 - "GET /robotics/rooms HTTP/1.1" 200 OK
64
- INFO: 127.0.0.1:64294 - "GET /robotics/rooms HTTP/1.1" 200 OK
65
- INFO: 127.0.0.1:64308 - "GET /robotics/rooms HTTP/1.1" 200 OK
66
- INFO: 127.0.0.1:64320 - "GET /robotics/rooms HTTP/1.1" 200 OK
67
- INFO: 127.0.0.1:64322 - "GET /robotics/rooms HTTP/1.1" 200 OK
68
- INFO: 127.0.0.1:64327 - "GET /robotics/rooms HTTP/1.1" 200 OK
69
- INFO: 127.0.0.1:64327 - "GET /robotics/rooms HTTP/1.1" 200 OK
70
- INFO: 127.0.0.1:64327 - "GET /robotics/rooms HTTP/1.1" 200 OK
71
- INFO: 127.0.0.1:64349 - "GET /robotics/rooms HTTP/1.1" 200 OK
72
- INFO: 127.0.0.1:64349 - "GET /robotics/rooms HTTP/1.1" 200 OK
73
- INFO: 127.0.0.1:64353 - "GET /robotics/rooms HTTP/1.1" 200 OK
74
- INFO: 127.0.0.1:64353 - "GET /robotics/rooms HTTP/1.1" 200 OK
75
- INFO: 127.0.0.1:64358 - "GET /robotics/rooms HTTP/1.1" 200 OK
76
- INFO: 127.0.0.1:64358 - "GET /robotics/rooms HTTP/1.1" 200 OK
77
- INFO: 127.0.0.1:64371 - "GET /robotics/rooms HTTP/1.1" 200 OK
78
- INFO: 127.0.0.1:64384 - "POST /video/rooms HTTP/1.1" 200 OK
79
- INFO: 127.0.0.1:64386 - "POST /robotics/rooms HTTP/1.1" 200 OK
80
- INFO: 127.0.0.1:64388 - "POST /robotics/rooms HTTP/1.1" 200 OK
81
- INFO: 127.0.0.1:64400 - "POST /video/rooms HTTP/1.1" 200 OK
82
- INFO: 127.0.0.1:64422 - "GET /robotics/rooms HTTP/1.1" 200 OK
83
- INFO: 127.0.0.1:64422 - "GET /robotics/rooms HTTP/1.1" 200 OK
84
- INFO: 127.0.0.1:64422 - "GET /video/rooms HTTP/1.1" 200 OK
85
- INFO: 127.0.0.1:64422 - "GET /video/rooms HTTP/1.1" 200 OK
86
- INFO: 127.0.0.1:64422 - "GET /video/rooms HTTP/1.1" 200 OK
87
- INFO: 127.0.0.1:64422 - "GET /video/rooms/act-session-cb638131-camera/state HTTP/1.1" 200 OK
88
- INFO: 127.0.0.1:64441 - "POST /video/rooms/act-session-cb638131-camera/webrtc/signal HTTP/1.1" 200 OK
89
- INFO: 127.0.0.1:64422 - "OPTIONS /video/rooms/act-session-cb638131-camera/webrtc/signal HTTP/1.1" 200 OK
90
- INFO: 127.0.0.1:64443 - "GET /video/rooms/act-session-cb638131-camera HTTP/1.1" 200 OK
91
- INFO: 127.0.0.1:64422 - "POST /video/rooms/act-session-cb638131-camera/webrtc/signal HTTP/1.1" 200 OK
92
- INFO: 127.0.0.1:64422 - "POST /video/rooms/act-session-cb638131-camera/webrtc/signal HTTP/1.1" 200 OK
93
- INFO: 127.0.0.1:64422 - "GET /video/rooms/act-session-cb638131-camera HTTP/1.1" 200 OK
94
- INFO: 127.0.0.1:64422 - "POST /video/rooms/act-session-cb638131-camera/webrtc/signal HTTP/1.1" 200 OK
95
- INFO: 127.0.0.1:64445 - "GET /video/rooms HTTP/1.1" 200 OK
96
- INFO: 127.0.0.1:64445 - "GET /video/rooms HTTP/1.1" 200 OK
97
- INFO: 127.0.0.1:64445 - "GET /video/rooms HTTP/1.1" 200 OK
98
- INFO: 127.0.0.1:64445 - "GET /video/rooms HTTP/1.1" 200 OK
99
- INFO: 127.0.0.1:64445 - "GET /video/rooms HTTP/1.1" 200 OK
100
- INFO: 127.0.0.1:64451 - "POST /video/rooms HTTP/1.1" 200 OK
101
- INFO: 127.0.0.1:64453 - "POST /robotics/rooms HTTP/1.1" 200 OK
102
- INFO: 127.0.0.1:64455 - "POST /robotics/rooms HTTP/1.1" 200 OK
103
- INFO: 127.0.0.1:64445 - "GET /video/rooms HTTP/1.1" 200 OK
104
- INFO: 127.0.0.1:64465 - "GET /video/rooms HTTP/1.1" 200 OK
105
- INFO: 127.0.0.1:64465 - "GET /video/rooms HTTP/1.1" 200 OK
106
- INFO: 127.0.0.1:64482 - "GET /video/rooms HTTP/1.1" 200 OK
107
- INFO: 127.0.0.1:64482 - "GET /video/rooms HTTP/1.1" 200 OK
108
- INFO: 127.0.0.1:64496 - "GET /video/rooms HTTP/1.1" 200 OK
109
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
110
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
111
- INFO: 127.0.0.1:64499 - "GET /robotics/rooms HTTP/1.1" 200 OK
112
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
113
- INFO: 127.0.0.1:64499 - "GET /robotics/rooms HTTP/1.1" 200 OK
114
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
115
- INFO: 127.0.0.1:64499 - "GET /robotics/rooms HTTP/1.1" 200 OK
116
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
117
- INFO: 127.0.0.1:64499 - "GET /robotics/rooms HTTP/1.1" 200 OK
118
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
119
- INFO: 127.0.0.1:64499 - "GET /robotics/rooms HTTP/1.1" 200 OK
120
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
121
- INFO: 127.0.0.1:64520 - "POST /video/rooms HTTP/1.1" 200 OK
122
- INFO: 127.0.0.1:64522 - "POST /robotics/rooms HTTP/1.1" 200 OK
123
- INFO: 127.0.0.1:64524 - "POST /robotics/rooms HTTP/1.1" 200 OK
124
- INFO: 127.0.0.1:64499 - "GET /robotics/rooms HTTP/1.1" 200 OK
125
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
126
- INFO: 127.0.0.1:64499 - "GET /robotics/rooms HTTP/1.1" 200 OK
127
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
128
- INFO: 127.0.0.1:64499 - "GET /video/rooms HTTP/1.1" 200 OK
129
- INFO: 127.0.0.1:64568 - "GET /robotics/rooms/act-session-a440b338-joint-output/state HTTP/1.1" 200 OK
130
- INFO: 127.0.0.1:64598 - "GET /video/rooms HTTP/1.1" 200 OK
131
- INFO: 127.0.0.1:64600 - "OPTIONS /video/rooms HTTP/1.1" 200 OK
132
- INFO: 127.0.0.1:64600 - "POST /video/rooms HTTP/1.1" 200 OK
133
- INFO: 127.0.0.1:64600 - "GET /video/rooms HTTP/1.1" 200 OK
134
- INFO: 127.0.0.1:64600 - "GET /video/rooms HTTP/1.1" 200 OK
135
- INFO: 127.0.0.1:64609 - "GET /video/rooms/sdpoksd/state HTTP/1.1" 200 OK
136
- INFO: 127.0.0.1:64609 - "GET /video/rooms HTTP/1.1" 200 OK
137
- INFO: 127.0.0.1:64614 - "GET /video/rooms HTTP/1.1" 200 OK
138
- INFO: 127.0.0.1:64616 - "GET /video/rooms HTTP/1.1" 200 OK
139
- INFO: 127.0.0.1:64618 - "GET /video/rooms HTTP/1.1" 200 OK
140
- INFO: 127.0.0.1:64621 - "GET /video/rooms HTTP/1.1" 200 OK
141
- Shutting down server...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/p2p&server.txt DELETED
@@ -1,1817 +0,0 @@
1
- """
2
- Video API routes for LeRobot Arena
3
-
4
- This module provides REST API endpoints for video room management,
5
- WebRTC signaling, and real-time communication.
6
- """
7
-
8
- import logging
9
-
10
- from fastapi import APIRouter, Depends, HTTPException, WebSocket
11
-
12
- from .core import VideoCore
13
- from .models import (
14
- CommunicationStrategy,
15
- CreateRoomRequest,
16
- ParticipantRole,
17
- WebRTCSignalRequest,
18
- )
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
- # Global video core instance
23
- video_core = VideoCore()
24
-
25
- # Create router
26
- router = APIRouter(prefix="/video", tags=["video"])
27
-
28
-
29
- def get_video_core() -> VideoCore:
30
- """Dependency to get the video core instance"""
31
- return video_core
32
-
33
-
34
- @router.get("/rooms")
35
- async def list_rooms():
36
- """List all video rooms with details"""
37
- return {
38
- "success": True,
39
- "rooms": video_core.list_rooms(),
40
- "total": len(video_core.rooms),
41
- }
42
-
43
-
44
- @router.post("/rooms")
45
- async def create_room(request: CreateRoomRequest | None = None):
46
- """Create a new video room"""
47
- room_id = None
48
- config = None
49
- recovery_config = None
50
- communication_config = None
51
-
52
- if request:
53
- room_id = request.room_id
54
- config = request.config
55
- recovery_config = request.recovery_config
56
- communication_config = request.communication_config
57
-
58
- room_id = video_core.create_room(
59
- room_id, config, recovery_config, communication_config
60
- )
61
-
62
- return {
63
- "success": True,
64
- "room_id": room_id,
65
- "message": f"Video room {room_id} created successfully",
66
- "communication_strategy": video_core.rooms[room_id].active_strategy.value
67
- if room_id in video_core.rooms
68
- else "unknown",
69
- }
70
-
71
-
72
- @router.get("/rooms/{room_id}")
73
- async def get_room(room_id: str):
74
- """Get room details"""
75
- room_info = video_core.get_room_info(room_id)
76
- if "error" in room_info:
77
- raise HTTPException(status_code=404, detail="Room not found")
78
-
79
- return {"success": True, "room": room_info}
80
-
81
-
82
- @router.delete("/rooms/{room_id}")
83
- async def delete_room(room_id: str):
84
- """Delete a video room"""
85
- if video_core.delete_room(room_id):
86
- return {"success": True, "message": f"Room {room_id} deleted successfully"}
87
- raise HTTPException(status_code=404, detail="Room not found")
88
-
89
-
90
- @router.get("/rooms/{room_id}/state")
91
- async def get_room_state(room_id: str):
92
- """Get current room state with participants and video stats"""
93
- state = video_core.get_room_state(room_id)
94
- if "error" in state:
95
- raise HTTPException(status_code=404, detail="Room not found")
96
-
97
- return {"success": True, "state": state}
98
-
99
-
100
- @router.post("/rooms/{room_id}/webrtc/signal")
101
- async def handle_webrtc_signal(room_id: str, signal: WebRTCSignalRequest):
102
- """Handle WebRTC signaling (offer, answer, ice)"""
103
- try:
104
- # Get the participant's role from the room
105
- room = video_core.rooms.get(room_id)
106
- participant_role = None
107
-
108
- if room:
109
- if room.producer == signal.client_id:
110
- participant_role = "producer"
111
- elif signal.client_id in room.consumers:
112
- participant_role = "consumer"
113
-
114
- logger.info(
115
- f"🔧 WebRTC signal from {signal.client_id} (role: {participant_role}) in room {room_id}"
116
- )
117
-
118
- response = await video_core.handle_webrtc_signal(
119
- room_id, signal.client_id, signal.message, participant_role
120
- )
121
-
122
- except ValueError as e:
123
- raise HTTPException(status_code=400) from e
124
- except Exception as e:
125
- raise HTTPException(status_code=500) from e
126
- else:
127
- if response:
128
- return {"success": True, "response": response}
129
- return {"success": True, "message": "Signal processed"}
130
-
131
-
132
- @router.post("/rooms/{room_id}/strategy")
133
- async def switch_room_strategy(
134
- room_id: str,
135
- request: dict,
136
- video_core: VideoCore = Depends(get_video_core),
137
- ):
138
- """Switch communication strategy for a room"""
139
- try:
140
- strategy_str = request.get("strategy")
141
- reason = request.get("reason", "API request")
142
-
143
- if not strategy_str:
144
- raise HTTPException(status_code=400, detail="Strategy is required")
145
-
146
- try:
147
- strategy = CommunicationStrategy(strategy_str)
148
- except ValueError:
149
- raise HTTPException(
150
- status_code=400, detail=f"Invalid strategy: {strategy_str}"
151
- )
152
-
153
- success = await video_core.switch_strategy(room_id, strategy, reason)
154
-
155
- if not success:
156
- raise HTTPException(status_code=404, detail="Room not found")
157
-
158
- # Get updated strategy info
159
- room = video_core.rooms.get(room_id)
160
- if not room:
161
- raise HTTPException(status_code=404, detail="Room not found")
162
-
163
- strategy_info = {
164
- "current_strategy": room.active_strategy.value,
165
- "available_strategies": ["p2p", "server", "hybrid"],
166
- "can_switch": True,
167
- "switch_count": 0, # Could be tracked if needed
168
- }
169
-
170
- return {
171
- "success": True,
172
- "strategy_info": strategy_info,
173
- "message": f"Strategy switched to {strategy.value}",
174
- }
175
-
176
- except HTTPException:
177
- raise
178
- except Exception as e:
179
- logger.exception(f"Error switching strategy for room {room_id}")
180
- raise HTTPException(status_code=500, detail=f"Failed to switch strategy: {e!s}")
181
-
182
-
183
- @router.get("/rooms/{room_id}/strategy")
184
- async def get_room_strategy(
185
- room_id: str,
186
- video_core: VideoCore = Depends(get_video_core),
187
- ):
188
- """Get current strategy information for a room"""
189
- room = video_core.rooms.get(room_id)
190
- if not room:
191
- raise HTTPException(status_code=404, detail="Room not found")
192
-
193
- strategy_info = {
194
- "current_strategy": room.active_strategy.value,
195
- "available_strategies": ["p2p", "server", "hybrid"],
196
- "can_switch": True,
197
- "switch_count": 0, # Could be tracked if needed
198
- }
199
-
200
- return {"success": True, "strategy_info": strategy_info}
201
-
202
-
203
- @router.get("/status")
204
- async def get_status():
205
- """Get video service status"""
206
- strategy_handlers = video_core.strategy_handlers
207
- strategy_stats = {}
208
-
209
- for room_id, handler in strategy_handlers.items():
210
- strategy = handler.strategy.value
211
- if strategy not in strategy_stats:
212
- strategy_stats[strategy] = 0
213
- strategy_stats[strategy] += 1
214
-
215
- return {
216
- "service": "video",
217
- "status": "active",
218
- "rooms_count": len(video_core.rooms),
219
- "websocket_connections_count": len(video_core.websocket_connections),
220
- "version": "2.0.0",
221
- "supported_roles": [role.value for role in ParticipantRole],
222
- "supported_encodings": ["jpeg", "h264", "vp8", "vp9"],
223
- "recovery_policies": [
224
- "freeze_last_frame",
225
- "connection_info",
226
- "black_screen",
227
- "fade_to_black",
228
- "overlay_status",
229
- ],
230
- "communication_strategies": {
231
- "supported": ["p2p", "server", "hybrid"],
232
- "active_by_strategy": strategy_stats,
233
- "total_strategy_handlers": len(strategy_handlers),
234
- },
235
- }
236
-
237
-
238
- @router.get("/health")
239
- async def health_check():
240
- """Health check endpoint"""
241
- return {"status": "healthy", "service": "video"}
242
-
243
-
244
- @router.get("/")
245
- async def video_status():
246
- """Video service main endpoint"""
247
- return {
248
- "status": "active",
249
- "service": "video",
250
- "message": "Video service running with room-based WebRTC streaming",
251
- "version": "2.0.0",
252
- "architecture": "producer/consumer rooms",
253
- "endpoints": [
254
- "/video/rooms - List all video rooms",
255
- "/video/rooms/{room_id} - Get room details",
256
- "/video/rooms/{room_id}/webrtc/signal - WebRTC signaling",
257
- "/video/rooms/{room_id}/ws - WebSocket connection",
258
- "/video/status - Service status",
259
- ],
260
- }
261
-
262
-
263
- @router.websocket("/rooms/{room_id}/ws")
264
- async def websocket_endpoint(websocket: WebSocket, room_id: str):
265
- """WebSocket connection for room management and heartbeat"""
266
- await video_core.handle_websocket(websocket, room_id)
267
-
268
- <script lang="ts">
269
- import { onMount } from 'svelte';
270
- import { video } from 'lerobot-arena-client';
271
- import type { video as videoTypes } from 'lerobot-arena-client';
272
-
273
- // State
274
- let producer: video.VideoProducer;
275
- let connected = $state<boolean>(false);
276
- let connecting = $state<boolean>(false);
277
- let streaming = $state<boolean>(false);
278
- let roomId = $state<string>('');
279
- let participantId = $state<string>('');
280
- let error = $state<string>('');
281
-
282
- // Video configuration
283
- let videoConfig = $state<videoTypes.VideoConfig>({
284
- resolution: { width: 640, height: 480 },
285
- framerate: 30,
286
- bitrate: 1000000,
287
- encoding: 'vp8',
288
- quality: 80
289
- });
290
-
291
- // Available configurations
292
- const resolutions = [
293
- { label: '320x240', value: { width: 320, height: 240 } },
294
- { label: '640x480', value: { width: 640, height: 480 } },
295
- { label: '1280x720', value: { width: 1280, height: 720 } },
296
- { label: '1920x1080', value: { width: 1920, height: 1080 } }
297
- ];
298
-
299
- const framerates = [15, 24, 30, 60];
300
- const bitrates = [
301
- { label: '500 Kbps', value: 500000 },
302
- { label: '1 Mbps', value: 1000000 },
303
- { label: '2 Mbps', value: 2000000 },
304
- { label: '5 Mbps', value: 5000000 }
305
- ];
306
-
307
- // Streaming state
308
- let streamType = $state<'camera' | 'screen'>('camera');
309
- let localVideoElement: HTMLVideoElement;
310
-
311
- // Command history
312
- let commandHistory = $state<Array<{ timestamp: string; command: string; data: any }>>([]);
313
-
314
- // Debug info
315
- let debugInfo = $state<{
316
- connectionAttempts: number;
317
- lastCommandSent: string;
318
- commandsSent: number;
319
- wsConnected: boolean;
320
- currentRoom: string;
321
- webrtcState: string;
322
- }>({
323
- connectionAttempts: 0,
324
- lastCommandSent: '',
325
- commandsSent: 0,
326
- wsConnected: false,
327
- currentRoom: '',
328
- webrtcState: 'disconnected'
329
- });
330
-
331
- // WebRTC Stats
332
- let stats = $state<videoTypes.WebRTCStats | null>(null);
333
-
334
- async function connectProducer() {
335
- if (!roomId.trim() || !participantId.trim()) {
336
- error = 'Please enter both Room ID and Participant ID';
337
- return;
338
- }
339
-
340
- debugInfo.connectionAttempts++;
341
-
342
- try {
343
- connecting = true;
344
- error = '';
345
-
346
- producer = new video.VideoProducer('http://localhost:8000');
347
-
348
- // Set up event handlers
349
- producer.onConnected(() => {
350
- connected = true;
351
- connecting = false;
352
- debugInfo.wsConnected = true;
353
- debugInfo.currentRoom = roomId;
354
- addToHistory('CONNECTED', 'Connected to room as video producer');
355
- });
356
-
357
- producer.onDisconnected(() => {
358
- connected = false;
359
- streaming = false;
360
- debugInfo.wsConnected = false;
361
- addToHistory('DISCONNECTED', 'Disconnected from room');
362
- });
363
-
364
- producer.onError((errorMsg) => {
365
- error = errorMsg;
366
- addToHistory('ERROR', errorMsg);
367
- });
368
-
369
- const success = await producer.connect(roomId, participantId);
370
- if (!success) {
371
- error = 'Failed to connect. Room might not exist or already have a producer.';
372
- connecting = false;
373
- }
374
- } catch (err) {
375
- error = `Connection failed: ${err}`;
376
- connecting = false;
377
- }
378
- }
379
-
380
- async function createNewRoom() {
381
- debugInfo.connectionAttempts++;
382
-
383
- try {
384
- connecting = true;
385
- error = '';
386
-
387
- producer = new video.VideoProducer('http://localhost:8000');
388
-
389
- // Set up event handlers
390
- producer.onConnected(() => {
391
- connected = true;
392
- connecting = false;
393
- roomId = producer.currentRoomId || '';
394
- debugInfo.wsConnected = true;
395
- debugInfo.currentRoom = roomId;
396
- addToHistory('ROOM_CREATED', `Created and connected to room ${roomId}`);
397
- });
398
-
399
- producer.onDisconnected(() => {
400
- connected = false;
401
- streaming = false;
402
- debugInfo.wsConnected = false;
403
- });
404
-
405
- producer.onError((errorMsg) => {
406
- error = errorMsg;
407
- addToHistory('ERROR', errorMsg);
408
- });
409
-
410
- const newProducer = await video.VideoProducer.createAndConnect(
411
- 'http://localhost:8000',
412
- undefined,
413
- participantId || `producer_${Date.now()}`
414
- );
415
-
416
- producer = newProducer;
417
- roomId = producer.currentRoomId || '';
418
- connected = true;
419
- connecting = false;
420
- debugInfo.wsConnected = true;
421
- debugInfo.currentRoom = roomId;
422
-
423
- addToHistory('ROOM_CREATED', `Created room ${roomId}`);
424
- } catch (err) {
425
- error = `Failed to create room: ${err}`;
426
- connecting = false;
427
- }
428
- }
429
-
430
- async function startStream() {
431
- if (!connected || !producer) return;
432
-
433
- try {
434
- let stream: MediaStream;
435
-
436
- if (streamType === 'camera') {
437
- const constraints = {
438
- video: {
439
- width: videoConfig.resolution?.width || 640,
440
- height: videoConfig.resolution?.height || 480,
441
- frameRate: videoConfig.framerate || 30
442
- },
443
- audio: false
444
- };
445
- stream = await producer.startCamera(constraints);
446
- } else {
447
- stream = await producer.startScreenShare();
448
- }
449
-
450
- // Display local stream
451
- if (localVideoElement) {
452
- localVideoElement.srcObject = stream;
453
- }
454
-
455
- streaming = true;
456
- addToHistory('STREAM_STARTED', `Started ${streamType} stream`);
457
-
458
- // Monitor WebRTC connection
459
- startStatsMonitoring();
460
- } catch (err) {
461
- error = `Failed to start ${streamType} stream: ${err}`;
462
- addToHistory('ERROR', `Failed to start ${streamType} stream`);
463
- }
464
- }
465
-
466
- async function stopStream() {
467
- if (!connected || !producer) return;
468
-
469
- try {
470
- await producer.stopStreaming();
471
- streaming = false;
472
-
473
- if (localVideoElement) {
474
- localVideoElement.srcObject = null;
475
- }
476
-
477
- addToHistory('STREAM_STOPPED', `Stopped ${streamType} stream`);
478
- } catch (err) {
479
- error = `Failed to stop stream: ${err}`;
480
- }
481
- }
482
-
483
- async function updateVideoConfig() {
484
- if (!connected || !producer) return;
485
-
486
- debugInfo.commandsSent++;
487
- debugInfo.lastCommandSent = 'VIDEO_CONFIG_UPDATE';
488
-
489
- try {
490
- await producer.updateVideoConfig(videoConfig);
491
- addToHistory('CONFIG_UPDATE', videoConfig);
492
- } catch (err) {
493
- error = `Failed to update video config: ${err}`;
494
- }
495
- }
496
-
497
- async function sendEmergencyStop() {
498
- if (!connected || !producer) return;
499
-
500
- debugInfo.commandsSent++;
501
- debugInfo.lastCommandSent = 'EMERGENCY_STOP';
502
-
503
- try {
504
- await producer.sendEmergencyStop('Manual emergency stop from video producer interface');
505
- addToHistory('EMERGENCY_STOP', 'Manual emergency stop triggered');
506
- } catch (err) {
507
- error = `Failed to send emergency stop: ${err}`;
508
- }
509
- }
510
-
511
- async function sendHeartbeat() {
512
- if (!connected || !producer) return;
513
-
514
- debugInfo.commandsSent++;
515
- debugInfo.lastCommandSent = 'HEARTBEAT';
516
-
517
- try {
518
- await producer.sendHeartbeat();
519
- addToHistory('HEARTBEAT', 'Heartbeat sent');
520
- } catch (err) {
521
- error = `Failed to send heartbeat: ${err}`;
522
- }
523
- }
524
-
525
- function startStatsMonitoring() {
526
- const interval = setInterval(async () => {
527
- if (!producer || !streaming) {
528
- clearInterval(interval);
529
- return;
530
- }
531
-
532
- try {
533
- stats = await producer.getStats();
534
- const pc = producer.getPeerConnection();
535
- if (pc) {
536
- debugInfo.webrtcState = pc.connectionState;
537
- }
538
- } catch (err) {
539
- console.error('Failed to get stats:', err);
540
- }
541
- }, 1000);
542
- }
543
-
544
- function addToHistory(command: string, data: any) {
545
- commandHistory = [
546
- {
547
- timestamp: new Date().toLocaleTimeString(),
548
- command,
549
- data
550
- },
551
- ...commandHistory
552
- ].slice(0, 10); // Keep last 10 commands only
553
- }
554
-
555
- async function exitSession() {
556
- if (producer && connected) {
557
- await producer.disconnect();
558
- }
559
- connected = false;
560
- streaming = false;
561
- debugInfo.wsConnected = false;
562
- debugInfo.currentRoom = '';
563
- }
564
-
565
- onMount(() => {
566
- participantId = `producer_${Date.now()}`;
567
-
568
- return () => {
569
- exitSession();
570
- };
571
- });
572
- </script>
573
-
574
- <svelte:head>
575
- <title>Video Producer - LeRobot Arena</title>
576
- </svelte:head>
577
-
578
- <div class="mx-auto max-w-6xl space-y-6">
579
- <!-- Header -->
580
- <div class="flex items-center justify-between">
581
- <div>
582
- <h1 class="font-mono text-2xl font-bold text-gray-900">📹 Video Producer Session</h1>
583
- <p class="mt-1 font-mono text-sm text-gray-600">
584
- Stream camera or screen with real-time WebRTC
585
- </p>
586
- </div>
587
- <div class="flex items-center space-x-4">
588
- <div class="flex items-center space-x-2">
589
- {#if connected}
590
- <div class="h-3 w-3 rounded-full bg-green-500"></div>
591
- <span class="font-mono text-sm font-medium text-green-700">Connected</span>
592
- {:else if connecting}
593
- <div class="h-3 w-3 animate-pulse rounded-full bg-yellow-500"></div>
594
- <span class="font-mono text-sm font-medium text-yellow-700">Connecting...</span>
595
- {:else}
596
- <div class="h-3 w-3 rounded-full bg-red-500"></div>
597
- <span class="font-mono text-sm font-medium text-red-700">Disconnected</span>
598
- {/if}
599
- </div>
600
- {#if streaming}
601
- <div class="flex items-center space-x-2">
602
- <div class="h-3 w-3 animate-pulse rounded-full bg-purple-500"></div>
603
- <span class="font-mono text-sm font-medium text-purple-700">Streaming</span>
604
- </div>
605
- {/if}
606
- <a
607
- href="/video"
608
- class="rounded border bg-gray-100 px-3 py-2 font-mono text-sm hover:bg-gray-200">← Back</a
609
- >
610
- </div>
611
- </div>
612
-
613
- <!-- Debug Info -->
614
- <div class="rounded border bg-gray-900 p-4 font-mono text-sm text-green-400">
615
- <div class="mb-2 font-bold">VIDEO PRODUCER DEBUG</div>
616
- <div class="grid grid-cols-3 gap-4 md:grid-cols-6">
617
- <div>Attempts: {debugInfo.connectionAttempts}</div>
618
- <div>Commands: {debugInfo.commandsSent}</div>
619
- <div>Last: {debugInfo.lastCommandSent || 'None'}</div>
620
- <div>WS: {debugInfo.wsConnected ? 'ON' : 'OFF'}</div>
621
- <div>Room: {debugInfo.currentRoom || 'None'}</div>
622
- <div>WebRTC: {debugInfo.webrtcState}</div>
623
- </div>
624
- {#if stats}
625
- <div class="mt-2 grid grid-cols-3 gap-4 md:grid-cols-6">
626
- <div>FPS: {stats.framesPerSecond.toFixed(1)}</div>
627
- <div>Bitrate: {(stats.videoBitsPerSecond / 1000).toFixed(0)}k</div>
628
- <div>Resolution: {stats.frameWidth}x{stats.frameHeight}</div>
629
- <div>Packets Lost: {stats.packetsLost}</div>
630
- <div>Total Packets: {stats.totalPackets}</div>
631
- <div>
632
- Loss Rate: {stats.totalPackets > 0
633
- ? ((stats.packetsLost / stats.totalPackets) * 100).toFixed(1)
634
- : 0}%
635
- </div>
636
- </div>
637
- {/if}
638
- {#if error}
639
- <div class="mt-2 text-red-400">Error: {error}</div>
640
- {/if}
641
- </div>
642
-
643
- {#if !connected}
644
- <!-- Connection Section -->
645
- <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
646
- <!-- Join Existing Room -->
647
- <div class="rounded border p-6">
648
- <h3 class="mb-4 font-mono text-lg font-medium">Join Existing Room</h3>
649
- <div class="space-y-4">
650
- <div>
651
- <label for="roomId" class="mb-1 block font-mono text-sm font-medium text-gray-700">
652
- Room ID
653
- </label>
654
- <input
655
- id="roomId"
656
- type="text"
657
- bind:value={roomId}
658
- placeholder="Enter room ID"
659
- class="w-full rounded border border-gray-300 px-3 py-2 font-mono focus:border-purple-500 focus:ring-purple-500"
660
- />
661
- </div>
662
- <div>
663
- <label
664
- for="participantId"
665
- class="mb-1 block font-mono text-sm font-medium text-gray-700"
666
- >
667
- Participant ID
668
- </label>
669
- <input
670
- id="participantId"
671
- type="text"
672
- bind:value={participantId}
673
- placeholder="Your participant ID"
674
- class="w-full rounded border border-gray-300 px-3 py-2 font-mono focus:border-purple-500 focus:ring-purple-500"
675
- />
676
- </div>
677
- <button
678
- onclick={connectProducer}
679
- disabled={connecting || !roomId.trim() || !participantId.trim()}
680
- class={[
681
- 'w-full rounded border px-4 py-2 font-mono',
682
- connecting || !roomId.trim() || !participantId.trim()
683
- ? 'bg-gray-200 text-gray-500'
684
- : 'bg-purple-600 text-white hover:bg-purple-700'
685
- ]}
686
- >
687
- {connecting ? 'Connecting...' : 'Join as Producer'}
688
- </button>
689
- </div>
690
- </div>
691
-
692
- <!-- Create New Room -->
693
- <div class="rounded border p-6">
694
- <h3 class="mb-4 font-mono text-lg font-medium">Create New Room</h3>
695
- <div class="space-y-4">
696
- <p class="font-mono text-sm text-gray-600">Create a new room and connect as producer</p>
697
- <div>
698
- <label
699
- for="newParticipantId"
700
- class="mb-1 block font-mono text-sm font-medium text-gray-700"
701
- >
702
- Participant ID (optional)
703
- </label>
704
- <input
705
- id="newParticipantId"
706
- type="text"
707
- bind:value={participantId}
708
- placeholder="Auto-generated if empty"
709
- class="w-full rounded border border-gray-300 px-3 py-2 font-mono focus:border-purple-500 focus:ring-purple-500"
710
- />
711
- </div>
712
- <button
713
- onclick={createNewRoom}
714
- disabled={connecting}
715
- class={[
716
- 'w-full rounded border px-4 py-2 font-mono',
717
- connecting
718
- ? 'bg-gray-200 text-gray-500'
719
- : 'bg-green-600 text-white hover:bg-green-700'
720
- ]}
721
- >
722
- {connecting ? 'Creating...' : 'Create New Room'}
723
- </button>
724
- </div>
725
- </div>
726
- </div>
727
-
728
- {#if error}
729
- <div class="rounded border border-red-200 bg-red-50 p-4">
730
- <p class="font-mono text-sm text-red-700">{error}</p>
731
- </div>
732
- {/if}
733
- {:else}
734
- <!-- Control Interface -->
735
- <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
736
- <!-- Video Controls -->
737
- <div class="space-y-6 lg:col-span-2">
738
- <!-- Connection Info -->
739
- <div class="rounded border p-4">
740
- <h2 class="mb-3 font-mono text-lg font-semibold">Session Info</h2>
741
- <div class="grid grid-cols-3 gap-4 font-mono text-sm">
742
- <div><span class="text-gray-500">Room:</span> {roomId}</div>
743
- <div><span class="text-gray-500">ID:</span> {participantId}</div>
744
- <div><span class="text-gray-500">Role:</span> Producer</div>
745
- </div>
746
- </div>
747
-
748
- <!-- Stream Controls -->
749
- <div class="rounded border p-6">
750
- <div class="mb-4 flex items-center justify-between">
751
- <h2 class="font-mono text-lg font-semibold">Stream Control</h2>
752
- <div class="flex space-x-2">
753
- <button
754
- onclick={() => (streamType = 'camera')}
755
- class={[
756
- 'rounded border px-3 py-1 font-mono text-sm',
757
- streamType === 'camera' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100'
758
- ]}
759
- >
760
- 📹 Camera
761
- </button>
762
- <button
763
- onclick={() => (streamType = 'screen')}
764
- class={[
765
- 'rounded border px-3 py-1 font-mono text-sm',
766
- streamType === 'screen' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100'
767
- ]}
768
- >
769
- 🖥️ Screen
770
- </button>
771
- </div>
772
- </div>
773
-
774
- <!-- Local Video Preview -->
775
- <div class="mb-4">
776
- <div class="aspect-video rounded border bg-black">
777
- <video
778
- bind:this={localVideoElement}
779
- autoplay
780
- muted
781
- playsinline
782
- class="h-full w-full rounded object-contain"
783
- >
784
- <track kind="captions" />
785
- </video>
786
- </div>
787
- <p class="mt-2 text-center font-mono text-sm text-gray-500">
788
- Local {streamType} preview
789
- </p>
790
- </div>
791
-
792
- <!-- Stream Actions -->
793
- <div class="grid grid-cols-2 gap-3">
794
- {#if !streaming}
795
- <button
796
- onclick={startStream}
797
- class="rounded border bg-purple-600 px-4 py-2 font-mono text-white hover:bg-purple-700"
798
- >
799
- ▶️ Start {streamType === 'camera' ? 'Camera' : 'Screen Share'}
800
- </button>
801
- {:else}
802
- <button
803
- onclick={stopStream}
804
- class="rounded border bg-red-600 px-4 py-2 font-mono text-white hover:bg-red-700"
805
- >
806
- ⏹️ Stop Stream
807
- </button>
808
- {/if}
809
- <button
810
- onclick={updateVideoConfig}
811
- class="rounded border bg-blue-600 px-4 py-2 font-mono text-white hover:bg-blue-700"
812
- >
813
- ⚙️ Update Config
814
- </button>
815
- </div>
816
- </div>
817
-
818
- <!-- Video Configuration -->
819
- <div class="rounded border p-6">
820
- <h2 class="mb-4 font-mono text-lg font-semibold">Video Configuration</h2>
821
- <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
822
- <!-- Resolution -->
823
- <div>
824
- <label class="mb-2 block font-mono text-sm font-medium text-gray-700">
825
- Resolution
826
- </label>
827
- <select
828
- bind:value={videoConfig.resolution}
829
- class="w-full rounded border border-gray-300 px-3 py-2 font-mono focus:border-purple-500 focus:ring-purple-500"
830
- >
831
- {#each resolutions as res}
832
- <option value={res.value}>{res.label}</option>
833
- {/each}
834
- </select>
835
- </div>
836
-
837
- <!-- Framerate -->
838
- <div>
839
- <label class="mb-2 block font-mono text-sm font-medium text-gray-700">
840
- Framerate (FPS)
841
- </label>
842
- <select
843
- bind:value={videoConfig.framerate}
844
- class="w-full rounded border border-gray-300 px-3 py-2 font-mono focus:border-purple-500 focus:ring-purple-500"
845
- >
846
- {#each framerates as fps}
847
- <option value={fps}>{fps} FPS</option>
848
- {/each}
849
- </select>
850
- </div>
851
-
852
- <!-- Bitrate -->
853
- <div>
854
- <label class="mb-2 block font-mono text-sm font-medium text-gray-700">
855
- Bitrate
856
- </label>
857
- <select
858
- bind:value={videoConfig.bitrate}
859
- class="w-full rounded border border-gray-300 px-3 py-2 font-mono focus:border-purple-500 focus:ring-purple-500"
860
- >
861
- {#each bitrates as br}
862
- <option value={br.value}>{br.label}</option>
863
- {/each}
864
- </select>
865
- </div>
866
-
867
- <!-- Quality -->
868
- <div>
869
- <label class="mb-2 block font-mono text-sm font-medium text-gray-700">
870
- Quality: {videoConfig.quality}%
871
- </label>
872
- <input
873
- type="range"
874
- min="10"
875
- max="100"
876
- step="10"
877
- bind:value={videoConfig.quality}
878
- class="w-full"
879
- />
880
- </div>
881
- </div>
882
- </div>
883
-
884
- <!-- Control Actions -->
885
- <div class="rounded border p-4">
886
- <h2 class="mb-3 font-mono text-lg font-semibold">Actions</h2>
887
- <div class="grid grid-cols-2 gap-3">
888
- <button
889
- onclick={sendHeartbeat}
890
- class="rounded border bg-gray-600 px-4 py-2 font-mono text-white hover:bg-gray-700"
891
- >
892
- 💓 Heartbeat
893
- </button>
894
- <button
895
- onclick={sendEmergencyStop}
896
- class="rounded border bg-red-600 px-4 py-2 font-mono text-white hover:bg-red-700"
897
- >
898
- 🚨 E-Stop
899
- </button>
900
- </div>
901
- </div>
902
- </div>
903
-
904
- <!-- Status & History -->
905
- <div class="space-y-6">
906
- <!-- Current Status -->
907
- <div class="rounded border p-4">
908
- <h2 class="mb-3 font-mono text-lg font-semibold">Current Status</h2>
909
- <div class="space-y-2 font-mono text-sm">
910
- <div class="flex justify-between">
911
- <span class="text-gray-600">Stream Type:</span>
912
- <span class="font-bold capitalize">{streamType}</span>
913
- </div>
914
- <div class="flex justify-between">
915
- <span class="text-gray-600">Resolution:</span>
916
- <span class="font-bold"
917
- >{videoConfig.resolution?.width}x{videoConfig.resolution?.height}</span
918
- >
919
- </div>
920
- <div class="flex justify-between">
921
- <span class="text-gray-600">Framerate:</span>
922
- <span class="font-bold">{videoConfig.framerate} FPS</span>
923
- </div>
924
- <div class="flex justify-between">
925
- <span class="text-gray-600">Bitrate:</span>
926
- <span class="font-bold">{((videoConfig.bitrate || 0) / 1000000).toFixed(1)} Mbps</span
927
- >
928
- </div>
929
- <div class="flex justify-between">
930
- <span class="text-gray-600">Quality:</span>
931
- <span class="font-bold">{videoConfig.quality}%</span>
932
- </div>
933
- </div>
934
- </div>
935
-
936
- <!-- Command History -->
937
- <div class="rounded border p-4">
938
- <h2 class="mb-3 font-mono text-lg font-semibold">Recent Commands</h2>
939
- <div class="max-h-64 space-y-2 overflow-y-auto">
940
- {#each commandHistory as command}
941
- <div class="rounded border-l-4 border-purple-200 bg-gray-50 p-2">
942
- <div class="flex items-center justify-between">
943
- <span class="font-mono text-xs text-gray-500">{command.timestamp}</span>
944
- <span
945
- class={[
946
- 'font-mono text-xs font-medium',
947
- command.command === 'ERROR' || command.command === 'EMERGENCY_STOP'
948
- ? 'text-red-600'
949
- : 'text-purple-600'
950
- ]}
951
- >
952
- {command.command}
953
- </span>
954
- </div>
955
- {#if typeof command.data === 'string'}
956
- <div class="mt-1 font-mono text-xs text-gray-600">
957
- {command.data}
958
- </div>
959
- {:else if command.command === 'CONFIG_UPDATE'}
960
- <div class="mt-1 font-mono text-xs text-gray-600">
961
- {command.data.resolution?.width}x{command.data.resolution?.height} @ {command
962
- .data.framerate}fps
963
- </div>
964
- {/if}
965
- </div>
966
- {:else}
967
- <p class="py-4 text-center font-mono text-sm text-gray-500">No commands sent yet</p>
968
- {/each}
969
- </div>
970
- </div>
971
-
972
- <!-- Session Control -->
973
- <div class="rounded border p-4">
974
- <h2 class="mb-3 font-mono text-lg font-semibold">Session Control</h2>
975
- <button
976
- onclick={exitSession}
977
- class="w-full rounded border bg-gray-100 px-4 py-2 font-mono hover:bg-gray-200"
978
- >
979
- 🚪 Exit Session
980
- </button>
981
- </div>
982
- </div>
983
- </div>
984
- {/if}
985
- </div>
986
-
987
- <script lang="ts">
988
- import { page } from '$app/stores';
989
- import { onMount } from 'svelte';
990
- import { video } from 'lerobot-arena-client';
991
- import type { video as videoTypes } from 'lerobot-arena-client';
992
-
993
- // Get room ID from URL
994
- let roomIdFromUrl = $derived($page.params.room_id);
995
-
996
- // State
997
- let consumer: video.VideoConsumer;
998
- let connected = $state<boolean>(false);
999
- let connecting = $state<boolean>(false);
1000
- let receiving = $state<boolean>(false);
1001
- let roomId = $state<string>(roomIdFromUrl || '');
1002
- let participantId = $state<string>('');
1003
- let error = $state<string>('');
1004
-
1005
- // Video elements
1006
- let remoteVideoElement: HTMLVideoElement;
1007
-
1008
- // Current video state
1009
- let currentConfig = $state<videoTypes.VideoConfig>({});
1010
- let lastUpdate = $state<Date | null>(null);
1011
- let currentStrategy = $state<videoTypes.CommunicationStrategy | null>(null);
1012
- let strategyInfo = $state<videoTypes.StrategyInfo | null>(null);
1013
-
1014
- // Real-time monitoring
1015
- let frameCount = $state<number>(0);
1016
- let configUpdateCount = $state<number>(0);
1017
- let errorCount = $state<number>(0);
1018
- let strategyChangeCount = $state<number>(0);
1019
-
1020
- // History tracking
1021
- let frameHistory = $state<
1022
- Array<{ timestamp: string; frame: videoTypes.FrameData; source?: string }>
1023
- >([]);
1024
- let configHistory = $state<Array<{ timestamp: string; config: videoTypes.VideoConfig }>>([]);
1025
- let streamHistory = $state<
1026
- Array<{ timestamp: string; event: string; participant?: string; reason?: string }>
1027
- >([]);
1028
- let errorHistory = $state<Array<{ timestamp: string; message: string }>>([]);
1029
- let strategyHistory = $state<
1030
- Array<{ timestamp: string; oldStrategy: string; newStrategy: string; reason: string }>
1031
- >([]);
1032
-
1033
- // WebRTC Stats
1034
- let stats = $state<videoTypes.WebRTCStats | null>(null);
1035
-
1036
- // Strategy management
1037
- let availableStrategies: videoTypes.CommunicationStrategy[] = ['p2p', 'server', 'hybrid'];
1038
- let selectedStrategy = $state<videoTypes.CommunicationStrategy>('p2p');
1039
-
1040
- // Debug info
1041
- let debugInfo = $state<{
1042
- connectionAttempts: number;
1043
- messagesReceived: number;
1044
- lastMessageType: string;
1045
- wsConnected: boolean;
1046
- currentRoom: string;
1047
- webrtcState: string;
1048
- currentStrategy: string;
1049
- }>({
1050
- connectionAttempts: 0,
1051
- messagesReceived: 0,
1052
- lastMessageType: '',
1053
- wsConnected: false,
1054
- currentRoom: '',
1055
- webrtcState: 'disconnected',
1056
- currentStrategy: 'unknown'
1057
- });
1058
-
1059
- async function connectConsumer() {
1060
- if (!roomId.trim() || !participantId.trim()) {
1061
- error = 'Please enter both Room ID and Participant ID';
1062
- return;
1063
- }
1064
-
1065
- debugInfo.connectionAttempts++;
1066
-
1067
- try {
1068
- connecting = true;
1069
- error = '';
1070
-
1071
- consumer = new video.VideoConsumer('http://localhost:8000');
1072
-
1073
- // Set up event handlers
1074
- consumer.onConnected(() => {
1075
- connected = true;
1076
- connecting = false;
1077
- debugInfo.wsConnected = true;
1078
- debugInfo.currentRoom = roomId;
1079
- currentStrategy = consumer.getCurrentStrategy();
1080
- debugInfo.currentStrategy = currentStrategy || 'unknown';
1081
- loadInitialState();
1082
- });
1083
-
1084
- consumer.onDisconnected(() => {
1085
- connected = false;
1086
- receiving = false;
1087
- debugInfo.wsConnected = false;
1088
- currentStrategy = null;
1089
- debugInfo.currentStrategy = 'unknown';
1090
- });
1091
-
1092
- consumer.onError((errorMsg) => {
1093
- error = errorMsg;
1094
- errorCount++;
1095
- debugInfo.messagesReceived++;
1096
- debugInfo.lastMessageType = 'ERROR';
1097
- errorHistory = [
1098
- {
1099
- timestamp: new Date().toLocaleTimeString(),
1100
- message: errorMsg
1101
- },
1102
- ...errorHistory
1103
- ].slice(0, 10); // Keep last 10 errors only
1104
- });
1105
-
1106
- // Strategy change callback
1107
- consumer.onStrategyChanged((oldStrategy, newStrategy, reason) => {
1108
- strategyChangeCount++;
1109
- debugInfo.messagesReceived++;
1110
- debugInfo.lastMessageType = 'STRATEGY_CHANGED';
1111
- currentStrategy = newStrategy;
1112
- debugInfo.currentStrategy = newStrategy;
1113
-
1114
- // Add to history
1115
- strategyHistory = [
1116
- {
1117
- timestamp: new Date().toLocaleTimeString(),
1118
- oldStrategy,
1119
- newStrategy,
1120
- reason
1121
- },
1122
- ...strategyHistory
1123
- ].slice(0, 10);
1124
-
1125
- streamHistory = [
1126
- {
1127
- timestamp: new Date().toLocaleTimeString(),
1128
- event: 'STRATEGY_CHANGED',
1129
- reason: `${oldStrategy} → ${newStrategy}: ${reason}`
1130
- },
1131
- ...streamHistory
1132
- ].slice(0, 10);
1133
- });
1134
-
1135
- // P2P status callback
1136
- consumer.onP2PStatus((participantId, status, details) => {
1137
- debugInfo.messagesReceived++;
1138
- debugInfo.lastMessageType = 'P2P_STATUS';
1139
-
1140
- streamHistory = [
1141
- {
1142
- timestamp: new Date().toLocaleTimeString(),
1143
- event: 'P2P_STATUS',
1144
- participant: participantId,
1145
- reason: status
1146
- },
1147
- ...streamHistory
1148
- ].slice(0, 10);
1149
- });
1150
-
1151
- // Frame update callback
1152
- consumer.onFrameUpdate((frame) => {
1153
- frameCount++;
1154
- lastUpdate = new Date();
1155
- debugInfo.messagesReceived++;
1156
- debugInfo.lastMessageType = 'FRAME_UPDATE';
1157
-
1158
- // Add to history
1159
- frameHistory = [
1160
- {
1161
- timestamp: new Date().toLocaleTimeString(),
1162
- frame,
1163
- source: 'producer'
1164
- },
1165
- ...frameHistory
1166
- ].slice(0, 20); // Keep last 20 frames only
1167
- });
1168
-
1169
- // Video config update callback
1170
- consumer.onVideoConfigUpdate((config) => {
1171
- configUpdateCount++;
1172
- lastUpdate = new Date();
1173
- debugInfo.messagesReceived++;
1174
- debugInfo.lastMessageType = 'CONFIG_UPDATE';
1175
-
1176
- // Update current config
1177
- currentConfig = { ...config };
1178
-
1179
- // Add to history
1180
- configHistory = [
1181
- {
1182
- timestamp: new Date().toLocaleTimeString(),
1183
- config
1184
- },
1185
- ...configHistory
1186
- ].slice(0, 10); // Keep last 10 config updates only
1187
- });
1188
-
1189
- // Stream events
1190
- consumer.onStreamStarted((config, participantId, strategy) => {
1191
- receiving = true;
1192
- debugInfo.messagesReceived++;
1193
- debugInfo.lastMessageType = 'STREAM_STARTED';
1194
- currentStrategy = strategy;
1195
- debugInfo.currentStrategy = strategy;
1196
-
1197
- streamHistory = [
1198
- {
1199
- timestamp: new Date().toLocaleTimeString(),
1200
- event: 'STREAM_STARTED',
1201
- participant: participantId,
1202
- reason: `Using ${strategy} strategy`
1203
- },
1204
- ...streamHistory
1205
- ].slice(0, 10);
1206
-
1207
- // Start receiving
1208
- startReceiving();
1209
- });
1210
-
1211
- consumer.onStreamStopped((participantId, reason) => {
1212
- receiving = false;
1213
- debugInfo.messagesReceived++;
1214
- debugInfo.lastMessageType = 'STREAM_STOPPED';
1215
-
1216
- streamHistory = [
1217
- {
1218
- timestamp: new Date().toLocaleTimeString(),
1219
- event: 'STREAM_STOPPED',
1220
- participant: participantId,
1221
- reason
1222
- },
1223
- ...streamHistory
1224
- ].slice(0, 10);
1225
- });
1226
-
1227
- consumer.onRecoveryTriggered((policy, reason) => {
1228
- debugInfo.messagesReceived++;
1229
- debugInfo.lastMessageType = 'RECOVERY_TRIGGERED';
1230
-
1231
- streamHistory = [
1232
- {
1233
- timestamp: new Date().toLocaleTimeString(),
1234
- event: 'RECOVERY_TRIGGERED',
1235
- reason: `${policy}: ${reason}`
1236
- },
1237
- ...streamHistory
1238
- ].slice(0, 10);
1239
- });
1240
-
1241
- // Set up remote stream handler
1242
- consumer.on('remoteStream', (stream: MediaStream | null) => {
1243
- console.info(
1244
- '🎬 Svelte: Received remoteStream event, stream:',
1245
- stream ? `available (id: ${stream.id})` : 'null'
1246
- );
1247
-
1248
- if (remoteVideoElement) {
1249
- remoteVideoElement.srcObject = stream;
1250
- }
1251
- receiving = true;
1252
- startStatsMonitoring();
1253
- });
1254
-
1255
- const success = await consumer.connect(roomId, participantId);
1256
- if (!success) {
1257
- error = 'Failed to connect. Room might not exist.';
1258
- connecting = false;
1259
- }
1260
- } catch (err) {
1261
- error = `Connection failed: ${err}`;
1262
- connecting = false;
1263
- }
1264
- }
1265
-
1266
- async function startReceiving() {
1267
- if (!consumer || !connected) return;
1268
-
1269
- try {
1270
- await consumer.startReceiving();
1271
-
1272
- // Attach to video element
1273
- if (remoteVideoElement) {
1274
- consumer.attachToVideoElement(remoteVideoElement);
1275
- }
1276
- } catch (err) {
1277
- console.error('Failed to start receiving:', err);
1278
- }
1279
- }
1280
-
1281
- async function loadInitialState() {
1282
- if (!consumer || !connected) return;
1283
-
1284
- try {
1285
- const roomState = await consumer.getRoomState(roomId);
1286
- if (roomState.current_config) {
1287
- currentConfig = roomState.current_config;
1288
- }
1289
- if (roomState.active_strategy) {
1290
- currentStrategy = roomState.active_strategy;
1291
- debugInfo.currentStrategy = roomState.active_strategy;
1292
- }
1293
-
1294
- // Get strategy info
1295
- try {
1296
- strategyInfo = await consumer.getStrategyInfo(roomId);
1297
- } catch (err) {
1298
- console.warn('Failed to get strategy info:', err);
1299
- }
1300
- } catch (err) {
1301
- console.error('Failed to load initial state:', err);
1302
- }
1303
- }
1304
-
1305
- async function switchStrategy() {
1306
- if (!consumer || !connected || !selectedStrategy) return;
1307
-
1308
- try {
1309
- await consumer.switchToStrategy(selectedStrategy, 'User requested via UI');
1310
- } catch (err) {
1311
- error = `Failed to switch strategy: ${err}`;
1312
- console.error('Failed to switch strategy:', err);
1313
- }
1314
- }
1315
-
1316
- function startStatsMonitoring() {
1317
- const interval = setInterval(async () => {
1318
- if (!consumer || !receiving) {
1319
- clearInterval(interval);
1320
- return;
1321
- }
1322
-
1323
- try {
1324
- stats = await consumer.getVideoStats();
1325
- const pc = consumer.getPeerConnection();
1326
- if (pc) {
1327
- debugInfo.webrtcState = pc.connectionState;
1328
- }
1329
- } catch (err) {
1330
- console.error('Failed to get video stats:', err);
1331
- }
1332
- }, 1000);
1333
- }
1334
-
1335
- async function exitSession() {
1336
- if (consumer && connected) {
1337
- await consumer.disconnect();
1338
- }
1339
- connected = false;
1340
- receiving = false;
1341
- debugInfo.wsConnected = false;
1342
- debugInfo.currentRoom = '';
1343
- }
1344
-
1345
- function clearHistory() {
1346
- frameHistory = [];
1347
- configHistory = [];
1348
- streamHistory = [];
1349
- errorHistory = [];
1350
- strategyHistory = [];
1351
- frameCount = 0;
1352
- configUpdateCount = 0;
1353
- errorCount = 0;
1354
- strategyChangeCount = 0;
1355
- debugInfo.messagesReceived = 0;
1356
- }
1357
-
1358
- // Update roomId when URL parameter changes
1359
- $effect(() => {
1360
- if (roomIdFromUrl) {
1361
- roomId = roomIdFromUrl;
1362
- }
1363
- });
1364
-
1365
- onMount(() => {
1366
- participantId = `consumer_${Date.now()}`;
1367
-
1368
- return () => {
1369
- exitSession();
1370
- };
1371
- });
1372
- </script>
1373
-
1374
- <svelte:head>
1375
- <title>Video Consumer - Room {roomId} - LeRobot Arena</title>
1376
- </svelte:head>
1377
-
1378
- <div class="mx-auto max-w-6xl space-y-6">
1379
- <!-- Header -->
1380
- <div class="flex items-center justify-between">
1381
- <div>
1382
- <h1 class="font-mono text-2xl font-bold text-gray-900">📺 Video Consumer</h1>
1383
- <p class="mt-1 font-mono text-sm text-gray-600">
1384
- Room: <span class="font-bold text-indigo-600">{roomId}</span>
1385
- </p>
1386
- </div>
1387
- <div class="flex items-center space-x-4">
1388
- <div class="flex items-center space-x-2">
1389
- {#if connected}
1390
- <div class="h-3 w-3 rounded-full bg-green-500"></div>
1391
- <span class="font-mono text-sm font-medium text-green-700">Connected</span>
1392
- {:else if connecting}
1393
- <div class="h-3 w-3 animate-pulse rounded-full bg-yellow-500"></div>
1394
- <span class="font-mono text-sm font-medium text-yellow-700">Connecting...</span>
1395
- {:else}
1396
- <div class="h-3 w-3 rounded-full bg-red-500"></div>
1397
- <span class="font-mono text-sm font-medium text-red-700">Disconnected</span>
1398
- {/if}
1399
- </div>
1400
- {#if receiving}
1401
- <div class="flex items-center space-x-2">
1402
- <div class="h-3 w-3 animate-pulse rounded-full bg-indigo-500"></div>
1403
- <span class="font-mono text-sm font-medium text-indigo-700">Receiving</span>
1404
- </div>
1405
- {/if}
1406
- <a
1407
- href="/video"
1408
- class="rounded border bg-gray-100 px-3 py-2 font-mono text-sm hover:bg-gray-200"
1409
- >← Back to Video</a
1410
- >
1411
- </div>
1412
- </div>
1413
-
1414
- <!-- Debug Info -->
1415
- <div class="rounded border bg-gray-900 p-4 font-mono text-sm text-green-400">
1416
- <div class="mb-2 font-bold">VIDEO CONSUMER DEBUG - ROOM {roomId}</div>
1417
- <div class="grid grid-cols-3 gap-4 md:grid-cols-6">
1418
- <div>Attempts: {debugInfo.connectionAttempts}</div>
1419
- <div>Messages: {debugInfo.messagesReceived}</div>
1420
- <div>Last: {debugInfo.lastMessageType || 'None'}</div>
1421
- <div>WS: {debugInfo.wsConnected ? 'ON' : 'OFF'}</div>
1422
- <div>Room: {debugInfo.currentRoom || 'None'}</div>
1423
- <div>WebRTC: {debugInfo.webrtcState}</div>
1424
- </div>
1425
- <div class="mt-2 grid grid-cols-4 gap-4">
1426
- <div>Frames: {frameCount}</div>
1427
- <div>Config Updates: {configUpdateCount}</div>
1428
- <div>Strategy Changes: {strategyChangeCount}</div>
1429
- <div>Errors: {errorCount}</div>
1430
- </div>
1431
- <div class="mt-2 grid grid-cols-1 gap-4">
1432
- <div>
1433
- Current Strategy:
1434
- {#if currentStrategy}
1435
- <span
1436
- class="inline-block rounded px-2 py-1 text-xs {currentStrategy === 'p2p'
1437
- ? 'bg-blue-600 text-white'
1438
- : currentStrategy === 'server'
1439
- ? 'bg-orange-600 text-white'
1440
- : currentStrategy === 'hybrid'
1441
- ? 'bg-purple-600 text-white'
1442
- : 'bg-gray-600 text-white'}">{currentStrategy.toUpperCase()}</span
1443
- >
1444
- {:else}
1445
- <span class="text-gray-400">Unknown</span>
1446
- {/if}
1447
- </div>
1448
- </div>
1449
- {#if stats}
1450
- <div class="mt-2 grid grid-cols-3 gap-4 md:grid-cols-6">
1451
- <div>FPS: {stats.framesPerSecond.toFixed(1)}</div>
1452
- <div>Bitrate: {(stats.videoBitsPerSecond / 1000).toFixed(0)}k</div>
1453
- <div>Resolution: {stats.frameWidth}x{stats.frameHeight}</div>
1454
- <div>Packets Lost: {stats.packetsLost}</div>
1455
- <div>Total Packets: {stats.totalPackets}</div>
1456
- <div>
1457
- Loss Rate: {stats.totalPackets > 0
1458
- ? ((stats.packetsLost / stats.totalPackets) * 100).toFixed(1)
1459
- : 0}%
1460
- </div>
1461
- </div>
1462
- {/if}
1463
- <div class="mt-2">Last Update: {lastUpdate ? lastUpdate.toLocaleTimeString() : 'Never'}</div>
1464
- {#if error}
1465
- <div class="mt-2 text-red-400">Error: {error}</div>
1466
- {/if}
1467
- </div>
1468
-
1469
- {#if !connected}
1470
- <!-- Connection Section -->
1471
- <div class="rounded border p-6">
1472
- <h2 class="mb-4 font-mono text-lg font-semibold">Connect to Video Room {roomId}</h2>
1473
-
1474
- <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
1475
- <div>
1476
- <label for="roomId" class="mb-1 block font-mono text-sm font-medium text-gray-700">
1477
- Room ID (from URL)
1478
- </label>
1479
- <input
1480
- id="roomId"
1481
- type="text"
1482
- bind:value={roomId}
1483
- readonly
1484
- class="w-full rounded border border-gray-300 bg-gray-50 px-3 py-2 font-mono text-gray-600"
1485
- />
1486
- </div>
1487
- <div>
1488
- <label for="participantId" class="mb-1 block font-mono text-sm font-medium text-gray-700">
1489
- Participant ID
1490
- </label>
1491
- <input
1492
- id="participantId"
1493
- type="text"
1494
- bind:value={participantId}
1495
- placeholder="Your participant ID"
1496
- class="w-full rounded border border-gray-300 px-3 py-2 font-mono focus:border-indigo-500 focus:ring-indigo-500"
1497
- />
1498
- </div>
1499
- </div>
1500
-
1501
- <div class="mt-4">
1502
- <button
1503
- onclick={connectConsumer}
1504
- disabled={connecting || !roomId.trim() || !participantId.trim()}
1505
- class={[
1506
- 'rounded border px-4 py-2 font-mono',
1507
- connecting || !roomId.trim() || !participantId.trim()
1508
- ? 'bg-gray-200 text-gray-500'
1509
- : 'bg-indigo-600 text-white hover:bg-indigo-700'
1510
- ]}
1511
- >
1512
- {connecting ? 'Connecting...' : 'Join as Viewer'}
1513
- </button>
1514
- </div>
1515
-
1516
- {#if error}
1517
- <div class="mt-4 rounded border border-red-200 bg-red-50 p-4">
1518
- <p class="font-mono text-sm text-red-700">{error}</p>
1519
- </div>
1520
- {/if}
1521
- </div>
1522
- {:else}
1523
- <!-- Monitoring Interface -->
1524
- <div class="space-y-6">
1525
- <!-- Status Overview -->
1526
- <div class="grid grid-cols-2 gap-4 md:grid-cols-4">
1527
- <div class="rounded border p-4 text-center">
1528
- <div class="font-mono text-2xl font-bold text-indigo-600">{frameCount}</div>
1529
- <div class="font-mono text-sm text-gray-500">Frames Received</div>
1530
- </div>
1531
- <div class="rounded border p-4 text-center">
1532
- <div class="font-mono text-2xl font-bold text-purple-600">{configUpdateCount}</div>
1533
- <div class="font-mono text-sm text-gray-500">Config Updates</div>
1534
- </div>
1535
- <div class="rounded border p-4 text-center">
1536
- <div class="font-mono text-2xl font-bold text-blue-600">{strategyChangeCount}</div>
1537
- <div class="font-mono text-sm text-gray-500">Strategy Changes</div>
1538
- </div>
1539
- <div class="rounded border p-4 text-center">
1540
- <div class="font-mono text-2xl font-bold text-red-600">{errorCount}</div>
1541
- <div class="font-mono text-sm text-gray-500">Errors</div>
1542
- </div>
1543
- </div>
1544
-
1545
- <!-- Video Display & Controls -->
1546
- <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
1547
- <!-- Video Stream -->
1548
- <div class="rounded border p-4 lg:col-span-2">
1549
- <h2 class="mb-3 font-mono text-lg font-semibold">Live Video Stream</h2>
1550
-
1551
- <div class="relative aspect-video rounded border bg-black">
1552
- <video
1553
- bind:this={remoteVideoElement}
1554
- autoplay
1555
- playsinline
1556
- controls
1557
- class="h-full w-full rounded object-contain"
1558
- >
1559
- <track kind="captions" />
1560
- </video>
1561
-
1562
- {#if !receiving}
1563
- <div class="absolute inset-0 flex items-center justify-center">
1564
- <div class="text-center text-white">
1565
- <div class="mb-2 text-4xl">📺</div>
1566
- <p class="font-mono">Waiting for video stream...</p>
1567
- <p class="font-mono text-sm">Producer needs to start streaming</p>
1568
- </div>
1569
- </div>
1570
- {/if}
1571
- </div>
1572
-
1573
- <!-- Current Video Config -->
1574
- {#if Object.keys(currentConfig).length > 0}
1575
- <div class="mt-4 grid grid-cols-2 gap-4 md:grid-cols-4">
1576
- <div class="text-center">
1577
- <div class="font-mono text-sm text-gray-500">Resolution</div>
1578
- <div class="font-mono text-lg font-bold">
1579
- {currentConfig.resolution?.width || '?'}x{currentConfig.resolution?.height || '?'}
1580
- </div>
1581
- </div>
1582
- <div class="text-center">
1583
- <div class="font-mono text-sm text-gray-500">Framerate</div>
1584
- <div class="font-mono text-lg font-bold">{currentConfig.framerate || '?'} FPS</div>
1585
- </div>
1586
- <div class="text-center">
1587
- <div class="font-mono text-sm text-gray-500">Bitrate</div>
1588
- <div class="font-mono text-lg font-bold">
1589
- {currentConfig.bitrate
1590
- ? (currentConfig.bitrate / 1000000).toFixed(1) + ' Mbps'
1591
- : '?'}
1592
- </div>
1593
- </div>
1594
- <div class="text-center">
1595
- <div class="font-mono text-sm text-gray-500">Encoding</div>
1596
- <div class="font-mono text-lg font-bold uppercase">
1597
- {currentConfig.encoding || '?'}
1598
- </div>
1599
- </div>
1600
- </div>
1601
- {/if}
1602
- </div>
1603
-
1604
- <!-- Session Info & Controls -->
1605
- <div class="space-y-6">
1606
- <!-- Session Info -->
1607
- <div class="rounded border p-4">
1608
- <h2 class="mb-3 font-mono text-lg font-semibold">Session Info</h2>
1609
- <div class="grid grid-cols-1 gap-2 font-mono text-sm">
1610
- <div><span class="text-gray-500">Room:</span> {roomId}</div>
1611
- <div><span class="text-gray-500">ID:</span> {participantId}</div>
1612
- <div><span class="text-gray-500">Role:</span> Consumer</div>
1613
- <div><span class="text-gray-500">Receiving:</span> {receiving ? 'Yes' : 'No'}</div>
1614
- <div>
1615
- <span class="text-gray-500">Strategy:</span>
1616
- {#if currentStrategy}
1617
- <span
1618
- class="inline-block rounded px-2 py-1 text-xs {currentStrategy === 'p2p'
1619
- ? 'bg-blue-600 text-white'
1620
- : currentStrategy === 'server'
1621
- ? 'bg-orange-600 text-white'
1622
- : currentStrategy === 'hybrid'
1623
- ? 'bg-purple-600 text-white'
1624
- : 'bg-gray-600 text-white'}">{currentStrategy.toUpperCase()}</span
1625
- >
1626
- {:else}
1627
- <span class="text-gray-500">Unknown</span>
1628
- {/if}
1629
- </div>
1630
- <div>
1631
- <span class="text-gray-500">Last Update:</span>
1632
- {lastUpdate ? lastUpdate.toLocaleTimeString() : 'Never'}
1633
- </div>
1634
- </div>
1635
- <div class="mt-4 flex space-x-3">
1636
- <button
1637
- onclick={clearHistory}
1638
- class="rounded border bg-gray-100 px-3 py-2 font-mono text-sm hover:bg-gray-200"
1639
- >
1640
- Clear History
1641
- </button>
1642
- <button
1643
- onclick={exitSession}
1644
- class="rounded border bg-gray-100 px-3 py-2 font-mono text-sm hover:bg-gray-200"
1645
- >
1646
- 🚪 Exit Session
1647
- </button>
1648
- </div>
1649
- </div>
1650
-
1651
- <!-- Strategy Management -->
1652
- <div class="rounded border p-4">
1653
- <h2 class="mb-3 font-mono text-lg font-semibold">Strategy Management</h2>
1654
- <div class="space-y-4">
1655
- <div>
1656
- <label
1657
- for="strategySelect"
1658
- class="mb-1 block font-mono text-sm font-medium text-gray-700"
1659
- >
1660
- Switch Strategy
1661
- </label>
1662
- <div class="flex gap-2">
1663
- <select
1664
- id="strategySelect"
1665
- bind:value={selectedStrategy}
1666
- class="flex-1 rounded border border-gray-300 px-3 py-2 font-mono text-sm focus:border-indigo-500 focus:ring-indigo-500"
1667
- >
1668
- {#each availableStrategies as strategy}
1669
- <option value={strategy}>{strategy.toUpperCase()}</option>
1670
- {/each}
1671
- </select>
1672
- <button
1673
- onclick={switchStrategy}
1674
- disabled={!connected || selectedStrategy === currentStrategy}
1675
- class={[
1676
- 'rounded border px-3 py-2 font-mono text-sm',
1677
- !connected || selectedStrategy === currentStrategy
1678
- ? 'bg-gray-200 text-gray-500'
1679
- : 'bg-indigo-600 text-white hover:bg-indigo-700'
1680
- ]}
1681
- >
1682
- Switch
1683
- </button>
1684
- </div>
1685
- </div>
1686
- {#if strategyHistory.length > 0}
1687
- <div>
1688
- <h3 class="mb-2 font-mono text-sm font-medium text-gray-700">Strategy History</h3>
1689
- <div class="max-h-32 space-y-1 overflow-y-auto">
1690
- {#each strategyHistory.slice(0, 5) as change}
1691
- <div class="rounded bg-gray-50 p-2 text-xs">
1692
- <div class="flex items-center justify-between">
1693
- <span class="font-mono text-gray-500">{change.timestamp}</span>
1694
- <span class="font-mono text-indigo-600">
1695
- {change.oldStrategy.toUpperCase()} → {change.newStrategy.toUpperCase()}
1696
- </span>
1697
- </div>
1698
- <div class="mt-1 font-mono text-gray-600">{change.reason}</div>
1699
- </div>
1700
- {/each}
1701
- </div>
1702
- </div>
1703
- {/if}
1704
- </div>
1705
- </div>
1706
-
1707
- <!-- Stream Events -->
1708
- <div class="rounded border p-4">
1709
- <h2 class="mb-3 font-mono text-lg font-semibold">Stream Events</h2>
1710
- <div class="max-h-48 space-y-2 overflow-y-auto">
1711
- {#each streamHistory as event}
1712
- <div class="rounded border-l-4 border-indigo-200 bg-gray-50 p-2">
1713
- <div class="flex items-center justify-between">
1714
- <span class="font-mono text-xs text-gray-500">{event.timestamp}</span>
1715
- <span class="font-mono text-xs text-indigo-600">{event.event}</span>
1716
- </div>
1717
- {#if event.participant}
1718
- <div class="mt-1 font-mono text-xs text-gray-600">
1719
- Participant: {event.participant}
1720
- </div>
1721
- {/if}
1722
- {#if event.reason}
1723
- <div class="mt-1 font-mono text-xs text-gray-600">
1724
- {event.reason}
1725
- </div>
1726
- {/if}
1727
- </div>
1728
- {:else}
1729
- <p class="py-4 text-center font-mono text-sm text-gray-500">No stream events yet</p>
1730
- {/each}
1731
- </div>
1732
- </div>
1733
- </div>
1734
- </div>
1735
-
1736
- <!-- Real-time Updates -->
1737
- <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
1738
- <!-- Frame Updates History -->
1739
- <div class="rounded border p-4">
1740
- <h2 class="mb-3 font-mono text-lg font-semibold">Recent Frames</h2>
1741
- <div class="max-h-64 space-y-2 overflow-y-auto">
1742
- {#each frameHistory.slice(0, 10) as frame}
1743
- <div class="rounded border-l-4 border-purple-200 bg-gray-50 p-2">
1744
- <div class="flex items-center justify-between">
1745
- <span class="font-mono text-xs text-gray-500">{frame.timestamp}</span>
1746
- <span class="font-mono text-xs text-purple-600">FRAME_UPDATE</span>
1747
- </div>
1748
- <div class="mt-1 text-sm">
1749
- <span class="mr-3 inline-block font-mono text-gray-700">
1750
- {frame.frame.width}x{frame.frame.height}
1751
- </span>
1752
- <span class="mr-3 inline-block font-mono text-gray-700">
1753
- {frame.frame.encoding.toUpperCase()}
1754
- </span>
1755
- <span class="inline-block font-mono text-gray-700">
1756
- {(frame.frame.data.byteLength / 1024).toFixed(1)}KB
1757
- </span>
1758
- </div>
1759
- </div>
1760
- {:else}
1761
- <p class="py-4 text-center font-mono text-sm text-gray-500">No frames received yet</p>
1762
- {/each}
1763
- </div>
1764
- </div>
1765
-
1766
- <!-- Config Updates History -->
1767
- <div class="rounded border p-4">
1768
- <h2 class="mb-3 font-mono text-lg font-semibold">Config Updates</h2>
1769
- <div class="max-h-64 space-y-2 overflow-y-auto">
1770
- {#each configHistory as config}
1771
- <div class="rounded border-l-4 border-blue-200 bg-gray-50 p-2">
1772
- <div class="flex items-center justify-between">
1773
- <span class="font-mono text-xs text-gray-500">{config.timestamp}</span>
1774
- <span class="font-mono text-xs text-blue-600">CONFIG_UPDATE</span>
1775
- </div>
1776
- <div class="mt-1 text-sm">
1777
- <div class="font-mono text-gray-700">
1778
- {config.config.resolution?.width}x{config.config.resolution?.height} @
1779
- {config.config.framerate}fps
1780
- </div>
1781
- <div class="font-mono text-gray-700">
1782
- {((config.config.bitrate || 0) / 1000000).toFixed(1)}Mbps,
1783
- {config.config.encoding?.toUpperCase()}
1784
- </div>
1785
- </div>
1786
- </div>
1787
- {:else}
1788
- <p class="py-4 text-center font-mono text-sm text-gray-500">
1789
- No config updates received yet
1790
- </p>
1791
- {/each}
1792
- </div>
1793
- </div>
1794
- </div>
1795
-
1796
- <!-- Error History -->
1797
- {#if errorHistory.length > 0}
1798
- <div class="rounded border p-4">
1799
- <h2 class="mb-3 font-mono text-lg font-semibold">Recent Errors</h2>
1800
- <div class="max-h-32 space-y-2 overflow-y-auto">
1801
- {#each errorHistory as error}
1802
- <div class="rounded border-l-4 border-red-200 bg-red-50 p-2">
1803
- <div class="flex items-center justify-between">
1804
- <span class="font-mono text-xs text-gray-500">{error.timestamp}</span>
1805
- <span class="font-mono text-xs text-red-600">ERROR</span>
1806
- </div>
1807
- <div class="mt-1 font-mono text-sm text-red-700">
1808
- {error.message}
1809
- </div>
1810
- </div>
1811
- {/each}
1812
- </div>
1813
- </div>
1814
- {/if}
1815
- </div>
1816
- {/if}
1817
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test-docker.sh CHANGED
@@ -95,7 +95,7 @@ fi
95
  # Test frontend
96
  echo "Testing frontend..."
97
  FRONTEND_RESPONSE=$(curl -s http://localhost:$PORT/)
98
- if echo "$FRONTEND_RESPONSE" | grep -q "LeRobot Arena"; then
99
  echo -e "${GREEN}✅ Frontend is served correctly${NC}"
100
  else
101
  echo -e "${RED}❌ Frontend test failed${NC}"
 
95
  # Test frontend
96
  echo "Testing frontend..."
97
  FRONTEND_RESPONSE=$(curl -s http://localhost:$PORT/)
98
+ if echo "$FRONTEND_RESPONSE" | grep -q "<!doctype html>" && echo "$FRONTEND_RESPONSE" | grep -q "_app/immutable"; then
99
  echo -e "${GREEN}✅ Frontend is served correctly${NC}"
100
  else
101
  echo -e "${RED}❌ Frontend test failed${NC}"