Created
October 12, 2021 01:37
-
-
Save karlstolley/9a4b78fd5eccd1c05c3ded639db2fc47 to your computer and use it in GitHub Desktop.
RTC 2021 Code Examples
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
// 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)); | |
} | |
} | |
}; | |
} |
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
// 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); | |
// | |
} |
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
// 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