Skip to content

Instantly share code, notes, and snippets.

@sajadjafari
Last active August 8, 2022 20:50
Show Gist options
  • Save sajadjafari/e3f5d2edfbbb1e7c0750af8224c6534d to your computer and use it in GitHub Desktop.
Save sajadjafari/e3f5d2edfbbb1e7c0750af8224c6534d to your computer and use it in GitHub Desktop.
MediaRecorder stream switcher inside a fake rtc peer connection.
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