Last active
May 28, 2023 17:53
-
-
Save Tubeee/2419333a7b00b34892c76c9390207a94 to your computer and use it in GitHub Desktop.
WebRTC Recorder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name WebRTC recorder | |
| // @namespace Violentmonkey Scripts | |
| // @version 0.1 | |
| // @description Record WebRTC media/data streams | |
| // @author Tubeee | |
| // @match https://*.discord.com/** | |
| // @match https://discord.com/** | |
| // @grant none | |
| // ==/UserScript== | |
| { | |
| const TIMESLICE = 10_000 | |
| mediaStreams = [] | |
| mediaRecorders = [] | |
| dataChannels = [] | |
| fileWritableStreams = [] | |
| async function initFileSave(object) { | |
| const isDataChannel = object instanceof RTCDataChannel | |
| let i = 0; | |
| //for await (const filename of mrSaveDirHandle.keys()) | |
| // if (filename.startsWith(object.label)) { | |
| // const number = +filename.match(/_\d+$/)?.[0] || 0 | |
| // i = number > i ? number : i | |
| // } | |
| try { | |
| await window.mrSaveDirHandle.getFileHandle(object.label) | |
| for (i = 1; i < 100; i++) //assuming something's awry if i > 100 | |
| await window.mrSaveDirHandle.getFileHandle(obect.label + '_' + i) | |
| } catch { } | |
| const saveFileStream = await (await window.mrSaveDirHandle.getFileHandle(object.label + (i ? '_' + i : ''), { create: true })).createWritable() | |
| fileWritableStreams.push(saveFileStream) | |
| if (object.recordedChunks) saveFileStream.write(new Blob( | |
| isDataChannel ? object.recordedChunks.map(blob => [Uint32Array.from([blob.size]), blob].flat()) : object.recordedChunks | |
| )) | |
| object.saveChunk = isDataChannel ? function (data) { | |
| const blob = new Blob(data) | |
| saveFileStream.write(new Blob([Uint32Array([blob.size]), blob])) | |
| } : saveFileStream.write.bind(saveFileStream) | |
| //object.addEventListener(isDataChannel ? 'close' : 'stop', saveFileStream.close.bind(saveFileStream)) | |
| } | |
| function recordMediaStream(...mediaStreams) { | |
| for (const mediaStream of mediaStreams) { | |
| if (window.mediaStreams.includes(mediaStream)) continue | |
| window.mediaStreams.push(mediaStream) | |
| mediaStream.onaddtrack = console.log | |
| const mediaRecorder = new MediaRecorder(mediaStream) | |
| mediaRecorders.push(mediaRecorder) | |
| mediaRecorder.__defineGetter__('label', function () { return this.stream.getTracks()?.[0]?.label }) | |
| mediaRecorder.saveChunk = function (chunk) { (this.recordedChunks ??= []).push(chunk) } | |
| mediaRecorder.ondataavailable = function (event) { | |
| console.log('ondataavailable', event) | |
| this.saveChunk(event.data) | |
| } | |
| if (window.mrSaveDirHandle) initFileSave(mediaRecorder) | |
| mediaStream.oninactive = e => console.dir('[MR] MediaStream inactive', e) | |
| if (!mediaStream.active) | |
| mediaStream.onactive = function () { | |
| mediaRecorder.start(TIMESLICE) | |
| console.log('[MR] MediaStream active. Starting recording.', this) | |
| } | |
| else { | |
| mediaRecorder.start(TIMESLICE) | |
| console.log('isactive', this) | |
| } | |
| } | |
| } | |
| function recordDataChannel(channel) { | |
| channel.saveChunk = function (data) { (this.recordedChunks ??= []).push(new Blob(data)) } | |
| channel.addEventListener('message', ({ data }) => channel.saveChunk(data)) | |
| if (window.mrSaveDirHandle) initFileSave(channel) | |
| } | |
| webkitMediaStream = MediaStream = new Proxy(MediaStream, { | |
| construct() { | |
| const instance = Reflect.construct(...arguments) | |
| recordMediaStream(instance) | |
| return instance | |
| } | |
| }) | |
| webkitRTCPeerConnection = RTCPeerConnection = new Proxy(RTCPeerConnection, { | |
| construct() { | |
| console.log('[MR] RTCPeerConnection created') | |
| const instance = Reflect.construct(...arguments) | |
| instance.addEventListener('addstream', ({ stream }) => recordMediaStream(stream)) | |
| instance.addEventListener('track', ({ streams }) => recordMediaStream(...streams)) | |
| instance.addEventListener('datachannel', ({ channel }) => recordChannel(channel)) | |
| return instance | |
| } | |
| }) | |
| RTCPeerConnection.prototype.createDataChannel = new Proxy(RTCPeerConnection.prototype.createDataChannel, { | |
| apply() { | |
| const instance = Reflect.construct(...arguments) | |
| recordChannel(instance) | |
| return instance | |
| } | |
| }) | |
| function pickSaveDirectory() { | |
| addEventListener('click', async () => { | |
| window.mrSaveDirHandle = await showDirectoryPicker() | |
| window.mrSaveFileHandles = new Map() | |
| for (const mediaRecorder of mediaRecorders) | |
| initFileSave(mediaRecorder) | |
| for (const dataChannel of dataChannels) | |
| initFileSave(dataChannel) | |
| addEventListener('beforeunload', () => { | |
| fileWritableStreams.forEach(s => { try { s.close() } catch { } }) | |
| //mediaRecorders.forEach(mediaRecorder => mediaRecorder.stop()) | |
| //dataChannels.forEach(channel => channel.close()) | |
| }) | |
| }, { once: true }) | |
| } | |
| function pickSaveDirectory() { | |
| addEventListener('click', async () => { | |
| let p | |
| let saveDirRoot | |
| let pr = new Promise(r => p = r) | |
| if ((await indexedDB.databases()).find(e => e.name === 'recFileStore')) | |
| indexedDB.open('recFileStore').onsuccess = ({ target: { result: db } }) => { | |
| db.transaction('rootHandle').objectStore('rootHandle').get('rootHandle').onsuccess = e => { | |
| saveDirRoot = e.target.result | |
| p() | |
| } | |
| } | |
| else { | |
| saveDirRoot = await showDirectoryPicker() | |
| indexedDB.open('recFileStore').onupgradeneeded = ({ target: { result: db } }) => { | |
| db.createObjectStore('rootHandle').put(saveDirRoot, 'rootHandle') | |
| p() | |
| } | |
| } | |
| await pr | |
| await saveDirRoot.requestPermission({ mode: 'readwrite' }) | |
| window.mrSaveDirHandle = await saveDirRoot.getDirectoryHandle(`${Date.now()}`, { create: true }) | |
| for (const mediaRecorder of mediaRecorders) initFileSave(mediaRecorder) | |
| for (const dataChannel of dataChannels) initFileSave(dataChannel) | |
| fileWritableStreams.forEach(s => { try { s.close() } catch { } }) | |
| //mediaRecorders.forEach(mediaRecorder => mediaRecorder.stop()) | |
| //dataChannels.forEach(channel => channel.close()) | |
| addEventListener('beforeunload', () => fileWritableStreams.forEach(s => { try { s.close() } catch { } })) | |
| }, { once: true }) | |
| } | |
| pickSaveDirectory() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment