(request: StartRecordingRequest)
| 817 | }; |
| 818 | |
| 819 | const startRecording = async (request: StartRecordingRequest) => { |
| 820 | if (activeRecording || startInProgress || retryInProgress) { |
| 821 | // Thrown before this attempt owns anything, so the cleanup below must |
| 822 | // never run for it: a duplicate start would otherwise tear down — and |
| 823 | // delete server-side — the live recording it was rejected to protect. |
| 824 | throw new Error("Recording is already active"); |
| 825 | } |
| 826 | startInProgress = true; |
| 827 | startCancelRequested = false; |
| 828 | |
| 829 | // Everything acquired by THIS attempt. The failure path releases exactly |
| 830 | // these; module state like activeRecording is only touched when this |
| 831 | // attempt is the one that set it. |
| 832 | const ownedStreams: MediaStream[] = []; |
| 833 | let ownedVideoId: string | null = null; |
| 834 | let ownedSpool: RecordingSpool | null = null; |
| 835 | let ownedRecording: ActiveRecording | null = null; |
| 836 | |
| 837 | try { |
| 838 | status = { phase: "creating" }; |
| 839 | |
| 840 | const mainStream = await getMainStream(request); |
| 841 | ownedStreams.push(mainStream); |
| 842 | throwIfStartCanceled(); |
| 843 | const captureSource = getCaptureSource(request, mainStream); |
| 844 | if (captureSource) { |
| 845 | broadcastCaptureSource(captureSource); |
| 846 | } |
| 847 | const microphoneStream = await getMicrophoneStream( |
| 848 | request.settings.microphone, |
| 849 | request.mode, |
| 850 | ); |
| 851 | if (microphoneStream) { |
| 852 | ownedStreams.push(microphoneStream); |
| 853 | } |
| 854 | throwIfStartCanceled(); |
| 855 | const { width, height, fps } = getStreamSize(mainStream); |
| 856 | const videoTracks = mainStream.getVideoTracks(); |
| 857 | if (videoTracks.length === 0) { |
| 858 | throw new Error("No video track was captured"); |
| 859 | } |
| 860 | const recordingStream = new MediaStream(videoTracks); |
| 861 | const streams = microphoneStream |
| 862 | ? [mainStream, microphoneStream] |
| 863 | : [mainStream]; |
| 864 | const audioContext = addAudioTracks({ |
| 865 | output: recordingStream, |
| 866 | streams, |
| 867 | routeFirstStreamToSpeakers: request.mode === "tab", |
| 868 | }); |
| 869 | const hasAudio = recordingStream.getAudioTracks().length > 0; |
| 870 | const pipeline = selectRecordingPipeline(hasAudio); |
| 871 | if (!pipeline) throw new Error("No supported recorder format is available"); |
| 872 | |
| 873 | const { videoCodec, audioCodec } = describeRecordingCodecs( |
| 874 | pipeline.mimeType, |
| 875 | hasAudio, |
| 876 | ); |
no test coverage detected