Last active
August 8, 2022 20:50
-
-
Save sajadjafari/e3f5d2edfbbb1e7c0750af8224c6534d to your computer and use it in GitHub Desktop.
MediaRecorder stream switcher inside a fake rtc peer connection.
This file contains 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
class MediaRecorderSwitch { | |
PC1: RTCPeerConnection; | |
PC2: RTCPeerConnection; | |
tracksAdded: number; | |
recorder: MediaRecorder; | |
chunks: Blob[]; | |
FPS: 60; | |
recorderMimeType: string; | |
videoCodec: string; | |
audioCodec: string; | |
profileLevelId: string; | |
constructor(opts: any) { | |
this.recorderMimeType = opts.recorderMimeType || 'video/webm;codecs:vp8, opus'; | |
this.videoCodec = opts.videoCodec || 'video/vp8'; // video/vp8, video/vp9, video/rtx, video/h264, video/av1, video/ulpfec, video/red | |
this.audioCodec = opts.audioCodec || 'audio/opus'; | |
this.profileLevelId = opts.profileLevelId || null; // for example for video/h264 -> ['42001f', '42e01f', '4d001f', '640032'] | |
this.chunks = []; | |
} | |
changeStream = (stream: MediaStream) => { | |
if ( | |
stream && | |
stream.constructor.name === 'MediaStream' && | |
this.PC1 && | |
this.PC2 && | |
this.PC1.connectionState === 'connected' && | |
this.PC2.connectionState === 'connected' | |
) { | |
stream.getTracks().forEach((track: MediaStreamTrack) => { | |
if ( | |
track && | |
(track.constructor.name === 'MediaStreamTrack' || track.constructor.name === 'CanvasCaptureMediaStreamTrack') | |
) { | |
const senders = this.PC1.getSenders().filter(sender => !!sender.track && sender.track.kind === track.kind); | |
if (senders.length) | |
senders[0] | |
.replaceTrack(track) | |
.then(() => { | |
console.log('MediaSwitcher: Track has been replaced', track); | |
}) | |
.catch(err => { | |
console.log('MediaSwitcher: Failed to replace track', track, err); | |
}); | |
} | |
}); | |
} | |
}; | |
addTransceiver(track: MediaStreamTrack, stream: MediaStream) { | |
const init: RTCRtpTransceiverInit = { | |
direction: 'sendonly', | |
streams: [stream], | |
}; | |
if (track.kind === 'video') { | |
init.sendEncodings = [ | |
{ | |
rid: 'f', | |
maxBitrate: 30_000_000, | |
maxFramerate: 50.0, | |
}, | |
]; | |
} | |
const transceiver = this.PC1.addTransceiver(track, init); | |
this.setPreferredCodecs(transceiver, track); | |
} | |
setPreferredCodecs(transceiver: RTCRtpTransceiver, track: MediaStreamTrack) { | |
if ('setCodecPreferences' in transceiver) { | |
let selCodec; | |
const cap = RTCRtpSender.getCapabilities(track.kind); | |
if (!cap) return; | |
if (track.kind === 'video') { | |
const allCodecProfiles = cap.codecs.filter(c => { | |
return c.mimeType.toLowerCase() === this.videoCodec; | |
}); | |
if (!allCodecProfiles) return; | |
if (this.profileLevelId) { | |
selCodec = allCodecProfiles.find(c => { | |
return c.sdpFmtpLine && c.sdpFmtpLine?.indexOf(`profile-level-id=${this.profileLevelId}`) >= 0; | |
}); | |
} | |
if (!selCodec) [selCodec] = allCodecProfiles; | |
} else { | |
selCodec = cap.codecs.find(c => { | |
return c.mimeType.toLowerCase() === this.videoCodec || c.mimeType.toLowerCase() === this.audioCodec; | |
}); | |
} | |
if (selCodec) { | |
console.log('Setting Codec Preferences for track', track.kind.toUpperCase(), selCodec); | |
transceiver.setCodecPreferences([selCodec]); | |
} | |
} | |
} | |
handleIceCandidate = (pc: RTCPeerConnection) => { | |
return (e: RTCPeerConnectionIceEvent) => { | |
pc.addIceCandidate(e.candidate).catch(error => console.error(error)); | |
}; | |
}; | |
initialize(stream: MediaStream) { | |
if ( | |
!stream || | |
(stream.constructor.name !== 'MediaStream' && stream.constructor.name !== 'CanvasCaptureMediaStream') | |
) { | |
console.error('Input stream is nonexistent or invalid.'); | |
return; | |
} | |
this.tracksAdded = 0; | |
this.PC1 = new RTCPeerConnection(null); | |
this.PC2 = new RTCPeerConnection(null); | |
this.PC1.onicecandidate = this.handleIceCandidate(this.PC2); | |
this.PC2.onicecandidate = this.handleIceCandidate(this.PC1); | |
this.PC2.ontrack = (e: RTCTrackEvent) => { | |
this.tracksAdded += 1; | |
if (stream.getTracks().length === this.tracksAdded) { | |
this.start(e.streams[0]); | |
} | |
}; | |
// Add stream to input peer | |
stream.getTracks().forEach(track => { | |
// this.PC1.addTrack(track, stream); | |
this.addTransceiver(track, stream); | |
}); | |
// Make RTC call | |
this.PC1.createOffer().then(offer => { | |
// Offer | |
this.PC1.setLocalDescription(offer); | |
this.PC2.setRemoteDescription(offer); | |
// Answer | |
this.PC2.createAnswer().then(answer => { | |
this.PC2.setLocalDescription(answer); | |
this.PC1.setRemoteDescription(answer); | |
}); | |
}); | |
} | |
start(stream: MediaStream) { | |
let newStream: MediaStream; | |
const video = document.createElement('video'); | |
video.autoplay = true; | |
video.muted = true; | |
video.srcObject = stream; | |
video | |
.play() | |
.then(() => { | |
if (video.captureStream) newStream = video.captureStream(this.FPS); | |
else if (video.mozCaptureStream) newStream = video.mozCaptureStream(this.FPS); | |
else { | |
console.log('Something went wrong on sending stream chunks (Can not capture stream)'); | |
return; | |
} | |
this.record(newStream); | |
}) | |
.catch(error => { | |
console.log('Something went wrong on playing stream capture', error); | |
}); | |
} | |
record(stream: MediaStream) { | |
this.recorder = new MediaRecorder(stream, { | |
mimeType: this.recorderMimeType, | |
}); | |
this.recorder.ondataavailable = event => { | |
this.chunks.push(event.data); | |
}; | |
this.recorder.onstop = () => { | |
this.download(); | |
}; | |
this.recorder.start(2000); | |
} | |
download() { | |
const data = new Blob(this.chunks, { type: 'video/webm' }); | |
const url = window.URL.createObjectURL(data); | |
const a = document.createElement('a'); | |
a.style.display = 'none'; | |
a.href = url; | |
a.download = 'test.webm'; | |
document.body.appendChild(a); | |
a.click(); | |
setTimeout(() => { | |
document.body.removeChild(a); | |
window.URL.revokeObjectURL(url); | |
}, 100); | |
} | |
stop() { | |
this.recorder.stop(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment