* Start capture with channel-based streaming. * Works on both desktop and iOS - frames are pushed from Rust as they arrive. * * On desktop: Shows selector window, waits for target selection, then starts xcap with channel * On iOS: Triggers ReplayKit picker, then HTTP-received frames are
(targetId?: string)
| 876 | * Returns MediaStreams for display and a cleanup function. |
| 877 | */ |
| 878 | async startCaptureStream(targetId?: string): Promise<CaptureStreamResult> { |
| 879 | if (!isTauri()) { |
| 880 | throw new Error('Screen capture only available in Tauri'); |
| 881 | } |
| 882 | |
| 883 | Logger.info("TAURI_STREAM", `Starting channel-based capture stream`); |
| 884 | |
| 885 | // On desktop, show selector and wait for target selection (unless a target is provided |
| 886 | // explicitly or was pre-seated via setPreselectedTarget). |
| 887 | let selectedTargetId: string | undefined = targetId ?? this.preselectedTargetId ?? undefined; |
| 888 | this.preselectedTargetId = null; // consume once |
| 889 | if (isDesktop() && !selectedTargetId) { |
| 890 | const selected = await this.waitForTargetSelection(); |
| 891 | if (!selected) { |
| 892 | throw new Error('Screen capture cancelled by user'); |
| 893 | } |
| 894 | selectedTargetId = selected; |
| 895 | } |
| 896 | |
| 897 | Logger.info("TAURI_STREAM", `Starting capture with target: ${selectedTargetId || 'default'}`); |
| 898 | |
| 899 | // Clean canvas (for AI/recording/preview). The PiP canvas only exists on mobile, |
| 900 | // where the status/timer overlay is baked into a separate stream. On desktop the |
| 901 | // overlay never draws (it's mobile-only), so a PiP canvas would be a pixel-identical |
| 902 | // copy of clean — a wasted per-frame drawImage plus a second canvas.captureStream() |
| 903 | // read-back. So on desktop we skip it and reuse the clean stream for the with-pip slot. |
| 904 | const usePip = !isDesktop(); |
| 905 | |
| 906 | const canvasClean = document.createElement('canvas'); |
| 907 | // Initial size - will be adapted to actual frame dimensions |
| 908 | canvasClean.width = 1920; |
| 909 | canvasClean.height = 1080; |
| 910 | const ctxClean = canvasClean.getContext('2d'); |
| 911 | if (!ctxClean) { |
| 912 | throw new Error('Failed to create canvas context'); |
| 913 | } |
| 914 | |
| 915 | const canvasPip = usePip ? document.createElement('canvas') : null; |
| 916 | const ctxPip = canvasPip ? canvasPip.getContext('2d') : null; |
| 917 | if (canvasPip) { |
| 918 | canvasPip.width = 1920; |
| 919 | canvasPip.height = 1080; |
| 920 | if (!ctxPip) { |
| 921 | throw new Error('Failed to create PiP canvas context'); |
| 922 | } |
| 923 | } |
| 924 | |
| 925 | // Create MediaStreams from canvases |
| 926 | const cleanStream = canvasClean.captureStream(30); |
| 927 | const pipStream = canvasPip ? canvasPip.captureStream(30) : cleanStream; |
| 928 | |
| 929 | let canvasSizeInitialized = false; |
| 930 | let frameCount = 0; |
| 931 | let isActive = true; |
| 932 | |
| 933 | // Create channels to receive frames and audio from Rust |
| 934 | const frameChannel = new Channel<FrameData>(); |
| 935 | const audioChannel = new Channel<AudioData>(); |
nothing calls this directly
no test coverage detected