File size: 3,451 Bytes
3337c5d 7902802 3337c5d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | /**
* Authrix Extension — Offscreen Document
*
* Offscreen documents can use getUserMedia with chromeMediaSource:'tab',
* which is NOT available in content scripts or service workers in MV3.
*/
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'RECORD_OFFSCREEN') {
startRecording(msg.streamId, msg.durationMs, msg.tabId)
.then(() => sendResponse({ ok: true }))
.catch(e => sendResponse({ ok: false, error: e.message }));
return true;
}
});
let activeRecorder = null;
async function startRecording(streamId, durationMs, tabId) {
if (activeRecorder && activeRecorder.state === 'recording') {
activeRecorder.stop();
}
const stream = await navigator.mediaDevices.getUserMedia({
video: {
mandatory: {
chromeMediaSource: 'tab',
chromeMediaSourceId: streamId,
},
},
audio: {
mandatory: {
chromeMediaSource: 'tab',
chromeMediaSourceId: streamId,
},
},
});
const mimeType = getSupportedMimeType();
const recorder = new MediaRecorder(stream, {
mimeType,
videoBitsPerSecond: 4_000_000, // 4Mbps — good quality, smaller file
});
activeRecorder = recorder;
const chunks = [];
recorder.ondataavailable = e => {
if (e.data && e.data.size > 0) chunks.push(e.data);
};
recorder.onstop = async () => {
stream.getTracks().forEach(t => t.stop());
try {
const blob = new Blob(chunks, { type: mimeType });
// Convert blob to ArrayBuffer (more reliable than base64 for large files)
const arrayBuffer = await blob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Chunk into 1MB pieces to avoid message size limits
const CHUNK_SIZE = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(uint8Array.length / CHUNK_SIZE);
const chunkedData = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, uint8Array.length);
const chunk = uint8Array.slice(start, end);
// Convert to regular array for JSON serialization
chunkedData.push(Array.from(chunk));
}
chrome.runtime.sendMessage({
type: 'BLOB_READY',
chunks: chunkedData,
mimeType,
tabId,
totalSize: uint8Array.length,
});
} catch (err) {
chrome.runtime.sendMessage({
type: 'RECORD_ERROR',
error: err.message,
tabId,
});
}
};
recorder.onerror = e => {
chrome.runtime.sendMessage({
type: 'RECORD_ERROR',
error: e.error?.message || 'MediaRecorder error',
tabId,
});
};
recorder.start(1000);
setTimeout(() => {
if (recorder.state === 'recording') recorder.stop();
}, durationMs);
// Progress ticks
const startTime = Date.now();
const tick = setInterval(() => {
if (recorder.state !== 'recording') { clearInterval(tick); return; }
const elapsed = Math.round((Date.now() - startTime) / 1000);
const total = Math.round(durationMs / 1000);
chrome.runtime.sendMessage({ type: 'RECORD_PROGRESS', elapsed, total, tabId });
}, 800);
}
function getSupportedMimeType() {
const types = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm',
'video/mp4',
];
return types.find(t => MediaRecorder.isTypeSupported(t)) || 'video/webm';
}
|