mirror of
git://f0xx.org/ac/ac-ms-sfu-signaling
synced 2026-08-12 18:12:44 +03:00
SFU M0: browser lab E2E client for Janus VideoRoom.
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>
This commit is contained in:
246
public/assets/sfu-lab.js
Normal file
246
public/assets/sfu-lab.js
Normal file
@@ -0,0 +1,246 @@
|
||||
(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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user