Skip to content

Instantly share code, notes, and snippets.

@karlstolley
Created October 12, 2021 01:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karlstolley/9a4b78fd5eccd1c05c3ded639db2fc47 to your computer and use it in GitHub Desktop.
Save karlstolley/9a4b78fd5eccd1c05c3ded639db2fc47 to your computer and use it in GitHub Desktop.
RTC 2021 Code Examples
// Create a data channel for exchanging
// feature-detection information. Here,
// just the default `binaryType` on the
// data channel itself:
function addFeaturesChannel(peer) {
peer.featuresChannel =
peer.connection.createDataChannel('features',
{ negotiated: true, id: 60 });
peer.featuresChannel.onopen = function(event) {
$self.features.binaryType = peer.featuresChannel.binaryType;
// Other feature-detection logic could go here...
peer.featuresChannel.send(JSON.stringify($self.features));
};
peer.featuresChannel.onmessage = function(event) {
peer.features = JSON.parse(event.data);
};
}
// Logic for sending a file over an asymmetric
// data channel
function sendFile(peer, prefix, metadata, file) {
const dc = peer.connection.createDataChannel(`${prefix}${metadata.name}`);
const chunk = 8 * 1024; // 8K chunks
dc.onopen = async function() {
if (!peer.features ||
($self.features.binaryType !== peer.features.binaryType)) {
dc.binaryType = 'arraybuffer';
}
// Prepare data according to the binaryType in use
const data = dc.binaryType === 'blob' ? file : await file.arrayBuffer();
// Send the metadata
dc.send(JSON.stringify(metadata));
// Send the prepared data in chunks
for (let i = 0; i < metadata.size; i += chunk) {
dc.send(data.slice(i, i + chunk));
}
};
dc.onmessage = function({ data }) {
// Sending side will only ever receive a response
handleResponse(JSON.parse(data));
dc.close();
};
}
// Logic for receiving a file
function receiveFile(dc) {
const chunks = [];
let metadata;
let bytesReceived = 0;
dc.onmessage = function({ data }) {
// Receive the metadata
if (typeof data === 'string' && data.startsWith('{')) {
metadata = JSON.parse(data);
} else {
// Receive and squirrel away chunks...
bytesReceived += data.size ? data.size : data.byteLength;
chunks.push(data);
// ...until the bytes received equal the file size
if (bytesReceived === metadata.size) {
const image = new Blob(chunks, { type: metadata.type });
const response = {
id: metadata.timestamp,
timestamp: Date.now()
};
appendMessage('peer', '#chat-log', metadata, image);
// Send an acknowledgement
try {
dc.send(JSON.stringify(response));
} catch(e) {
queueMessage(response);
}
}
}
};
}
// Logic for sending a file over an asymmetric
// data channel.
// `metadata` would be a custom JSON structure
// for the file's name, size, MIME type, and
// a timestamp for when it was sent.
function sendFile(peer, metadata, file) {
const dc = peer.connection.createDataChannel(`${metadata.name}`);
const chunk = 8 * 1024; // 8K chunks
dc.onopen = async function() {
if (!peer.features ||
($self.features.binaryType !== peer.features.binaryType)) {
dc.binaryType = 'arraybuffer';
}
// Prepare data according to the binaryType in use
const data = dc.binaryType === 'blob' ? file : await file.arrayBuffer();
// Send the metadata
dc.send(JSON.stringify(metadata));
// Send the prepared data in chunks
for (let i = 0; i < metadata.size; i += chunk) {
dc.send(data.slice(i, i + chunk));
}
};
dc.onmessage = function({ data }) {
// Sending side will only ever receive a response
handleResponse(JSON.parse(data));
dc.close();
};
}
// Logic for receiving a file
function receiveFile(dc) {
const chunks = [];
let metadata;
let bytesReceived = 0;
dc.onmessage = function({ data }) {
// Receive the metadata
if (typeof data === 'string' && data.startsWith('{')) {
metadata = JSON.parse(data);
} else {
// Receive and squirrel away chunks...
bytesReceived += data.size ? data.size : data.byteLength;
chunks.push(data);
// ...until the bytes received equal the file size
if (bytesReceived === metadata.size) {
const file = new Blob(chunks, { type: metadata.type });
const response = {
id: metadata.timestamp,
timestamp: Date.now()
};
// ... do something with the received file ...
// Acknowledge receipt of file
dc.send(JSON.stringify(response));
}
}
};
}
// Safari 15 on iOS, and sometimes on MacOS,
// seems to ignore the `autoplay` attribute,
// rendering only the first frame of a video
// stream (local or incoming).
// This is hacky and clunky, but it does work.
// I'm continuing to hunt down the source of
// this error and will file a bug report once
// I have the details clear.
function displayStream(video_id, stream) {
const video = document.querySelector(video_id);
video.srcObject = stream;
// Register a click event on the video
video.addEventListener('click', function(e) {
e.target.play();
});
// Issue an async artificial click
setTimeout(function() {
video.click();
}, 10);
//
}
// Signal callback
async function handleScSignal({ description, candidate }) {
if (description) {
if (description.type === '_reset') {
resetAndRetryConnection($peer);
return;
}
const readyForOffer =
!$self.isMakingOffer &&
($peer.connection.signalingState === 'stable'
|| $self.isSettingRemoteAnswerPending);
const offerCollision = description.type === 'offer' && !readyForOffer;
$self.isIgnoringOffer = !$self.isPolite && offerCollision;
if ($self.isIgnoringOffer) {
return;
}
$self.isSettingRemoteAnswerPending = description.type === 'answer';
console.log('Signaling state on incoming description:',
$peer.connection.signalingState);
try {
await $peer.connection.setRemoteDescription(description);
} catch(e) {
// For whatever reason, we cannot SRD.
// Reset and retry the connection.
resetAndRetryConnection($peer);
return;
}
$self.isSettingRemoteAnswerPending = false;
if (description.type === 'offer') {
// generate an answer
try {
// run SLD the modern way, to set an answer
await $peer.connection.setLocalDescription();
} catch(e) {
// or, run SLD the old-school way, by manually
// creating an answer, and passing it to SLD
const answer = await $peer.connection.createAnswer();
await $peer.connection.setLocalDescription(answer);
} finally {
// finally, however this was done, send the
// localDescription (answer) to the remote peer
sc.emit('signal',
{ description:
$peer.connection.localDescription });
// also, the polite peer no longer has to suppress
// initial offers:
$self.isSuppressingInitialOffer = false;
}
}
} else if (candidate) {
console.log('Received ICE candidate:', candidate);
try {
await $peer.connection.addIceCandidate(candidate);
} catch(e) {
if (!$self.isIgnoringOffer) {
console.error('Cannot add ICE candidate for peer', e);
}
}
}
}
// Reset and retry logic
function resetAndRetryConnection(peer) {
resetCall(peer);
$self.isMakingOffer = false;
$self.isIgnoringOffer = false;
$self.isSettingRemoteAnswerPending = false;
// Polite peer must suppress initial offer
$self.isSuppressingInitialOffer = $self.isPolite;
registerRtcEvents(peer); // register RTC events
establishCallFeatures(peer); // add media tracks, negotiated data channels, etc.
// Let the remote peer know we're resetting
if ($self.isPolite) {
sc.emit('signal',
{ description:
{ type: '_reset'}
});
}
}
// Suppress initial offer on `negotiationneeded` callback
async function handleRtcNegotiation() {
// Don't make an initial offer if suppressing
if ($self.isSuppressingInitialOffer) return;
console.log('RTC negotiation needed...');
// send an SDP description
$self.isMakingOffer = true;
try {
// run SLD the modern way...
await $peer.connection.setLocalDescription();
} catch(e) {
// or, run SLD the old-school way, by manually
// creating an offer, and passing it to SLD
const offer = await $peer.connection.createOffer();
await $peer.connection.setLocalDescription(offer);
} finally {
// finally, however this was done, send the
// localDescription to the remote peer
sc.emit('signal', { description:
$peer.connection.localDescription });
}
$self.isMakingOffer = false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment