|
// ---- 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(); |