* Start video-only capture stream. * Independent from audio - use startAudioStream separately if you also need audio. * Works on both desktop and iOS.
(targetId?: string)
| 461 | * Works on both desktop and iOS. |
| 462 | */ |
| 463 | async startVideoStream(targetId?: string): Promise<VideoStreamResult> { |
| 464 | if (!isTauri()) { |
| 465 | throw new Error('Screen capture only available in Tauri'); |
| 466 | } |
| 467 | |
| 468 | Logger.info("TAURI_STREAM", `Starting video-only capture stream`); |
| 469 | |
| 470 | // Desktop: Show selector and wait for target selection (unless a target is provided |
| 471 | // explicitly or was pre-seated via setPreselectedTarget). iOS: ReplayKit picker later. |
| 472 | let selectedTargetId: string | undefined = targetId ?? this.preselectedTargetId ?? undefined; |
| 473 | this.preselectedTargetId = null; // consume once, regardless of platform |
| 474 | if (isDesktop() && !selectedTargetId) { |
| 475 | const selected = await this.waitForTargetSelection(); |
| 476 | if (!selected) { |
| 477 | throw new Error('Screen capture cancelled by user'); |
| 478 | } |
| 479 | selectedTargetId = selected; |
| 480 | } |
| 481 | |
| 482 | Logger.info("TAURI_STREAM", `Starting video capture with target: ${selectedTargetId || 'iOS broadcast'}`); |
| 483 | |
| 484 | // Clean canvas (for AI/recording/preview). The PiP canvas only exists on mobile, |
| 485 | // where the status/timer overlay is baked into a separate stream. On desktop the |
| 486 | // overlay never draws (it's mobile-only), so a PiP canvas would be a pixel-identical |
| 487 | // copy of clean — a wasted per-frame drawImage plus a second canvas.captureStream() |
| 488 | // read-back. So on desktop we skip it and reuse the clean stream for the with-pip slot. |
| 489 | const usePip = !isDesktop(); |
| 490 | |
| 491 | const canvasClean = document.createElement('canvas'); |
| 492 | canvasClean.width = 1920; |
| 493 | canvasClean.height = 1080; |
| 494 | const ctxClean = canvasClean.getContext('2d'); |
| 495 | if (!ctxClean) { |
| 496 | throw new Error('Failed to create canvas context'); |
| 497 | } |
| 498 | |
| 499 | const canvasPip = usePip ? document.createElement('canvas') : null; |
| 500 | const ctxPip = canvasPip ? canvasPip.getContext('2d') : null; |
| 501 | if (canvasPip) { |
| 502 | canvasPip.width = 1920; |
| 503 | canvasPip.height = 1080; |
| 504 | if (!ctxPip) { |
| 505 | throw new Error('Failed to create PiP canvas context'); |
| 506 | } |
| 507 | } |
| 508 | |
| 509 | const cleanStream = canvasClean.captureStream(30); |
| 510 | const pipStream = canvasPip ? canvasPip.captureStream(30) : cleanStream; |
| 511 | |
| 512 | let canvasSizeInitialized = false; |
| 513 | let frameCount = 0; |
| 514 | let isActive = true; |
| 515 | |
| 516 | const frameChannel = new Channel<FrameData>(); |
| 517 | |
| 518 | // Coalescing decode pump: we only ever hold the NEWEST undecoded frame and decode |
| 519 | // one at a time. If frames arrive faster than we can decode+draw, the intermediate |
| 520 | // ones are dropped instead of queuing — so latency stays at ~one frame no matter how |
no test coverage detected