(type: MasterStreamType, captureImpl: typeof browserStreamCapture | typeof tauriStreamCapture)
| 483 | } |
| 484 | |
| 485 | private async acquireMasterStream(type: MasterStreamType, captureImpl: typeof browserStreamCapture | typeof tauriStreamCapture): Promise<void> { |
| 486 | try { |
| 487 | // Set up PiP overlay only on mobile, where the status/timer indicator is baked into |
| 488 | // a separate display stream. On desktop the overlay status is never set (see the |
| 489 | // isMobile() guard in ActiveAgentView), so the drawer would be a per-frame no-op and |
| 490 | // the desktop capture path skips the PiP canvas entirely. |
| 491 | if (isMobile() && type === 'display') { |
| 492 | tauriStreamCapture.setPipOverlayDrawer((ctx, width, height) => { |
| 493 | if (this.pipOverlayStatus) { |
| 494 | this.drawPipOverlay(ctx, width, height); |
| 495 | } |
| 496 | }); |
| 497 | } |
| 498 | |
| 499 | // Delegate to platform-specific implementation |
| 500 | await captureImpl.acquireMasterStream(type); |
| 501 | |
| 502 | // Sync local state from the implementation |
| 503 | const streams = captureImpl.getStreams(); |
| 504 | |
| 505 | if ('screenVideoStream' in streams) { |
| 506 | // Tauri streams |
| 507 | this.screenVideoStream = streams.screenVideoStream; |
| 508 | if (!isWeb()) { |
| 509 | // Tauri has separate PiP stream |
| 510 | const tauriStreams = streams as any; |
| 511 | this.screenVideoStreamWithPip = tauriStreams.screenVideoStreamWithPip || null; |
| 512 | } |
| 513 | this.screenAudioStream = streams.screenAudioStream; |
| 514 | this.cameraStream = streams.cameraStream; |
| 515 | this.microphoneStream = streams.microphoneStream; |
| 516 | } else { |
| 517 | // Browser streams |
| 518 | const browserStreams = streams as any; |
| 519 | if (browserStreams.masterDisplayStream) { |
| 520 | this.masterDisplayStream = browserStreams.masterDisplayStream; |
| 521 | } |
| 522 | if (browserStreams.masterCameraStream) { |
| 523 | this.masterCameraStream = browserStreams.masterCameraStream; |
| 524 | } |
| 525 | if (browserStreams.masterMicrophoneStream) { |
| 526 | this.masterMicrophoneStream = browserStreams.masterMicrophoneStream; |
| 527 | } |
| 528 | this.screenVideoStream = browserStreams.screenVideoStream; |
| 529 | this.screenAudioStream = browserStreams.screenAudioStream; |
| 530 | this.cameraStream = browserStreams.cameraStream; |
| 531 | this.microphoneStream = browserStreams.microphoneStream; |
| 532 | } |
| 533 | |
| 534 | // Create persistent video elements for capture (fixes iOS race condition) |
| 535 | // We await these to ensure the video has its first frame before the stream is considered "ready" |
| 536 | if (type === 'camera' && this.cameraStream && !this.cameraVideoElement) { |
| 537 | Logger.debug("StreamManager", "Creating persistent camera video element..."); |
| 538 | this.cameraVideoElement = await this.createPersistentVideoElement(this.cameraStream); |
| 539 | } |
| 540 | if (type === 'display' && this.screenVideoStream && !this.screenVideoElement) { |
| 541 | Logger.debug("StreamManager", "Creating persistent screen video element..."); |
| 542 | this.screenVideoElement = await this.createPersistentVideoElement(this.screenVideoStream); |
no test coverage detected