(function () { 'use strict'; var API = '/app/androidcast_project/sfu/api'; var WS_PATH = '/app/androidcast_project/sfu/ws'; var ICE = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, ]; function wsUrl() { var proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; return proto + '//' + location.host + WS_PATH; } function log(el, msg) { if (!el) return; var line = document.createElement('div'); line.textContent = new Date().toISOString().slice(11, 19) + ' ' + msg; el.prepend(line); } function JanusSession(sessionId, handleId, logEl) { this.sessionId = sessionId; this.handleId = handleId; this.logEl = logEl; this.tx = 0; this.ws = null; } JanusSession.prototype.tid = function () { this.tx += 1; return 'lab-' + this.tx; }; JanusSession.prototype.connect = function () { var self = this; return new Promise(function (resolve, reject) { self.ws = new WebSocket(wsUrl()); self.ws.onopen = function () { log(self.logEl, 'Janus WS open'); self.keepalive = setInterval(function () { if (self.ws && self.ws.readyState === WebSocket.OPEN) { self.ws.send( JSON.stringify({ janus: 'keepalive', session_id: self.sessionId, transaction: self.tid(), }) ); } }, 25000); resolve(); }; self.ws.onerror = function () { reject(new Error('WebSocket error')); }; self.ws.onmessage = function (ev) { var msg; try { msg = JSON.parse(ev.data); } catch (e) { return; } if (msg.janus === 'trickle' && msg.candidate && self.onTrickle) { self.onTrickle(msg.candidate); } if (msg.janus === 'webrtcup') { log(self.logEl, 'WebRTC up'); } if (msg.jsep && self.onJsep) { self.onJsep(msg.jsep); } }; }); }; JanusSession.prototype.sendTrickle = function (candidate) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; this.ws.send( JSON.stringify({ janus: 'trickle', session_id: this.sessionId, handle_id: this.handleId, transaction: this.tid(), candidate: candidate, }) ); }; JanusSession.prototype.close = function () { if (this.keepalive) clearInterval(this.keepalive); if (this.ws) this.ws.close(); }; function wireIce(pc, janus) { pc.onicecandidate = function (ev) { if (!janus) return; if (ev.candidate) { janus.sendTrickle({ candidate: ev.candidate.candidate, sdpMid: ev.candidate.sdpMid, sdpMLineIndex: ev.candidate.sdpMLineIndex, }); } else { janus.sendTrickle({ completed: true }); } }; janus.onTrickle = function (c) { if (c.completed) return; pc.addIceCandidate(c).catch(function () {}); }; } async function apiJoin(body) { var r = await fetch(API + '/join', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); var j = await r.json(); if (!j.ok) throw new Error(j.error || 'join failed'); return j; } async function startPublisher(opts) { var roomId = opts.roomId; var videoEl = opts.videoEl; var logEl = opts.logEl; var stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true, }); videoEl.srcObject = stream; var pc = new RTCPeerConnection({ iceServers: ICE }); stream.getTracks().forEach(function (t) { pc.addTrack(t, stream); }); var offer = await pc.createOffer(); await pc.setLocalDescription(offer); var join = await apiJoin({ room_id: roomId, role: 'publisher', jsep: { type: offer.type, sdp: offer.sdp }, }); var janus = new JanusSession(join.session_id, join.handle_id, logEl); wireIce(pc, janus); await janus.connect(); if (join.jsep && join.jsep.sdp) { await pc.setRemoteDescription(join.jsep); } log(logEl, 'Publisher joined room ' + roomId + ' feed=' + (join.publisher_id || '?')); return { pc: pc, janus: janus, stream: stream, publisherId: join.publisher_id }; } async function startSubscriber(opts) { var roomId = opts.roomId; var feedId = parseInt(opts.feedId, 10); var videoEl = opts.videoEl; var logEl = opts.logEl; if (!feedId) throw new Error('publisher feed id required'); var pc = new RTCPeerConnection({ iceServers: ICE }); pc.ontrack = function (ev) { if (ev.streams && ev.streams[0]) { videoEl.srcObject = ev.streams[0]; } }; pc.addTransceiver('video', { direction: 'recvonly' }); pc.addTransceiver('audio', { direction: 'recvonly' }); var offer = await pc.createOffer(); await pc.setLocalDescription(offer); var join = await apiJoin({ room_id: roomId, role: 'subscriber', feed_id: feedId, jsep: { type: offer.type, sdp: offer.sdp }, }); var janus = new JanusSession(join.session_id, join.handle_id, logEl); wireIce(pc, janus); await janus.connect(); if (join.jsep && join.jsep.sdp) { await pc.setRemoteDescription(join.jsep); } log(logEl, 'Subscriber watching feed ' + feedId); return { pc: pc, janus: janus }; } function bindUi() { var pubLog = document.getElementById('pub-log'); var subLog = document.getElementById('sub-log'); var pubVideo = document.getElementById('pub-video'); var subVideo = document.getElementById('sub-video'); var pubState = null; document.getElementById('btn-pub').addEventListener('click', function () { var roomId = parseInt(document.getElementById('room-id').value, 10); if (!roomId) { alert('Enter Janus room id (from dashboard)'); return; } document.getElementById('btn-pub').disabled = true; startPublisher({ roomId: roomId, videoEl: pubVideo, logEl: pubLog }) .then(function (s) { pubState = s; if (s.publisherId) { document.getElementById('feed-id').value = String(s.publisherId); } }) .catch(function (e) { log(pubLog, 'ERROR: ' + e.message); document.getElementById('btn-pub').disabled = false; }); }); document.getElementById('btn-sub').addEventListener('click', function () { var roomId = parseInt(document.getElementById('room-id').value, 10); var feedId = document.getElementById('feed-id').value; document.getElementById('btn-sub').disabled = true; startSubscriber({ roomId: roomId, feedId: feedId, videoEl: subVideo, logEl: subLog }) .catch(function (e) { log(subLog, 'ERROR: ' + e.message); document.getElementById('btn-sub').disabled = false; }); }); document.getElementById('btn-stop-pub').addEventListener('click', function () { if (pubState) { pubState.stream.getTracks().forEach(function (t) { t.stop(); }); pubState.janus.close(); pubState.pc.close(); pubState = null; pubVideo.srcObject = null; document.getElementById('btn-pub').disabled = false; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bindUi); } else { bindUi(); } })();