Julian Bilcke
commited on
Commit
·
cd845ad
1
Parent(s):
1382b6e
wip
Browse files- api_engine.py +35 -12
- api_server.py +113 -5
- client/client.js +151 -0
- client/index.html +9 -0
api_engine.py
CHANGED
|
@@ -54,7 +54,9 @@ class MatrixGameEngine:
|
|
| 54 |
self.frame_width = getattr(args, 'frame_width', 640)
|
| 55 |
self.frame_height = getattr(args, 'frame_height', 352)
|
| 56 |
self.fps = getattr(args, 'fps', 16)
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
self.seed = getattr(args, 'seed', 0)
|
| 59 |
self.config_path = getattr(args, 'config_path', 'configs/inference_yaml/inference_universal.yaml')
|
| 60 |
self.checkpoint_path = getattr(args, 'checkpoint_path', '')
|
|
@@ -328,13 +330,22 @@ class MatrixGameEngine:
|
|
| 328 |
if mouse_condition is None:
|
| 329 |
mouse_condition = [[0, 0]]
|
| 330 |
|
| 331 |
-
# Generate conditions
|
| 332 |
-
#
|
| 333 |
-
|
| 334 |
|
| 335 |
-
#
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
# Build conditional dict
|
| 340 |
cond_concat = torch.cat([scene_data['mask'][:, :4], scene_data['latent']], dim=1)
|
|
@@ -348,10 +359,21 @@ class MatrixGameEngine:
|
|
| 348 |
if mode in ['universal', 'gta_drive']:
|
| 349 |
conditional_dict['mouse_cond'] = mouse_tensor
|
| 350 |
|
| 351 |
-
# Generate noise for the frames
|
| 352 |
sampled_noise = torch.randn(
|
| 353 |
-
[1, 16,
|
| 354 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
# Generate frames with streaming pipeline
|
| 357 |
with torch.no_grad():
|
|
@@ -386,13 +408,14 @@ class MatrixGameEngine:
|
|
| 386 |
if outputs is not None:
|
| 387 |
logger.debug(f"Output tensor shape: {outputs.shape if hasattr(outputs, 'shape') else 'No shape attr'}")
|
| 388 |
|
| 389 |
-
# Decode first frame from latent
|
| 390 |
if outputs is not None and len(outputs) > 0:
|
| 391 |
decode_start = time.time()
|
| 392 |
logger.debug("Starting VAE decoding...")
|
|
|
|
| 393 |
|
| 394 |
-
# Extract first frame
|
| 395 |
-
frame_latent = outputs[0:1, :, 0:1] # Get first frame
|
| 396 |
logger.debug(f"Frame latent shape: {frame_latent.shape}")
|
| 397 |
|
| 398 |
decoded = self.pipeline.vae_decoder.decode(frame_latent)
|
|
|
|
| 54 |
self.frame_width = getattr(args, 'frame_width', 640)
|
| 55 |
self.frame_height = getattr(args, 'frame_height', 352)
|
| 56 |
self.fps = getattr(args, 'fps', 16)
|
| 57 |
+
# For streaming sessions: following original Matrix-Game-2 default
|
| 58 |
+
# 360 frames at 16 FPS = 22.5 seconds, divisible by num_frame_per_block (3)
|
| 59 |
+
self.max_num_output_frames = getattr(args, 'max_num_output_frames', 360) # 360 = 3 * 120, ~23 sec session
|
| 60 |
self.seed = getattr(args, 'seed', 0)
|
| 61 |
self.config_path = getattr(args, 'config_path', 'configs/inference_yaml/inference_universal.yaml')
|
| 62 |
self.checkpoint_path = getattr(args, 'checkpoint_path', '')
|
|
|
|
| 330 |
if mouse_condition is None:
|
| 331 |
mouse_condition = [[0, 0]]
|
| 332 |
|
| 333 |
+
# Generate conditions following the original Matrix-Game V2 pattern
|
| 334 |
+
# For streaming, use fewer frames for real-time performance
|
| 335 |
+
max_num_output_frames = self.max_num_output_frames # Use from constructor
|
| 336 |
|
| 337 |
+
# Calculate condition frames: (max_num_output_frames - 1) * 4 + 1
|
| 338 |
+
# This follows the pattern from the original implementation
|
| 339 |
+
condition_num_frames = (max_num_output_frames - 1) * 4 + 1
|
| 340 |
+
|
| 341 |
+
logger.info(f"Using {max_num_output_frames} output frames -> {condition_num_frames} condition frames")
|
| 342 |
+
|
| 343 |
+
# Create condition tensors with the correct length
|
| 344 |
+
keyboard_tensor = torch.tensor(keyboard_condition * condition_num_frames, dtype=self.weight_dtype).unsqueeze(0).to(self.device)
|
| 345 |
+
mouse_tensor = torch.tensor(mouse_condition * condition_num_frames, dtype=self.weight_dtype).unsqueeze(0).to(self.device)
|
| 346 |
+
|
| 347 |
+
logger.debug(f"Keyboard tensor shape: {keyboard_tensor.shape}")
|
| 348 |
+
logger.debug(f"Mouse tensor shape: {mouse_tensor.shape}")
|
| 349 |
|
| 350 |
# Build conditional dict
|
| 351 |
cond_concat = torch.cat([scene_data['mask'][:, :4], scene_data['latent']], dim=1)
|
|
|
|
| 359 |
if mode in ['universal', 'gta_drive']:
|
| 360 |
conditional_dict['mouse_cond'] = mouse_tensor
|
| 361 |
|
| 362 |
+
# Generate noise for the frames (following original implementation)
|
| 363 |
sampled_noise = torch.randn(
|
| 364 |
+
[1, 16, max_num_output_frames, 44, 80], device=self.device, dtype=self.weight_dtype
|
| 365 |
)
|
| 366 |
+
logger.debug(f"Generated noise shape: {sampled_noise.shape}")
|
| 367 |
+
|
| 368 |
+
pipeline_block_size = getattr(self.pipeline, 'num_frame_per_block', 3)
|
| 369 |
+
logger.debug(f"Pipeline num_frame_per_block: {pipeline_block_size}")
|
| 370 |
+
|
| 371 |
+
# Validate that max_num_output_frames is divisible by num_frame_per_block
|
| 372 |
+
if max_num_output_frames % pipeline_block_size != 0:
|
| 373 |
+
logger.error(f"Frame count mismatch: {max_num_output_frames} frames not divisible by block size {pipeline_block_size}")
|
| 374 |
+
raise ValueError(f"max_num_output_frames ({max_num_output_frames}) must be divisible by num_frame_per_block ({pipeline_block_size})")
|
| 375 |
+
else:
|
| 376 |
+
logger.debug(f"Frame count validation passed: {max_num_output_frames} frames / {pipeline_block_size} blocks = {max_num_output_frames // pipeline_block_size} blocks")
|
| 377 |
|
| 378 |
# Generate frames with streaming pipeline
|
| 379 |
with torch.no_grad():
|
|
|
|
| 408 |
if outputs is not None:
|
| 409 |
logger.debug(f"Output tensor shape: {outputs.shape if hasattr(outputs, 'shape') else 'No shape attr'}")
|
| 410 |
|
| 411 |
+
# Decode first frame from latent (following original pattern)
|
| 412 |
if outputs is not None and len(outputs) > 0:
|
| 413 |
decode_start = time.time()
|
| 414 |
logger.debug("Starting VAE decoding...")
|
| 415 |
+
logger.debug(f"Output tensor shape: {outputs.shape}")
|
| 416 |
|
| 417 |
+
# Extract first frame from the generated sequence
|
| 418 |
+
frame_latent = outputs[0:1, :, 0:1] # Get first frame [batch=1, channels, frames=1, h, w]
|
| 419 |
logger.debug(f"Frame latent shape: {frame_latent.shape}")
|
| 420 |
|
| 421 |
decoded = self.pipeline.vae_decoder.decode(frame_latent)
|
api_server.py
CHANGED
|
@@ -50,6 +50,13 @@ class GameSession:
|
|
| 50 |
self.keyboard_state = [0, 0, 0, 0, 0, 0] # forward, back, left, right, jump, attack
|
| 51 |
self.mouse_state = [0, 0] # x, y
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
self.background_tasks = []
|
| 54 |
|
| 55 |
async def start(self):
|
|
@@ -57,6 +64,13 @@ class GameSession:
|
|
| 57 |
self.background_tasks = [
|
| 58 |
asyncio.create_task(self._process_action_queue()),
|
| 59 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
logger.info(f"Started game session for user {self.user_id}")
|
| 61 |
|
| 62 |
async def stop(self):
|
|
@@ -99,6 +113,8 @@ class GameSession:
|
|
| 99 |
result = await self._handle_mouse_input(data)
|
| 100 |
elif action_type == 'change_scene':
|
| 101 |
result = await self._handle_scene_change(data)
|
|
|
|
|
|
|
| 102 |
else:
|
| 103 |
result = {
|
| 104 |
'action': action_type,
|
|
@@ -137,15 +153,36 @@ class GameSession:
|
|
| 137 |
'error': 'Stream already active'
|
| 138 |
}
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
fps = data.get('fps', 16)
|
| 141 |
self.is_streaming = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
self.stream_task = asyncio.create_task(self._stream_frames(fps))
|
| 143 |
|
| 144 |
return {
|
| 145 |
'action': 'start_stream',
|
| 146 |
'requestId': data.get('requestId'),
|
| 147 |
'success': True,
|
| 148 |
-
'message': f'Streaming started at {fps} FPS'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
}
|
| 150 |
|
| 151 |
async def _handle_stop_stream(self, data: Dict) -> Dict:
|
|
@@ -237,6 +274,42 @@ class GameSession:
|
|
| 237 |
'scene': scene_name
|
| 238 |
}
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
async def _stream_frames(self, fps: int):
|
| 241 |
"""Stream frames to the client at the specified FPS"""
|
| 242 |
frame_interval = 1.0 / fps # Time between frames in seconds
|
|
@@ -245,14 +318,20 @@ class GameSession:
|
|
| 245 |
logger.info(f"Starting frame streaming for user {self.user_id} at {fps} FPS (interval: {frame_interval:.3f}s)")
|
| 246 |
|
| 247 |
try:
|
| 248 |
-
while self.is_streaming:
|
| 249 |
stream_start_time = time.time()
|
| 250 |
|
| 251 |
# Generate frame based on current keyboard and mouse state
|
| 252 |
keyboard_condition = [self.keyboard_state]
|
| 253 |
mouse_condition = [self.mouse_state]
|
| 254 |
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
# Check if engine is available
|
| 258 |
if not self.game_manager.engine:
|
|
@@ -287,7 +366,7 @@ class GameSession:
|
|
| 287 |
frame_base64 = base64.b64encode(frame_bytes).decode('utf-8')
|
| 288 |
encode_time = time.time() - encode_start
|
| 289 |
|
| 290 |
-
# Send frame to client
|
| 291 |
send_start = time.time()
|
| 292 |
await self.ws.send_json({
|
| 293 |
'action': 'frame',
|
|
@@ -295,10 +374,21 @@ class GameSession:
|
|
| 295 |
'timestamp': time.time(),
|
| 296 |
'frameNumber': frame_count,
|
| 297 |
'generationTime': f"{generation_time:.3f}s",
|
| 298 |
-
'frameSize': len(frame_bytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
})
|
| 300 |
send_time = time.time() - send_start
|
| 301 |
|
|
|
|
|
|
|
|
|
|
| 302 |
# Calculate total time and performance metrics
|
| 303 |
total_time = time.time() - stream_start_time
|
| 304 |
sleep_time = max(0, frame_interval - total_time)
|
|
@@ -312,6 +402,24 @@ class GameSession:
|
|
| 312 |
|
| 313 |
frame_count += 1
|
| 314 |
await asyncio.sleep(sleep_time)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
except asyncio.CancelledError:
|
| 317 |
logger.info(f"Frame streaming cancelled for user {self.user_id}")
|
|
|
|
| 50 |
self.keyboard_state = [0, 0, 0, 0, 0, 0] # forward, back, left, right, jump, attack
|
| 51 |
self.mouse_state = [0, 0] # x, y
|
| 52 |
|
| 53 |
+
# Game session state
|
| 54 |
+
self.current_frame = 0
|
| 55 |
+
self.max_frames = 0 # Will be set when engine is available
|
| 56 |
+
self.session_duration = 0 # Total session duration in seconds
|
| 57 |
+
self.session_start_time = None
|
| 58 |
+
self.session_active = False
|
| 59 |
+
|
| 60 |
self.background_tasks = []
|
| 61 |
|
| 62 |
async def start(self):
|
|
|
|
| 64 |
self.background_tasks = [
|
| 65 |
asyncio.create_task(self._process_action_queue()),
|
| 66 |
]
|
| 67 |
+
|
| 68 |
+
# Initialize session parameters from engine
|
| 69 |
+
if self.game_manager.engine:
|
| 70 |
+
self.max_frames = self.game_manager.engine.max_num_output_frames
|
| 71 |
+
self.session_duration = self.max_frames / self.game_manager.engine.fps
|
| 72 |
+
logger.info(f"Session initialized - Max frames: {self.max_frames}, Duration: {self.session_duration:.1f}s")
|
| 73 |
+
|
| 74 |
logger.info(f"Started game session for user {self.user_id}")
|
| 75 |
|
| 76 |
async def stop(self):
|
|
|
|
| 113 |
result = await self._handle_mouse_input(data)
|
| 114 |
elif action_type == 'change_scene':
|
| 115 |
result = await self._handle_scene_change(data)
|
| 116 |
+
elif action_type == 'reset_session':
|
| 117 |
+
result = await self._handle_session_reset(data)
|
| 118 |
else:
|
| 119 |
result = {
|
| 120 |
'action': action_type,
|
|
|
|
| 153 |
'error': 'Stream already active'
|
| 154 |
}
|
| 155 |
|
| 156 |
+
# Check if session has reached max frames
|
| 157 |
+
if self.current_frame >= self.max_frames:
|
| 158 |
+
return {
|
| 159 |
+
'action': 'start_stream',
|
| 160 |
+
'requestId': data.get('requestId'),
|
| 161 |
+
'success': False,
|
| 162 |
+
'error': 'Session has ended. Please reset to start a new session.',
|
| 163 |
+
'sessionEnded': True
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
fps = data.get('fps', 16)
|
| 167 |
self.is_streaming = True
|
| 168 |
+
self.session_active = True
|
| 169 |
+
if self.session_start_time is None:
|
| 170 |
+
self.session_start_time = time.time()
|
| 171 |
+
logger.info(f"Game session started for user {self.user_id}")
|
| 172 |
+
|
| 173 |
self.stream_task = asyncio.create_task(self._stream_frames(fps))
|
| 174 |
|
| 175 |
return {
|
| 176 |
'action': 'start_stream',
|
| 177 |
'requestId': data.get('requestId'),
|
| 178 |
'success': True,
|
| 179 |
+
'message': f'Streaming started at {fps} FPS',
|
| 180 |
+
'sessionInfo': {
|
| 181 |
+
'maxFrames': self.max_frames,
|
| 182 |
+
'sessionDuration': self.session_duration,
|
| 183 |
+
'currentFrame': self.current_frame,
|
| 184 |
+
'remainingFrames': self.max_frames - self.current_frame
|
| 185 |
+
}
|
| 186 |
}
|
| 187 |
|
| 188 |
async def _handle_stop_stream(self, data: Dict) -> Dict:
|
|
|
|
| 274 |
'scene': scene_name
|
| 275 |
}
|
| 276 |
|
| 277 |
+
async def _handle_session_reset(self, data: Dict) -> Dict:
|
| 278 |
+
"""Handle session reset requests"""
|
| 279 |
+
# Reset session state
|
| 280 |
+
self.current_frame = 0
|
| 281 |
+
self.session_start_time = None
|
| 282 |
+
self.session_active = False
|
| 283 |
+
|
| 284 |
+
# Stop current streaming if active
|
| 285 |
+
if self.is_streaming:
|
| 286 |
+
self.is_streaming = False
|
| 287 |
+
if self.stream_task:
|
| 288 |
+
self.stream_task.cancel()
|
| 289 |
+
try:
|
| 290 |
+
await self.stream_task
|
| 291 |
+
except asyncio.CancelledError:
|
| 292 |
+
pass
|
| 293 |
+
self.stream_task = None
|
| 294 |
+
|
| 295 |
+
# Reset input states
|
| 296 |
+
self.keyboard_state = [0, 0, 0, 0, 0, 0]
|
| 297 |
+
self.mouse_state = [0, 0]
|
| 298 |
+
|
| 299 |
+
logger.info(f"Session reset for user {self.user_id}")
|
| 300 |
+
|
| 301 |
+
return {
|
| 302 |
+
'action': 'reset_session',
|
| 303 |
+
'requestId': data.get('requestId'),
|
| 304 |
+
'success': True,
|
| 305 |
+
'message': 'Session reset successfully',
|
| 306 |
+
'sessionInfo': {
|
| 307 |
+
'maxFrames': self.max_frames,
|
| 308 |
+
'sessionDuration': self.session_duration,
|
| 309 |
+
'currentFrame': self.current_frame
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
async def _stream_frames(self, fps: int):
|
| 314 |
"""Stream frames to the client at the specified FPS"""
|
| 315 |
frame_interval = 1.0 / fps # Time between frames in seconds
|
|
|
|
| 318 |
logger.info(f"Starting frame streaming for user {self.user_id} at {fps} FPS (interval: {frame_interval:.3f}s)")
|
| 319 |
|
| 320 |
try:
|
| 321 |
+
while self.is_streaming and self.current_frame < self.max_frames:
|
| 322 |
stream_start_time = time.time()
|
| 323 |
|
| 324 |
# Generate frame based on current keyboard and mouse state
|
| 325 |
keyboard_condition = [self.keyboard_state]
|
| 326 |
mouse_condition = [self.mouse_state]
|
| 327 |
|
| 328 |
+
# Calculate session progress
|
| 329 |
+
progress_percent = (self.current_frame / self.max_frames) * 100 if self.max_frames > 0 else 0
|
| 330 |
+
remaining_frames = self.max_frames - self.current_frame
|
| 331 |
+
elapsed_time = time.time() - self.session_start_time if self.session_start_time else 0
|
| 332 |
+
|
| 333 |
+
logger.debug(f"Session progress: {self.current_frame}/{self.max_frames} ({progress_percent:.1f}%) - "
|
| 334 |
+
f"KB: {keyboard_condition[0]}, Mouse: {mouse_condition[0]}")
|
| 335 |
|
| 336 |
# Check if engine is available
|
| 337 |
if not self.game_manager.engine:
|
|
|
|
| 366 |
frame_base64 = base64.b64encode(frame_bytes).decode('utf-8')
|
| 367 |
encode_time = time.time() - encode_start
|
| 368 |
|
| 369 |
+
# Send frame to client with session progress
|
| 370 |
send_start = time.time()
|
| 371 |
await self.ws.send_json({
|
| 372 |
'action': 'frame',
|
|
|
|
| 374 |
'timestamp': time.time(),
|
| 375 |
'frameNumber': frame_count,
|
| 376 |
'generationTime': f"{generation_time:.3f}s",
|
| 377 |
+
'frameSize': len(frame_bytes),
|
| 378 |
+
'sessionProgress': {
|
| 379 |
+
'currentFrame': self.current_frame,
|
| 380 |
+
'maxFrames': self.max_frames,
|
| 381 |
+
'progress': progress_percent,
|
| 382 |
+
'remainingFrames': remaining_frames,
|
| 383 |
+
'elapsedTime': elapsed_time,
|
| 384 |
+
'remainingTime': (remaining_frames / fps) if fps > 0 else 0
|
| 385 |
+
}
|
| 386 |
})
|
| 387 |
send_time = time.time() - send_start
|
| 388 |
|
| 389 |
+
# Increment session frame counter
|
| 390 |
+
self.current_frame += 1
|
| 391 |
+
|
| 392 |
# Calculate total time and performance metrics
|
| 393 |
total_time = time.time() - stream_start_time
|
| 394 |
sleep_time = max(0, frame_interval - total_time)
|
|
|
|
| 402 |
|
| 403 |
frame_count += 1
|
| 404 |
await asyncio.sleep(sleep_time)
|
| 405 |
+
|
| 406 |
+
# Handle session end
|
| 407 |
+
if self.current_frame >= self.max_frames:
|
| 408 |
+
logger.info(f"Game session ended for user {self.user_id} - {self.current_frame}/{self.max_frames} frames completed")
|
| 409 |
+
self.is_streaming = False
|
| 410 |
+
self.session_active = False
|
| 411 |
+
|
| 412 |
+
# Send session end notification
|
| 413 |
+
await self.ws.send_json({
|
| 414 |
+
'action': 'session_ended',
|
| 415 |
+
'message': 'Game session completed!',
|
| 416 |
+
'sessionStats': {
|
| 417 |
+
'totalFrames': self.current_frame,
|
| 418 |
+
'maxFrames': self.max_frames,
|
| 419 |
+
'elapsedTime': elapsed_time,
|
| 420 |
+
'sessionDuration': self.session_duration
|
| 421 |
+
}
|
| 422 |
+
})
|
| 423 |
|
| 424 |
except asyncio.CancelledError:
|
| 425 |
logger.info(f"Frame streaming cancelled for user {self.user_id}")
|
client/client.js
CHANGED
|
@@ -8,10 +8,23 @@ let lastFrameTime = 0;
|
|
| 8 |
let frameCount = 0;
|
| 9 |
let fpsUpdateInterval = null;
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
// DOM Elements
|
| 12 |
const connectBtn = document.getElementById('connect-btn');
|
| 13 |
const startStreamBtn = document.getElementById('start-stream-btn');
|
| 14 |
const stopStreamBtn = document.getElementById('stop-stream-btn');
|
|
|
|
| 15 |
const sceneSelect = document.getElementById('scene-select');
|
| 16 |
const gameCanvas = document.getElementById('game-canvas');
|
| 17 |
const connectionLog = document.getElementById('connection-log');
|
|
@@ -19,6 +32,15 @@ const mousePosition = document.getElementById('mouse-position');
|
|
| 19 |
const fpsCounter = document.getElementById('fps-counter');
|
| 20 |
const mouseTrackingArea = document.getElementById('mouse-tracking-area');
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
// Pointer Lock API support check
|
| 23 |
const pointerLockSupported = 'pointerLockElement' in document ||
|
| 24 |
'mozPointerLockElement' in document ||
|
|
@@ -167,6 +189,8 @@ function setupWebSocketHandlers() {
|
|
| 167 |
logMessage('WebSocket connection established');
|
| 168 |
connectBtn.textContent = 'Disconnect';
|
| 169 |
startStreamBtn.disabled = false;
|
|
|
|
|
|
|
| 170 |
sceneSelect.disabled = false;
|
| 171 |
};
|
| 172 |
|
|
@@ -225,14 +249,28 @@ function setupWebSocketHandlers() {
|
|
| 225 |
case 'start_stream':
|
| 226 |
if (message.success) {
|
| 227 |
isStreaming = true;
|
|
|
|
| 228 |
startStreamBtn.disabled = true;
|
| 229 |
stopStreamBtn.disabled = false;
|
| 230 |
logMessage(`Streaming started: ${message.message}`);
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
// Start FPS counter
|
| 233 |
startFpsCounter();
|
| 234 |
} else {
|
| 235 |
logMessage(`Error starting stream: ${message.error}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
}
|
| 237 |
break;
|
| 238 |
|
|
@@ -280,6 +318,56 @@ function setupWebSocketHandlers() {
|
|
| 280 |
}
|
| 281 |
break;
|
| 282 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
default:
|
| 284 |
logMessage(`Received message: ${JSON.stringify(message)}`);
|
| 285 |
}
|
|
@@ -367,6 +455,47 @@ function sendChangeScene(scene) {
|
|
| 367 |
}
|
| 368 |
}
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
// Process incoming frame
|
| 371 |
function processFrame(message) {
|
| 372 |
// Update FPS calculation
|
|
@@ -380,6 +509,11 @@ function processFrame(message) {
|
|
| 380 |
if (message.frameData) {
|
| 381 |
gameCanvas.src = `data:image/jpeg;base64,${message.frameData}`;
|
| 382 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}
|
| 384 |
|
| 385 |
// Generate a random request ID
|
|
@@ -430,8 +564,12 @@ function resetUI() {
|
|
| 430 |
connectBtn.textContent = 'Connect';
|
| 431 |
startStreamBtn.disabled = true;
|
| 432 |
stopStreamBtn.disabled = true;
|
|
|
|
| 433 |
sceneSelect.disabled = true;
|
| 434 |
|
|
|
|
|
|
|
|
|
|
| 435 |
// Reset key indicators
|
| 436 |
for (const key in keyElements) {
|
| 437 |
keyElements[key].classList.remove('active');
|
|
@@ -442,6 +580,18 @@ function resetUI() {
|
|
| 442 |
|
| 443 |
// Reset streaming state
|
| 444 |
isStreaming = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
}
|
| 446 |
|
| 447 |
// Event Listeners
|
|
@@ -455,6 +605,7 @@ connectBtn.addEventListener('click', () => {
|
|
| 455 |
|
| 456 |
startStreamBtn.addEventListener('click', sendStartStream);
|
| 457 |
stopStreamBtn.addEventListener('click', sendStopStream);
|
|
|
|
| 458 |
|
| 459 |
sceneSelect.addEventListener('change', () => {
|
| 460 |
sendChangeScene(sceneSelect.value);
|
|
|
|
| 8 |
let frameCount = 0;
|
| 9 |
let fpsUpdateInterval = null;
|
| 10 |
|
| 11 |
+
// Session state
|
| 12 |
+
let sessionInfo = {
|
| 13 |
+
maxFrames: 0,
|
| 14 |
+
sessionDuration: 0,
|
| 15 |
+
currentFrame: 0,
|
| 16 |
+
remainingFrames: 0,
|
| 17 |
+
elapsedTime: 0,
|
| 18 |
+
remainingTime: 0,
|
| 19 |
+
sessionStartTime: null,
|
| 20 |
+
sessionEnded: false
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
// DOM Elements
|
| 24 |
const connectBtn = document.getElementById('connect-btn');
|
| 25 |
const startStreamBtn = document.getElementById('start-stream-btn');
|
| 26 |
const stopStreamBtn = document.getElementById('stop-stream-btn');
|
| 27 |
+
const resetSessionBtn = document.getElementById('reset-session-btn');
|
| 28 |
const sceneSelect = document.getElementById('scene-select');
|
| 29 |
const gameCanvas = document.getElementById('game-canvas');
|
| 30 |
const connectionLog = document.getElementById('connection-log');
|
|
|
|
| 32 |
const fpsCounter = document.getElementById('fps-counter');
|
| 33 |
const mouseTrackingArea = document.getElementById('mouse-tracking-area');
|
| 34 |
|
| 35 |
+
// Session progress elements
|
| 36 |
+
const sessionProgress = document.getElementById('session-progress');
|
| 37 |
+
const progressFill = document.getElementById('progress-fill');
|
| 38 |
+
const currentFrameEl = document.getElementById('current-frame');
|
| 39 |
+
const maxFramesEl = document.getElementById('max-frames');
|
| 40 |
+
const elapsedTimeEl = document.getElementById('elapsed-time');
|
| 41 |
+
const sessionDurationEl = document.getElementById('session-duration');
|
| 42 |
+
const remainingTimeEl = document.getElementById('remaining-time');
|
| 43 |
+
|
| 44 |
// Pointer Lock API support check
|
| 45 |
const pointerLockSupported = 'pointerLockElement' in document ||
|
| 46 |
'mozPointerLockElement' in document ||
|
|
|
|
| 189 |
logMessage('WebSocket connection established');
|
| 190 |
connectBtn.textContent = 'Disconnect';
|
| 191 |
startStreamBtn.disabled = false;
|
| 192 |
+
stopStreamBtn.disabled = true;
|
| 193 |
+
resetSessionBtn.disabled = false;
|
| 194 |
sceneSelect.disabled = false;
|
| 195 |
};
|
| 196 |
|
|
|
|
| 249 |
case 'start_stream':
|
| 250 |
if (message.success) {
|
| 251 |
isStreaming = true;
|
| 252 |
+
sessionInfo.sessionEnded = false;
|
| 253 |
startStreamBtn.disabled = true;
|
| 254 |
stopStreamBtn.disabled = false;
|
| 255 |
logMessage(`Streaming started: ${message.message}`);
|
| 256 |
|
| 257 |
+
// Update session info if provided
|
| 258 |
+
if (message.sessionInfo) {
|
| 259 |
+
updateSessionProgress(message.sessionInfo);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
// Start FPS counter
|
| 263 |
startFpsCounter();
|
| 264 |
} else {
|
| 265 |
logMessage(`Error starting stream: ${message.error}`);
|
| 266 |
+
|
| 267 |
+
// Check if session ended
|
| 268 |
+
if (message.sessionEnded) {
|
| 269 |
+
sessionInfo.sessionEnded = true;
|
| 270 |
+
startStreamBtn.disabled = true;
|
| 271 |
+
stopStreamBtn.disabled = true;
|
| 272 |
+
logMessage('Session has ended. Please reset to start a new session.');
|
| 273 |
+
}
|
| 274 |
}
|
| 275 |
break;
|
| 276 |
|
|
|
|
| 318 |
}
|
| 319 |
break;
|
| 320 |
|
| 321 |
+
case 'session_ended':
|
| 322 |
+
logMessage(`🏁 ${message.message}`);
|
| 323 |
+
isStreaming = false;
|
| 324 |
+
sessionInfo.sessionEnded = true;
|
| 325 |
+
startStreamBtn.disabled = true;
|
| 326 |
+
stopStreamBtn.disabled = true;
|
| 327 |
+
|
| 328 |
+
// Show session stats if available
|
| 329 |
+
if (message.sessionStats) {
|
| 330 |
+
const stats = message.sessionStats;
|
| 331 |
+
logMessage(`Session Stats: ${stats.totalFrames}/${stats.maxFrames} frames, ${formatTime(stats.elapsedTime)} elapsed`);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
// Stop FPS counter
|
| 335 |
+
stopFpsCounter();
|
| 336 |
+
break;
|
| 337 |
+
|
| 338 |
+
case 'reset_session':
|
| 339 |
+
if (message.success) {
|
| 340 |
+
logMessage(`🔄 ${message.message}`);
|
| 341 |
+
|
| 342 |
+
// Reset session state
|
| 343 |
+
sessionInfo = {
|
| 344 |
+
maxFrames: 0,
|
| 345 |
+
sessionDuration: 0,
|
| 346 |
+
currentFrame: 0,
|
| 347 |
+
remainingFrames: 0,
|
| 348 |
+
elapsedTime: 0,
|
| 349 |
+
remainingTime: 0,
|
| 350 |
+
sessionStartTime: null,
|
| 351 |
+
sessionEnded: false
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
// Update UI
|
| 355 |
+
isStreaming = false;
|
| 356 |
+
startStreamBtn.disabled = false;
|
| 357 |
+
stopStreamBtn.disabled = true;
|
| 358 |
+
sessionProgress.style.display = 'none';
|
| 359 |
+
|
| 360 |
+
// Update session info if provided
|
| 361 |
+
if (message.sessionInfo) {
|
| 362 |
+
updateSessionProgress(message.sessionInfo);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
stopFpsCounter();
|
| 366 |
+
} else {
|
| 367 |
+
logMessage(`Error resetting session: ${message.error}`);
|
| 368 |
+
}
|
| 369 |
+
break;
|
| 370 |
+
|
| 371 |
default:
|
| 372 |
logMessage(`Received message: ${JSON.stringify(message)}`);
|
| 373 |
}
|
|
|
|
| 455 |
}
|
| 456 |
}
|
| 457 |
|
| 458 |
+
// Reset session
|
| 459 |
+
function sendResetSession() {
|
| 460 |
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
| 461 |
+
socket.send(JSON.stringify({
|
| 462 |
+
action: 'reset_session',
|
| 463 |
+
requestId: generateRequestId()
|
| 464 |
+
}));
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
// Update session progress display
|
| 469 |
+
function updateSessionProgress(progress) {
|
| 470 |
+
if (progress) {
|
| 471 |
+
sessionInfo = { ...sessionInfo, ...progress };
|
| 472 |
+
|
| 473 |
+
// Show progress UI if session is active
|
| 474 |
+
if (sessionInfo.maxFrames > 0) {
|
| 475 |
+
sessionProgress.style.display = 'block';
|
| 476 |
+
|
| 477 |
+
// Update progress bar
|
| 478 |
+
const progressPercent = (sessionInfo.currentFrame / sessionInfo.maxFrames) * 100;
|
| 479 |
+
progressFill.style.width = `${progressPercent}%`;
|
| 480 |
+
|
| 481 |
+
// Update text displays
|
| 482 |
+
currentFrameEl.textContent = sessionInfo.currentFrame;
|
| 483 |
+
maxFramesEl.textContent = sessionInfo.maxFrames;
|
| 484 |
+
elapsedTimeEl.textContent = formatTime(sessionInfo.elapsedTime);
|
| 485 |
+
sessionDurationEl.textContent = formatTime(sessionInfo.sessionDuration);
|
| 486 |
+
remainingTimeEl.textContent = formatTime(sessionInfo.remainingTime);
|
| 487 |
+
}
|
| 488 |
+
}
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
// Format time in MM:SS format
|
| 492 |
+
function formatTime(seconds) {
|
| 493 |
+
if (isNaN(seconds) || seconds < 0) return '0:00';
|
| 494 |
+
const minutes = Math.floor(seconds / 60);
|
| 495 |
+
const secs = Math.floor(seconds % 60);
|
| 496 |
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
// Process incoming frame
|
| 500 |
function processFrame(message) {
|
| 501 |
// Update FPS calculation
|
|
|
|
| 509 |
if (message.frameData) {
|
| 510 |
gameCanvas.src = `data:image/jpeg;base64,${message.frameData}`;
|
| 511 |
}
|
| 512 |
+
|
| 513 |
+
// Update session progress if available
|
| 514 |
+
if (message.sessionProgress) {
|
| 515 |
+
updateSessionProgress(message.sessionProgress);
|
| 516 |
+
}
|
| 517 |
}
|
| 518 |
|
| 519 |
// Generate a random request ID
|
|
|
|
| 564 |
connectBtn.textContent = 'Connect';
|
| 565 |
startStreamBtn.disabled = true;
|
| 566 |
stopStreamBtn.disabled = true;
|
| 567 |
+
resetSessionBtn.disabled = true;
|
| 568 |
sceneSelect.disabled = true;
|
| 569 |
|
| 570 |
+
// Hide session progress
|
| 571 |
+
sessionProgress.style.display = 'none';
|
| 572 |
+
|
| 573 |
// Reset key indicators
|
| 574 |
for (const key in keyElements) {
|
| 575 |
keyElements[key].classList.remove('active');
|
|
|
|
| 580 |
|
| 581 |
// Reset streaming state
|
| 582 |
isStreaming = false;
|
| 583 |
+
|
| 584 |
+
// Reset session state
|
| 585 |
+
sessionInfo = {
|
| 586 |
+
maxFrames: 0,
|
| 587 |
+
sessionDuration: 0,
|
| 588 |
+
currentFrame: 0,
|
| 589 |
+
remainingFrames: 0,
|
| 590 |
+
elapsedTime: 0,
|
| 591 |
+
remainingTime: 0,
|
| 592 |
+
sessionStartTime: null,
|
| 593 |
+
sessionEnded: false
|
| 594 |
+
};
|
| 595 |
}
|
| 596 |
|
| 597 |
// Event Listeners
|
|
|
|
| 605 |
|
| 606 |
startStreamBtn.addEventListener('click', sendStartStream);
|
| 607 |
stopStreamBtn.addEventListener('click', sendStopStream);
|
| 608 |
+
resetSessionBtn.addEventListener('click', sendResetSession);
|
| 609 |
|
| 610 |
sceneSelect.addEventListener('change', () => {
|
| 611 |
sendChangeScene(sceneSelect.value);
|
client/index.html
CHANGED
|
@@ -257,12 +257,21 @@
|
|
| 257 |
<img id="game-canvas" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" alt="Game Frame">
|
| 258 |
<div id="mouse-position">Mouse: 0.00, 0.00</div>
|
| 259 |
<div class="fps-counter" id="fps-counter">FPS: 0</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
</div>
|
| 261 |
|
| 262 |
<div class="controls">
|
| 263 |
<button id="connect-btn">Connect</button>
|
| 264 |
<button id="start-stream-btn" disabled>Start Stream</button>
|
| 265 |
<button id="stop-stream-btn" disabled>Stop Stream</button>
|
|
|
|
| 266 |
<select id="scene-select" disabled>
|
| 267 |
<option value="universal">Universal Mode</option>
|
| 268 |
<option value="gta_drive">GTA Drive Mode</option>
|
|
|
|
| 257 |
<img id="game-canvas" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" alt="Game Frame">
|
| 258 |
<div id="mouse-position">Mouse: 0.00, 0.00</div>
|
| 259 |
<div class="fps-counter" id="fps-counter">FPS: 0</div>
|
| 260 |
+
<div id="session-progress" style="position: absolute; top: 50px; left: 10px; background-color: rgba(0,0,0,0.8); color: #4CAF50; padding: 10px; border-radius: 5px; font-family: monospace; z-index: 20; display: none;">
|
| 261 |
+
<div>Session Progress: <span id="progress-bar" style="display: inline-block; width: 200px; height: 10px; background-color: #333; border-radius: 5px; margin-left: 10px; position: relative;">
|
| 262 |
+
<div id="progress-fill" style="height: 100%; background-color: #4CAF50; border-radius: 5px; width: 0%; transition: width 0.3s;"></div>
|
| 263 |
+
</span></div>
|
| 264 |
+
<div style="margin-top: 5px;">Frame: <span id="current-frame">0</span>/<span id="max-frames">0</span></div>
|
| 265 |
+
<div>Time: <span id="elapsed-time">0:00</span> / <span id="session-duration">0:00</span></div>
|
| 266 |
+
<div>Remaining: <span id="remaining-time">0:00</span></div>
|
| 267 |
+
</div>
|
| 268 |
</div>
|
| 269 |
|
| 270 |
<div class="controls">
|
| 271 |
<button id="connect-btn">Connect</button>
|
| 272 |
<button id="start-stream-btn" disabled>Start Stream</button>
|
| 273 |
<button id="stop-stream-btn" disabled>Stop Stream</button>
|
| 274 |
+
<button id="reset-session-btn" disabled style="background-color: #FF5722;">Reset Session</button>
|
| 275 |
<select id="scene-select" disabled>
|
| 276 |
<option value="universal">Universal Mode</option>
|
| 277 |
<option value="gta_drive">GTA Drive Mode</option>
|