mirror of
git://f0xx.org/ac/ac-ms-sfu-signaling
synced 2026-08-12 18:12:44 +03:00
Add lab page with screen-share publisher and subscriber viewer, join API feed support, shared session auth, static assets route, and lab docs. Co-authored-by: Cursor <cursoragent@cursor.com>
247 lines
7.4 KiB
JavaScript
247 lines
7.4 KiB
JavaScript
(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();
|
|
}
|
|
})();
|