Skip to content

Instantly share code, notes, and snippets.

@p7g
Last active December 8, 2022 00:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save p7g/7bd59bf33b7c5b2ecbaae5004ec95eda to your computer and use it in GitHub Desktop.
Save p7g/7bd59bf33b7c5b2ecbaae5004ec95eda to your computer and use it in GitHub Desktop.
webrtc demo

how to use

You are the signalling server

  1. turn on camera on both (dunno if required)
  2. create a peer on both
  3. create offer on one, paste into "add remote session description form" and submit
  4. copy answer from previous step and paste into "add remote session description form" in other client, submit
  5. copy ice candidates array from each client and paste into "add ice candidate" on other, submit both

now you should see video from the other client on each.

what doesn't work

  • audio (probably works but the video players are all muted)
  • changing video device
  • turning off video
<!DOCTYPE html>
<html>
<body>
<div>
<select id="cameraSelect"></select>
<label>
<input type="checkbox" id="videoEnabled" />
Enable video
</label>
</div>
<video id="myVideo" autoplay playsinline muted></video>
<div id="peerContainer">
<button id="addPeer" style="display: block">Add peer</button>
</div>
<script type="module" src="./client.js"></script>
</body>
</html>
// ---- P2P
const ICE_SERVERS = [
// https://www.metered.ca/tools/openrelay/
{
urls: "stun:openrelay.metered.ca:80",
},
{
urls: "turn:openrelay.metered.ca:80",
username: "openrelayproject",
credential: "openrelayproject",
},
{
urls: "turn:openrelay.metered.ca:443",
username: "openrelayproject",
credential: "openrelayproject",
},
{
urls: "turn:openrelay.metered.ca:443?transport=tcp",
username: "openrelayproject",
credential: "openrelayproject",
},
];
function createPeerConnection(videoStream) {
const peer = new RTCPeerConnection({ iceServers: ICE_SERVERS });
for (const track of videoStream.getTracks()) {
console.log(track, videoStream);
peer.addTrack(track, videoStream);
}
return peer;
}
function createPeerVideo(peerConnection) {
const videoEl = document.createElement("video");
videoEl.setAttribute("muted", "muted");
videoEl.setAttribute("autoplay", "autoplay");
videoEl.setAttribute("playsinline", "playsinline");
peerConnection.addEventListener("track", event => {
event.track.onunmute = () => {
videoEl.srcObject = event.streams[0];
};
});
return videoEl;
}
function createIceCandidateList(peerConnection) {
const container = document.createElement("div");
const pre = document.createElement("pre");
container.append("ice candidates", pre);
container.style.border = "1px solid black";
const candidates = [];
peerConnection.addEventListener("icecandidate", event => {
candidates.push(event.candidate);
pre.textContent = JSON.stringify(candidates);
});
return container;
}
function createPeerIceCandidateForm(peerConnection) {
const form = document.createElement("form");
const fieldset = document.createElement("fieldset");
const legend = document.createElement("legend");
legend.textContent = "add ice candidate";
const textbox = document.createElement("textarea");
const submitbutton = document.createElement("input");
submitbutton.type = "submit";
fieldset.append(legend, textbox, submitbutton);
form.append(fieldset);
form.onsubmit = async function(event) {
event.preventDefault();
let candidates = JSON.parse(textbox.value);
if (!Array.isArray(candidates)) candidates = [candidates];
for (const candidate of candidates) {
if (candidate === null) break;
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
form.reset();
};
return form;
}
function createCreateOffer(peerConnection) {
const container = document.createElement("div");
const button = document.createElement("button");
const resultbox = document.createElement("pre");
container.append(button, resultbox);
button.textContent = "create offer";
button.onclick = async function() {
button.disabled = "true";
await peerConnection.setLocalDescription();
resultbox.textContent = JSON.stringify(peerConnection.localDescription);
};
return container;
}
function createAddRemoteSessionDescription(peerConnection) {
const form = document.createElement("form");
const fieldset = document.createElement("fieldset");
const legend = document.createElement("legend");
legend.textContent = "add remote session description";
const textbox = document.createElement("textarea");
const result = document.createElement("pre");
const submitbutton = document.createElement("input");
submitbutton.type = "submit";
const resetbutton = document.createElement("button");
resetbutton.type = "reset";
resetbutton.textContent = "reset";
fieldset.append(legend, textbox, result, submitbutton, resetbutton);
form.append(fieldset);
form.onsubmit = async function(event) {
event.preventDefault();
const remoteDescription = new RTCSessionDescription(JSON.parse(textbox.value));
await peerConnection.setRemoteDescription(remoteDescription);
if (remoteDescription.type === "offer") {
await peerConnection.setLocalDescription()
result.textContent = JSON.stringify(peerConnection.localDescription);
}
form.reset();
};
return form;
}
function createPeerElement(peerConnection) {
const peerContainer = document.createElement("div");
peerContainer.style.border = "1px solid black";
peerContainer.append(
createPeerVideo(peerConnection),
createIceCandidateList(peerConnection),
createPeerIceCandidateForm(peerConnection),
createCreateOffer(peerConnection),
createAddRemoteSessionDescription(peerConnection),
);
return peerContainer;
}
const peers = [];
window.peers = peers;
addPeer.onclick = async function() {
const videoStream = await getVideoDevice();
const peer = createPeerConnection(videoStream);
const el = createPeerElement(peer);
peerContainer.append(el);
peers.push(peer);
};
// ---- MEDIA
async function getVideoDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === "videoinput");
}
async function getVideoDevice() {
const selectedCameraId = cameraSelect.options[cameraSelect.selectedIndex]?.value;
return await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
deviceId: selectedCameraId,
},
});
}
function populateVideoSelector(devices) {
while (cameraSelect.lastChild) cameraSelect.removeChild(cameraSelect.lastChild);
for (const device of devices) {
const cameraOption = document.createElement('option');
cameraOption.label = device.label || "Untitled camera";
cameraOption.value = device.deviceId;
cameraSelect.appendChild(cameraOption);
}
}
async function setVideoDevice() {
if (videoEnabled.checked) {
const stream = await getVideoDevice();
myVideo.srcObject = stream;
for (const peer of peers) {
peer.addStream(stream);
}
} else {
const stream = await getVideoDevice();
stream?.getTracks().forEach(track => track.stop());
myVideo.srcObject = null;
if (stream) {
for (const peer of peers) {
peer.removeStream(stream);
}
}
}
}
// ---- INIT
navigator.mediaDevices.addEventListener('devicechange', event => {
const newCameraList = getConnectedDevices('video');
updateCameraList(newCameraList);
});
videoEnabled.onchange = cameraSelect.onchange = function onCameraSelectChange() {
setVideoDevice();
};
async function init() {
populateVideoSelector(await getVideoDevices());
await setVideoDevice();
}
init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment