<!DOCTYPE html>
<html>
<head>
<title>WebRTC Chat example</title>
<style>
body {
background: black;
color: white;
}
</style>
</head>
<body>
<button id="rtc" onClick="startRtc()">Connect</button>
<button id="cam" onClick="startCam()" disabled>Cam</button>
<button id="mic" onClick="startMic()" disabled>Mic</button>
ICE: <span id="ice_status">new</span>
<div id="chan_status">Waiting for data channel.</div>
<div id="media"></div>
<script>
let streamCam;
let streamMic;
let dataChannel;
let rtc = new RTCPeerConnection();
rtc.oniceconnectionstatechange = () => {
byId('ice_status').innerText = rtc.iceConnectionState;
if (rtc.iceConnectionState == 'disconnected' || rtc.iceConnectionState == 'failed') {
if (streamCam) {
streamCam.getTracks()[0]?.stop();
}
if (streamMic) {
streamMic.getTracks()[0]?.stop();
}
rtc.close();
byId('rtc').disabled = true;
byId('cam').disabled = true;
byId('mic').disabled = true;
}
};
const byId = (id) => document.getElementById(id);
const byTag = (tag) => [].slice.call(document.getElementsByTagName(tag));
var callback = null;
async function negotiate() {
const offer = await rtc.createOffer();
console.log('do offer', offer.sdp.split('\r\n'));
rtc.setLocalDescription(offer);
await dataChannel.send(JSON.stringify(offer));
const json = await new Promise((rs) => {
callback = rs;
});
const answer = JSON.parse(json);
console.log('received answer', answer.sdp.split('\r\n'));
rtc.setRemoteDescription(answer);
}
async function handleOffer(json) {
const offer = JSON.parse(json);
console.log('handle offer', offer.sdp.split('\r\n'));
rtc.setRemoteDescription(offer);
const answer = await rtc.createAnswer();
console.log('offer response', answer.sdp.split('\r\n'));
rtc.setLocalDescription(answer);
await dataChannel.send(JSON.stringify(answer));
}
async function startCam() {
byId('cam').disabled = true;
streamCam = await navigator.mediaDevices.getUserMedia({
video: {
width: 640,
height: 360,
},
});
const tr = rtc.addTransceiver(streamCam.getTracks()[0], {
direction: "sendonly",
streams: [streamCam],
});
await negotiate();
}
async function startMic() {
byId('mic').disabled = true;
streamMic = await navigator.mediaDevices.getUserMedia({
audio: true,
});
const tr = rtc.addTransceiver(streamMic.getTracks()[0], {
streams: [streamMic],
direction: "sendonly"
});
await negotiate();
}
rtc.ontrack = (e) => {
console.log('ontrack', e.track);
const track = e.track;
const domId = `media-${track.id}`;
const el = document.createElement('video');
if (byId(domId)) {
return;
}
el.id = domId;
el.width = 500;
byId('media').appendChild(el);
el.controls = true;
el.autoplay = true;
setTimeout(() => {
const media = new MediaStream();
media.addTrack(track);
el.srcObject = media;
}, 1);
track.addEventListener('mute', () => {
console.log('track muted', track);
el.parentNode.removeChild(el);
});
track.addEventListener('unmute', () => {
console.log('track unmuted', track);
byId('media').appendChild(el);
});
};
async function startRtc() {
byId('rtc').disabled = true;
dataChannel = rtc.createDataChannel("offer/answer");
dataChannel.onmessage = (event) => {
let json = JSON.parse(event.data);
if (json.type == 'offer') {
handleOffer(event.data);
} else if (json.type == 'answer') {
callback(event.data);
callback = null;
}
};
dataChannel.onopen = () => {
byId('chan_status').innerText = '';
byId('mic').disabled = false;
byId('cam').disabled = false;
};
const offer = await rtc.createOffer();
rtc.setLocalDescription(offer);
console.log('POST offer', offer.sdp.split('\r\n'));
const res = await fetch('/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(offer),
});
const answer = await res.json();
rtc.setRemoteDescription(answer);
console.log('POST answer', answer.sdp.split('\r\n'));
}
</script>
</body>
</html>