返回 DeepSeek-Reasonix
recorderRenderer.ts
根目录 / desktop / electron / src / main / browser / recorderRenderer.ts
1 /// <reference lib="dom" />
2 // This file is compiled independently, without Node integration or page code.
3 window.addEventListener("message", event => {
4 if (event.source !== window || event.data !== "reasonix-recorder-port" || !event.ports[0]) return;
5 const port = event.ports[0];
6 let recorder: MediaRecorder | undefined;
7 let stream: MediaStream | undefined;
8 let output: MediaStream | undefined;
9 let paintTimer: ReturnType<typeof setInterval> | undefined;
10 let bytes = 0;
11 let chunks: Blob[] = [];
12 let started = 0;
13 let announced = false;
14 let cancelled = false;
15 let writes = Promise.resolve();
16 const cleanup = () => { clearInterval(paintTimer); output?.getTracks().forEach(track => track.stop()); stream?.getTracks().forEach(track => track.stop()); stream = undefined; };
17 const fail = (error: unknown) => { cancelled = true; cleanup(); port.postMessage({ type: "error", message: String(error).slice(0, 300) }); };
18 port.onmessage = async ({ data }) => {
19 if (data.type === "cancel") { cancelled = true; if (recorder?.state === "recording") recorder.stop(); cleanup(); return; }
20 if (data.type === "stop") { if (recorder?.state === "recording") recorder.stop(); return; }
21 if (data.type !== "start" || recorder) return;
22 try {
23 // No frameRate: a static page sends only its first frame and the refresh a
24 // new sink asks for, and a capped track drops that refresh when it trails
25 // the first by less than a frame period. The paint interval sets output rate.
26 stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
27 if (cancelled) { cleanup(); return; }
28 const input = document.createElement("video"); input.muted = true; input.srcObject = stream;
29 await input.play();
30 const canvas = document.createElement("canvas"); canvas.width = data.width; canvas.height = data.height;
31 const context = canvas.getContext("2d");
32 if (!context || !canvas.width || !canvas.height) throw new Error("invalid recording viewport");
33 const paint = () => {
34 if (!input.videoWidth || !input.videoHeight) return;
35 context.drawImage(input, 0, 0, input.videoWidth * data.cropWidth, input.videoHeight * data.cropHeight, 0, 0, canvas.width, canvas.height);
36 (output?.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack | undefined)?.requestFrame();
37 };
38 output = canvas.captureStream(0);
39 const mimeType = ["video/webm;codecs=vp8", "video/webm"].find(type => MediaRecorder.isTypeSupported(type));
40 if (!mimeType) throw new Error("WebM recording is unavailable");
41 recorder = new MediaRecorder(output, { mimeType, videoBitsPerSecond: 3_000_000 });
42 recorder.ondataavailable = event => {
43 if (cancelled || !event.data.size) return;
44 bytes += event.data.size;
45 if (bytes > 64 * 1024 * 1024) { fail("recording exceeded 64 MiB"); recorder?.stop(); return; }
46 chunks.push(event.data);
47 if (!announced) { announced = true; port.postMessage({ type: "started", elapsedMs: performance.now() - started }); }
48 // Electron MessagePortMain can drop transferred ArrayBuffers. Structured
49 // clone preserves chunk ordering across the DOM-to-main boundary.
50 writes = writes.then(async () => { const chunk = await event.data.arrayBuffer(); port.postMessage({ type: "chunk", data: chunk }); });
51 };
52 recorder.onerror = event => fail(event.error);
53 recorder.onstop = async () => {
54 cleanup();
55 if (cancelled) return;
56 try {
57 await writes;
58 const blob = new Blob(chunks, { type: mimeType }); chunks = [];
59 const url = URL.createObjectURL(blob);
60 const video = document.createElement("video"); video.muted = true; video.src = url;
61 try {
62 await new Promise<void>((resolve, reject) => { video.onloadeddata = () => resolve(); video.onerror = () => reject(new Error("recorded WebM cannot be decoded")); });
63 if (!video.videoWidth || !video.videoHeight) throw new Error("recording has no video dimensions");
64 port.postMessage({ type: "stopped", width: video.videoWidth, height: video.videoHeight, durationMs: performance.now() - started, bytes });
65 } finally { video.removeAttribute("src"); video.load(); URL.revokeObjectURL(url); }
66 } catch (error) { fail(error); }
67 };
68 stream.getVideoTracks()[0]?.addEventListener("ended", () => { if (recorder?.state === "recording") fail("target page capture ended"); });
69 started = performance.now(); recorder.start(250);
70 paint(); paintTimer = setInterval(paint, 40);
71 } catch (error) { fail(error); }
72 };
73 port.postMessage({ type: "ready" });
74 }, { once: true });
75
75 lines TYPESCRIPT