* Create a persistent video element that stays attached to a stream. * Waits for the video to be fully ready (first frame rendered) before resolving. * This eliminates race conditions on frame capture, especially on iOS.
(stream: MediaStream, timeoutMs: number = 5000)
| 348 | * This eliminates race conditions on frame capture, especially on iOS. |
| 349 | */ |
| 350 | private async createPersistentVideoElement(stream: MediaStream, timeoutMs: number = 5000): Promise<HTMLVideoElement> { |
| 351 | const video = document.createElement('video'); |
| 352 | video.srcObject = stream; |
| 353 | video.muted = true; |
| 354 | video.playsInline = true; |
| 355 | |
| 356 | // Wait for video to be ready with first frame |
| 357 | await new Promise<void>((resolve, reject) => { |
| 358 | const timeout = setTimeout(() => { |
| 359 | Logger.warn("StreamManager", "Video element ready timeout - proceeding anyway"); |
| 360 | resolve(); |
| 361 | }, timeoutMs); |
| 362 | |
| 363 | const checkReady = () => { |
| 364 | // readyState >= 2 (HAVE_CURRENT_DATA) means we have at least one frame |
| 365 | if (video.readyState >= 2 && video.videoWidth > 0) { |
| 366 | clearTimeout(timeout); |
| 367 | resolve(); |
| 368 | return true; |
| 369 | } |
| 370 | return false; |
| 371 | }; |
| 372 | |
| 373 | // Check if already ready |
| 374 | if (checkReady()) return; |
| 375 | |
| 376 | // Listen for loadeddata event (fires when first frame is available) |
| 377 | video.addEventListener('loadeddata', () => { |
| 378 | if (checkReady()) return; |
| 379 | }, { once: true }); |
| 380 | |
| 381 | // Also listen for canplay as fallback |
| 382 | video.addEventListener('canplay', () => { |
| 383 | if (checkReady()) return; |
| 384 | }, { once: true }); |
| 385 | |
| 386 | // Start playing |
| 387 | video.play().catch(e => { |
| 388 | Logger.warn("StreamManager", `Video play failed: ${e}`); |
| 389 | clearTimeout(timeout); |
| 390 | reject(e); |
| 391 | }); |
| 392 | }); |
| 393 | |
| 394 | Logger.debug("StreamManager", `Video element ready: readyState=${video.readyState}, ${video.videoWidth}x${video.videoHeight}`); |
| 395 | return video; |
| 396 | } |
| 397 | |
| 398 | /** |
| 399 | * Capture a frame from a video stream. Returns base64 PNG or null. |
no test coverage detected