Skip to content

Instantly share code, notes, and snippets.

@Tubeee
Last active May 28, 2023 17:53
Show Gist options
  • Select an option

  • Save Tubeee/2419333a7b00b34892c76c9390207a94 to your computer and use it in GitHub Desktop.

Select an option

Save Tubeee/2419333a7b00b34892c76c9390207a94 to your computer and use it in GitHub Desktop.
WebRTC Recorder
// ==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