* 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)
| 326 | * This eliminates race conditions on frame capture, especially on iOS. |
| 327 | */ |
| 328 | private async createPersistentVideoElement(stream: MediaStream, timeoutMs: number = 5000): Promise<HTMLVideoElement> { |
| 329 | const video = document.createElement('video'); |
| 330 | video.srcObject = stream; |
| 331 | video.muted = true; |
| 332 | video.playsInline = true; |
| 333 | |
| 334 | // Wait for video to be ready with first frame |
| 335 | await new Promise<void>((resolve, reject) => { |
| 336 | const timeout = setTimeout(() => { |
| 337 | Logger.warn("StreamManager", "Video element ready timeout - proceeding anyway"); |
| 338 | resolve(); |
| 339 | }, timeoutMs); |
| 340 | |
| 341 | const checkReady = () => { |
| 342 | // readyState >= 2 (HAVE_CURRENT_DATA) means we have at least one frame |
| 343 | if (video.readyState >= 2 && video.videoWidth > 0) { |
| 344 | clearTimeout(timeout); |
| 345 | resolve(); |
| 346 | return true; |
| 347 | } |
| 348 | return false; |
| 349 | }; |
| 350 | |
| 351 | // Check if already ready |
| 352 | if (checkReady()) return; |
| 353 | |
| 354 | // Listen for loadeddata event (fires when first frame is available) |
| 355 | video.addEventListener('loadeddata', () => { |
| 356 | if (checkReady()) return; |
| 357 | }, { once: true }); |
| 358 | |
| 359 | // Also listen for canplay as fallback |
| 360 | video.addEventListener('canplay', () => { |
| 361 | if (checkReady()) return; |
| 362 | }, { once: true }); |
| 363 | |
| 364 | // Start playing |
| 365 | video.play().catch(e => { |
| 366 | Logger.warn("StreamManager", `Video play failed: ${e}`); |
| 367 | clearTimeout(timeout); |
| 368 | reject(e); |
| 369 | }); |
| 370 | }); |
| 371 | |
| 372 | Logger.debug("StreamManager", `Video element ready: readyState=${video.readyState}, ${video.videoWidth}x${video.videoHeight}`); |
| 373 | return video; |
| 374 | } |
| 375 | |
| 376 | /** |
| 377 | * Capture a frame from a video stream. Returns base64 PNG or null. |
no test coverage detected