diff --git a/examples/videoroom-ms/html/index.html b/examples/videoroom-ms/html/index.html
new file mode 100644
index 0000000..f0590f9
--- /dev/null
+++ b/examples/videoroom-ms/html/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+ VideoRoom Socket.IO Janode
+
+
+
+
+
+
+
+
--- VIDEOROOM () ---
+
+
-- LOCALS --
+
+
+
+
-- REMOTES --
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/videoroom-ms/html/videoroom-ms-client.js b/examples/videoroom-ms/html/videoroom-ms-client.js
new file mode 100644
index 0000000..102eb54
--- /dev/null
+++ b/examples/videoroom-ms/html/videoroom-ms-client.js
@@ -0,0 +1,952 @@
+/* eslint-disable no-sparse-arrays */
+/* global io */
+
+'use strict';
+
+const RTCPeerConnection = (window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection).bind(window);
+
+let pubPc, subPc;
+let subscriptions = new Map();
+const pendingOfferMap = new Map();
+const myRoom = getURLParameter('room') ? parseInt(getURLParameter('room')) : (getURLParameter('room_str') || 1234);
+const randName = ('John_Doe_' + Math.floor(10000 * Math.random()));
+const myName = getURLParameter('name') || randName;
+let myFeed;
+
+const button = document.getElementById('button');
+button.onclick = () => {
+ if (socket.connected)
+ socket.disconnect();
+ else
+ socket.connect();
+};
+
+function getId() {
+ return Math.floor(Number.MAX_SAFE_INTEGER * Math.random());
+}
+
+function getURLParameter(name) {
+ return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [, ''])[1].replace(/\+/g, '%20')) || null;
+}
+
+const scheduleConnection = (function () {
+ let task = null;
+ const delay = 5000;
+
+ return (function (secs) {
+ if (task) return;
+ const timeout = secs * 1000 || delay;
+ console.log('scheduled joining in ' + timeout + ' ms');
+ task = setTimeout(() => {
+ join();
+ task = null;
+ }, timeout);
+ });
+})();
+
+const socket = io({
+ rejectUnauthorized: false,
+ autoConnect: false,
+ reconnection: false,
+});
+
+function join({ room = myRoom, display = myName, token = null } = {}) {
+ const joinData = {
+ room,
+ display,
+ token,
+ };
+
+ socket.emit('join', {
+ data: joinData,
+ _id: getId(),
+ });
+}
+
+function subscribe({ streams, room = myRoom }) {
+ const subscribeData = {
+ room,
+ streams,
+ };
+
+ socket.emit('subscribe', {
+ data: subscribeData,
+ _id: getId(),
+ });
+}
+
+function subscribeTo(publishers, room = myRoom) {
+ const newStreams = [];
+ publishers.forEach(({ feed, streams }) => {
+ streams.forEach(s => {
+ if (!hasFeedMidSubscription(feed, s.mid)) {
+ newStreams.push({
+ feed,
+ mid: s.mid,
+ });
+ }
+ });
+ });
+
+ if (newStreams.length > 0) {
+ subscribe({
+ streams: newStreams,
+ room,
+ });
+ }
+}
+
+function trickle({ feed, candidate }) {
+ const trickleData = candidate ? { candidate } : {};
+ if (feed) trickleData.feed = feed;
+ const trickleEvent = candidate ? 'trickle' : 'trickle-complete';
+
+ socket.emit(trickleEvent, {
+ data: trickleData,
+ _id: getId(),
+ });
+}
+
+function configure({ feed, display, jsep, restart, streams }) {
+ const configureData = {};
+ if (feed) configureData.feed = feed;
+ if (display) configureData.display = display;
+ if (jsep) configureData.jsep = jsep;
+ if (streams) configureData.streams = streams;
+ if (typeof restart === 'boolean') configureData.restart = restart;
+
+ const configId = getId();
+
+ socket.emit('configure', {
+ data: configureData,
+ _id: configId,
+ });
+
+ if (jsep)
+ pendingOfferMap.set(configId, { feed });
+}
+
+async function _publish({ feed = myFeed, display = myName } = {}) {
+ try {
+ const offer = await doOffer(feed, display);
+ configure({ feed, jsep: offer });
+ } catch (e) {
+ console.log('error while doing offer', e);
+ }
+}
+
+function _unpublish({ feed = myFeed } = {}) {
+ const unpublishData = {
+ feed,
+ };
+
+ socket.emit('unpublish', {
+ data: unpublishData,
+ _id: getId(),
+ });
+}
+
+function _leave({ feed = myFeed } = {}) {
+ const leaveData = {
+ feed,
+ };
+
+ socket.emit('leave', {
+ data: leaveData,
+ _id: getId(),
+ });
+}
+
+function _listParticipants({ room = myRoom } = {}) {
+ const listData = {
+ room,
+ };
+
+ socket.emit('list-participants', {
+ data: listData,
+ _id: getId(),
+ });
+}
+
+function _kick({ feed, room = myRoom, secret = 'adminpwd' }) {
+ const kickData = {
+ room,
+ feed,
+ secret,
+ };
+
+ socket.emit('kick', {
+ data: kickData,
+ _id: getId(),
+ });
+}
+
+function start({ jsep = null } = {}) {
+ const startData = {
+ jsep,
+ };
+
+ socket.emit('start', {
+ data: startData,
+ _id: getId(),
+ });
+}
+
+function _pause() {
+ const pauseData = {};
+
+ socket.emit('pause', {
+ data: pauseData,
+ _id: getId(),
+ });
+}
+
+function _unsubscribe({ streams, room = myRoom }) {
+ const unsubscribeData = {
+ room,
+ streams,
+ };
+
+ socket.emit('unsubscribe', {
+ data: unsubscribeData,
+ _id: getId(),
+ });
+}
+
+function _switch({ streams }) {
+ const switchData = {
+ streams,
+ };
+
+ socket.emit('switch', {
+ data: switchData,
+ _id: getId(),
+ });
+}
+
+function _exists({ room = myRoom } = {}) {
+ const existsData = {
+ room,
+ };
+
+ socket.emit('exists', {
+ data: existsData,
+ _id: getId(),
+ });
+}
+
+function _listRooms() {
+ socket.emit('list-rooms', {
+ _id: getId(),
+ });
+}
+
+function _create({ room, description, max_publishers = 6, audiocodec = 'opus', videocodec = 'vp8', talking_events = false, talking_level_threshold = 25, talking_packets_threshold = 100, permanent = false }) {
+ socket.emit('create', {
+ data: {
+ room,
+ description,
+ max_publishers,
+ audiocodec,
+ videocodec,
+ talking_events,
+ talking_level_threshold,
+ talking_packets_threshold,
+ permanent,
+ },
+ _id: getId(),
+ });
+}
+
+function _destroy({ room = myRoom, permanent = false, secret = 'adminpwd' }) {
+ socket.emit('destroy', {
+ data: {
+ room,
+ permanent,
+ secret,
+ },
+ _id: getId(),
+ });
+}
+
+// add remove enable disable token mgmt
+function _allow({ room = myRoom, action, token, secret = 'adminpwd' }) {
+ const allowData = {
+ room,
+ action,
+ secret,
+ };
+ if (action != 'disable' && token) allowData.list = [token];
+
+ socket.emit('allow', {
+ data: allowData,
+ _id: getId(),
+ });
+}
+
+function _startForward({ feed = myFeed, host, room = myRoom, streams, secret = 'adminpwd' }) {
+ socket.emit('rtp-fwd-start', {
+ data: {
+ room,
+ feed,
+ host,
+ streams,
+ secret,
+ },
+ _id: getId(),
+ });
+}
+
+function _stopForward({ stream, feed, room = myRoom, secret = 'adminpwd' }) {
+ socket.emit('rtp-fwd-stop', {
+ data: {
+ room,
+ stream,
+ feed,
+ secret,
+ },
+ _id: getId(),
+ });
+}
+
+function _listForward({ room = myRoom, secret = 'adminpwd' } = {}) {
+ socket.emit('rtp-fwd-list', {
+ data: { room, secret },
+ _id: getId(),
+ });
+}
+
+socket.on('connect', () => {
+ console.log('socket connected');
+ socket.sendBuffer = [];
+ scheduleConnection(0.1);
+});
+
+socket.on('disconnect', () => {
+ console.log('socket disconnected');
+ pendingOfferMap.clear();
+ subscriptions.clear();
+ removeAllMediaElements();
+ closeAllPCs();
+});
+
+socket.on('videoroom-error', ({ error, _id }) => {
+ console.log('videoroom error', error);
+ if (error === 'backend-failure' || error === 'session-not-available') {
+ socket.disconnect();
+ return;
+ }
+ if (pendingOfferMap.has(_id)) {
+ removeAllLocalMediaElements();
+ closePubPc();
+ pendingOfferMap.delete(_id);
+ return;
+ }
+});
+
+socket.on('joined', async ({ data }) => {
+ console.log('joined to room', data);
+ setLocalMediaElement(null, null, null, data.room);
+
+ try {
+ await _publish({ feed: data.feed, display: data.display });
+ subscribeTo(data.publishers, data.room);
+ } catch (e) {
+ console.log('error while publishing', e);
+ }
+});
+
+socket.on('subscribed', async ({ data }) => {
+ console.log('subscribed to feed', data);
+ /*
+ * data.streams
+ * [
+ * {
+ * "type": "audio",
+ * "active": true,
+ * "mindex": 0,
+ * "mid": "0",
+ * "ready": false,
+ * "send": true,
+ * "feed_id": 947374180882471,
+ * "feed_display": "John_Doe_8186",
+ * "feed_mid": "0",
+ * "codec": "opus"
+ * },
+ * {
+ * "type": "video",
+ * "active": true,
+ * "mindex": 1,
+ * "mid": "1",
+ * "ready": false,
+ * "send": true,
+ * "feed_id": 947374180882471,
+ * "feed_display": "John_Doe_8186",
+ * "feed_mid": "1",
+ * "codec": "vp8"
+ * }
+ * ]
+ */
+ updateSubscriptions(data.streams);
+
+ try {
+ if (data.jsep) {
+ const answer = await doAnswer(data.jsep);
+ start({ jsep: answer });
+ }
+ } catch (e) { console.log('error while doing answer', e); }
+});
+
+socket.on('unsubscribed', async ({ data }) => {
+ console.log('unsubscribed to feed', data);
+ /*
+ * data.streams
+ * [
+ * {
+ * "type": "audio",
+ * "active": true,
+ * "mindex": 0,
+ * "mid": "0",
+ * "ready": true,
+ * "send": true,
+ * "feed_id": 5431908509285044,
+ * "feed_display": "John_Doe_2332",
+ * "feed_mid": "0",
+ * "codec": "opus"
+ * },
+ * {
+ * "type": "video",
+ * "active": false,
+ * "mindex": 1,
+ * "mid": "1",
+ * "ready": false,
+ * "send": true
+ * }
+ * ]
+ */
+ updateSubscriptions(data.streams);
+
+ try {
+ if (data.jsep) {
+ const answer = await doAnswer(data.jsep);
+ start({ jsep: answer });
+ }
+ } catch (e) { console.log('error while doing answer', e); }
+});
+
+socket.on('updated', async ({ data }) => {
+ console.log('updated subscription', data);
+ /*
+ * data.streams
+ * [
+ * {
+ * "type": "audio",
+ * "active": false,
+ * "mindex": 0,
+ * "mid": "0",
+ * "ready": false,
+ * "send": true
+ * },
+ * {
+ * "type": "video",
+ * "active": false,
+ * "mindex": 1,
+ * "mid": "1",
+ * "ready": false,
+ * "send": true
+ * }
+ * ]
+ */
+ updateSubscriptions(data.streams);
+
+ try {
+ if (data.jsep) {
+ const answer = await doAnswer(data.jsep);
+ start({ jsep: answer });
+ }
+ } catch (e) { console.log('error while doing answer', e); }
+});
+
+socket.on('participants-list', ({ data }) => {
+ console.log('participants list', data);
+});
+
+socket.on('talking', ({ data }) => {
+ console.log('talking notify', data);
+});
+
+socket.on('kicked', ({ data }) => {
+ console.log('participant kicked', data);
+ if (data.feed) {
+ const streams = subscriptions.values().toArray().map(s => {
+ const stream = {};
+ for (const attr in s) {
+ stream[attr] = s[attr];
+ }
+ if (stream.feed_id == data.feed) {
+ stream.active = false;
+ stream.feed_id = null;
+ stream.feed_mid = null;
+ stream.feed_display = null;
+ }
+ return stream;
+ });
+ updateSubscriptions(streams);
+ }
+});
+
+socket.on('allowed', ({ data }) => {
+ console.log('token management', data);
+});
+
+socket.on('configured', async ({ data, _id }) => {
+ console.log('feed configured', data);
+ pendingOfferMap.delete(_id);
+
+ const pc = data.feed ? pubPc : subPc;
+ if (data.jsep) {
+ try {
+ await pc.setRemoteDescription(data.jsep);
+ if (data.jsep.type === 'offer') {
+ const answer = await doAnswer(data.jsep);
+ start({ jsep: answer });
+ }
+ console.log('configure remote sdp OK');
+ } catch (e) {
+ console.log('error setting remote sdp', e);
+ }
+ }
+ if (data.display) {
+ setLocalMediaElement(null, data.feed, data.display);
+ }
+});
+
+socket.on('display', ({ data }) => {
+ console.log('feed changed display name', data);
+ const streams = subscriptions.values().toArray().map(s => {
+ if (s.feed_id === data.feed) {
+ s.feed_display = data.display;
+ }
+ return s;
+ });
+ updateSubscriptions(streams);
+});
+
+socket.on('started', ({ data }) => {
+ console.log('subscriber feed started', data);
+});
+
+socket.on('paused', ({ data }) => {
+ console.log('feed paused', data);
+});
+
+socket.on('switched', ({ data }) => {
+ console.log('feed switched', data);
+ updateSubscriptions(data.streams);
+});
+
+socket.on('feed-list', ({ data }) => {
+ console.log('new feeds available!', data);
+ subscribeTo(data.publishers, data.room);
+});
+
+socket.on('unpublished', ({ data }) => {
+ console.log('feed unpublished', data);
+ if (data.feed) {
+ if (data.feed === myFeed) {
+ removeAllLocalMediaElements();
+ closePubPc();
+ }
+ }
+});
+
+socket.on('leaving', ({ data }) => {
+ console.log('feed leaving', data);
+ if (data.feed) {
+ if (data.feed === myFeed) {
+ removeAllLocalMediaElements();
+ closePubPc();
+ }
+ else {
+ const streams = subscriptions.values().toArray().map(s => {
+ const stream = {};
+ for (const attr in s) {
+ stream[attr] = s[attr];
+ }
+ if (stream.feed_id == data.feed) {
+ stream.active = false;
+ stream.feed_id = null;
+ stream.feed_mid = null;
+ stream.feed_display = null;
+ }
+ return stream;
+ });
+ updateSubscriptions(streams);
+ }
+ }
+});
+
+socket.on('exists', ({ data }) => {
+ console.log('room exists', data);
+});
+
+socket.on('rooms-list', ({ data }) => {
+ console.log('rooms list', data);
+});
+
+socket.on('created', ({ data }) => {
+ console.log('room created', data);
+});
+
+socket.on('destroyed', ({ data }) => {
+ console.log('room destroyed', data);
+ if (data.room === myRoom) {
+ socket.disconnect();
+ }
+});
+
+socket.on('rtp-fwd-started', ({ data }) => {
+ console.log('rtp forwarding started', data);
+});
+
+socket.on('rtp-fwd-stopped', ({ data }) => {
+ console.log('rtp forwarding stopped', data);
+});
+
+socket.on('rtp-fwd-list', ({ data }) => {
+ console.log('rtp forwarders list', data);
+});
+
+async function _restartPublisher({ feed = myFeed } = {}) {
+ return _publish({ feed });
+}
+
+async function _restartSubscriber() {
+ configure({ restart: true });
+}
+
+async function doOffer(feed, display) {
+ if (!pubPc) {
+ const pc = new RTCPeerConnection({
+ 'iceServers': [{
+ urls: 'stun:stun.l.google.com:19302'
+ }],
+ });
+
+ pc.onnegotiationneeded = event => console.log('pc.onnegotiationneeded', event);
+ pc.onicecandidate = event => trickle({ feed, candidate: event.candidate });
+ pc.oniceconnectionstatechange = () => {
+ if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') {
+ removeAllLocalMediaElements();
+ closePubPc();
+ }
+ };
+ /* This one below should not be fired, cause the PC is used just to send */
+ pc.ontrack = event => console.log('pc.ontrack', event);
+
+ pubPc = pc;
+
+ try {
+ const localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
+ localStream.getTracks().forEach(track => {
+ console.log('adding track', track);
+ pc.addTrack(track, localStream);
+ });
+ setLocalMediaElement(localStream, feed, display, null);
+ } catch (e) {
+ console.log('error while doing offer', e);
+ removeAllLocalMediaElements();
+ closePubPc();
+ return;
+ }
+ }
+ else {
+ console.log('Performing ICE restart');
+ pubPc.restartIce();
+ }
+ myFeed = feed;
+
+ try {
+ const offer = await pubPc.createOffer();
+ await pubPc.setLocalDescription(offer);
+ console.log('set local sdp OK');
+ return offer;
+ } catch (e) {
+ console.log('error while doing offer', e);
+ removeAllLocalMediaElements();
+ closePubPc();
+ return;
+ }
+}
+
+function hasFeedMidSubscription(feed, mid) {
+ for (let [_, s] of subscriptions) {
+ if (s.feed === feed && s.mid === mid) return true;
+ }
+ return false;
+}
+
+function updateSubscriptions(streams) {
+ if (!streams) return;
+ removeRemoteMediaElements(streams);
+ const newSubscriptions = new Map();
+ streams.forEach(s => {
+ s.ms = subscriptions.get(s.mid)?.ms;
+ newSubscriptions.set(s.mid, s);
+ });
+ subscriptions = newSubscriptions;
+ refreshRemoteMediaElements();
+}
+
+function removeRemoteMediaElements(new_streams) {
+ if (!new_streams) return;
+ const oldSubscriptions = subscriptions;
+ const oldSubMids = oldSubscriptions.values().toArray().map(s => s.active && s.mid).filter(mid => mid);
+ const newSubMids = new_streams.values().toArray().map(s => s.active && s.mid).filter(mid => mid);
+ const deletedSubMids = oldSubMids.filter(mid => !newSubMids.includes(mid));
+ deletedSubMids.forEach(mid => removeRemoteMediaElementsBySubMid(mid, false));
+}
+
+function refreshRemoteMediaElements() {
+ for (let [sub_mid, s] of subscriptions) {
+ const { feed_display, type, feed_id, feed_mid, active, ms } = s;
+ if (active) {
+ if (type === 'video')
+ setRemoteVideoElement(ms, sub_mid, [feed_display, feed_id, feed_mid, sub_mid].join('|'));
+ if (type === 'audio')
+ setRemoteAudioElement(ms, sub_mid);
+ }
+ }
+}
+
+async function doAnswer(offer) {
+ if (!subPc) {
+ const pc = new RTCPeerConnection({
+ 'iceServers': [{
+ urls: 'stun:stun.l.google.com:19302'
+ }],
+ });
+
+ subPc = pc;
+
+ pc.onnegotiationneeded = event => console.log('pc.onnegotiationneeded', event);
+ pc.onicecandidate = event => trickle({ candidate: event.candidate });
+ pc.oniceconnectionstatechange = () => {
+ if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') {
+ removeAllRemoteMediaElements();
+ closeSubPc();
+ }
+ };
+ pc.ontrack = event => {
+ console.log('pc.ontrack', event);
+
+ event.track.onunmute = evt => {
+ console.log('track.onunmute', evt);
+ };
+ event.track.onmute = evt => {
+ console.log('track.onmute', evt);
+ };
+ event.track.onended = evt => {
+ console.log('track.onended', evt);
+ };
+
+ /* avoid latching tracks */
+ const submid = event.transceiver?.mid || event.receiver.mid;
+ const remoteStream = event.streams[0].id === 'janus' ? (new MediaStream([event.track])) : event.streams[0];
+ if (subscriptions.has(submid)) {
+ const stream = subscriptions.get(submid);
+ stream.ms = remoteStream;
+ refreshRemoteMediaElements();
+ }
+ };
+ }
+
+ try {
+ await subPc.setRemoteDescription(offer);
+ console.log('set remote sdp OK');
+ const answer = await subPc.createAnswer();
+ await subPc.setLocalDescription(answer);
+ console.log('set local sdp OK');
+ return answer;
+ } catch (e) {
+ console.log('error creating subscriber answer', e);
+ removeAllRemoteMediaElements();
+ closeSubPc();
+ throw e;
+ }
+}
+
+function setLocalMediaElement(localStream, feed, display, room) {
+ if (room) document.getElementById('videos').getElementsByTagName('span')[0].innerHTML = ' --- VIDEOROOM (' + room + ') --- ';
+ if (!feed) return;
+
+ const id = `video_${feed}_local`;
+ let localVideoContainer = document.getElementById(id);
+ if (!localVideoContainer) {
+ const nameElem = document.createElement('span');
+ nameElem.style.display = 'table';
+
+ const localVideoStreamElem = document.createElement('video');
+ localVideoStreamElem.width = 320;
+ localVideoStreamElem.height = 240;
+ localVideoStreamElem.autoplay = true;
+ localVideoStreamElem.muted = true;
+ localVideoStreamElem.style.cssText = '-moz-transform: scale(-1, 1); -webkit-transform: scale(-1, 1); -o-transform: scale(-1, 1); transform: scale(-1, 1); filter: FlipH;';
+
+ localVideoContainer = document.createElement('div');
+ localVideoContainer.id = id;
+ localVideoContainer.appendChild(nameElem);
+ localVideoContainer.appendChild(localVideoStreamElem);
+
+ document.getElementById('locals').appendChild(localVideoContainer);
+ }
+ if (display) {
+ const nameElem = localVideoContainer.getElementsByTagName('span')[0];
+ nameElem.innerHTML = [display, feed].join('|');
+ }
+ if (localStream) {
+ const localVideoStreamElem = localVideoContainer.getElementsByTagName('video')[0];
+ localVideoStreamElem.srcObject = localStream;
+ }
+}
+
+function setRemoteVideoElement(remoteStream, sub_mid, display) {
+ if (!sub_mid) return;
+
+ /* Target specific sub_mid/feed/mid */
+ const id = `video_remote_${sub_mid}`;
+ let remoteVideoContainer = document.getElementById(id);
+ if (!remoteVideoContainer) {
+ /* Non existing */
+ const nameElem = document.createElement('span');
+ nameElem.innerHTML = display;
+ nameElem.style.display = 'table';
+
+ const remoteVideoStreamElem = document.createElement('video');
+ remoteVideoStreamElem.width = 320;
+ remoteVideoStreamElem.height = 240;
+ remoteVideoStreamElem.autoplay = true;
+ remoteVideoStreamElem.style.cssText = '-moz-transform: scale(-1, 1); -webkit-transform: scale(-1, 1); -o-transform: scale(-1, 1); transform: scale(-1, 1); filter: FlipH;';
+
+ remoteVideoContainer = document.createElement('div');
+ remoteVideoContainer.id = id;
+ remoteVideoContainer.appendChild(nameElem);
+ remoteVideoContainer.appendChild(remoteVideoStreamElem);
+
+ document.getElementById('remotes').appendChild(remoteVideoContainer);
+ }
+ if (display) {
+ const nameElem = remoteVideoContainer.getElementsByTagName('span')[0];
+ nameElem.innerHTML = display;
+ }
+ if (remoteStream) {
+ const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0];
+ remoteVideoStreamElem.srcObject = remoteStream;
+ }
+}
+
+function setRemoteAudioElement(remoteStream, sub_mid) {
+ if (!sub_mid) return;
+
+ /* Target specific sub_mid/feed/mid */
+ const id = `audio_remote_${sub_mid}`;
+ let remoteAudioContainer = document.getElementById(id);
+ if (!remoteAudioContainer) {
+ const remoteAudioStreamElem = document.createElement('audio');
+ remoteAudioStreamElem.autoplay = true;
+
+ remoteAudioContainer = document.createElement('div');
+ remoteAudioContainer.id = id;
+ remoteAudioContainer.appendChild(remoteAudioStreamElem);
+
+ document.getElementById('remotes').appendChild(remoteAudioContainer);
+ }
+ if (remoteStream) {
+ const remoteAudioStreamElem = remoteAudioContainer.getElementsByTagName('audio')[0];
+ remoteAudioStreamElem.srcObject = remoteStream;
+ }
+}
+
+function removeMediaElement(container, stopTracks = true) {
+ let streamElem = null;
+ if (container.getElementsByTagName('video').length > 0)
+ streamElem = container.getElementsByTagName('video')[0];
+ if (container.getElementsByTagName('audio').length > 0)
+ streamElem = container.getElementsByTagName('audio')[0];
+ if (streamElem && streamElem.srcObject && stopTracks) {
+ streamElem.srcObject.getTracks().forEach(track => track.stop());
+ streamElem.srcObject = null;
+ }
+ container.remove();
+}
+
+function removeRemoteMediaElementsBySubMid(sub_mid, stopTracks) {
+ const idEndsWith = `_remote_${sub_mid}`;
+ const containers = document.querySelectorAll(`[id$=${idEndsWith}]`);
+ containers.forEach(container => removeMediaElement(container, stopTracks));
+}
+
+function removeAllLocalMediaElements() {
+ const locals = document.getElementById('locals');
+ const localMediaContainers = locals.getElementsByTagName('div');
+ for (let i = 0; localMediaContainers && i < localMediaContainers.length; i++)
+ removeMediaElement(localMediaContainers[i]);
+ while (locals.firstChild)
+ locals.removeChild(locals.firstChild);
+}
+
+function removeAllRemoteMediaElements() {
+ var remotes = document.getElementById('remotes');
+ const remoteMediaContainers = remotes.getElementsByTagName('div');
+ for (let i = 0; remoteMediaContainers && i < remoteMediaContainers.length; i++)
+ removeMediaElement(remoteMediaContainers[i]);
+ while (remotes.firstChild)
+ remotes.removeChild(remotes.firstChild);
+}
+
+function removeAllMediaElements() {
+ removeAllLocalMediaElements();
+ removeAllRemoteMediaElements();
+ document.getElementById('videos').getElementsByTagName('span')[0].innerHTML = ' --- VIDEOROOM () --- ';
+}
+
+function closePubPc() {
+ if (pubPc) {
+ console.log('closing pc for publisher');
+ _closePC(pubPc);
+ pubPc = null;
+ }
+}
+
+function closeSubPc() {
+ if (subPc) {
+ console.log('closing pc for subscriber');
+ _closePC(subPc);
+ subPc = null;
+ }
+}
+
+function _closePC(pc) {
+ if (!pc) return;
+ pc.getSenders().forEach(sender => {
+ if (sender.track)
+ sender.track.stop();
+ });
+ pc.getReceivers().forEach(receiver => {
+ if (receiver.track)
+ receiver.track.stop();
+ });
+ pc.onnegotiationneeded = null;
+ pc.onicecandidate = null;
+ pc.oniceconnectionstatechange = null;
+ pc.ontrack = null;
+ try {
+ pc.close();
+ } catch (_e) { }
+}
+
+function closeAllPCs() {
+ console.log('closing all pcs');
+ closePubPc();
+ closeSubPc();
+}
diff --git a/examples/videoroom-ms/package-lock.json b/examples/videoroom-ms/package-lock.json
new file mode 100644
index 0000000..e42c5ef
--- /dev/null
+++ b/examples/videoroom-ms/package-lock.json
@@ -0,0 +1,930 @@
+{
+ "name": "janode-videoroom-ms",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "janode-videoroom-ms",
+ "license": "ISC",
+ "dependencies": {
+ "express": "^4.13.4",
+ "socket.io": "^4.2.0"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.17",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+ "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.14.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz",
+ "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+ "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.5.4",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
+ "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==",
+ "dependencies": {
+ "@types/cookie": "^0.4.1",
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.4.1",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.11.0"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
+ "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/cookie": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
+ "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
+ "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.2",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.6.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.7.5",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
+ "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.5.2",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz",
+ "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==",
+ "dependencies": {
+ "debug": "~4.3.4",
+ "ws": "~8.11.0"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
+ "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
+ "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
+ "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/examples/videoroom-ms/package.json b/examples/videoroom-ms/package.json
new file mode 100644
index 0000000..2e43b92
--- /dev/null
+++ b/examples/videoroom-ms/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "janode-videoroom-ms",
+ "description": "Janode videoroom multistream app",
+ "type": "module",
+ "keywords": [
+ "janus",
+ "webrtc",
+ "meetecho"
+ ],
+ "author": {
+ "name": "Alessandro Toppi",
+ "email": "atoppi@meetecho.com"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/meetecho/janode.git"
+ },
+ "license": "ISC",
+ "private": true,
+ "main": "src/index.js",
+ "dependencies": {
+ "express": "^4.13.4",
+ "socket.io": "^4.2.0"
+ },
+ "scripts": {
+ "build": "npm install --omit=dev",
+ "build-config": "node -e \"var fs = require('fs');fs.createReadStream('src/config.template.js').pipe(fs.createWriteStream('src/config.js'));\"",
+ "start": "node src/index.js"
+ }
+}
\ No newline at end of file
diff --git a/examples/videoroom-ms/src/config.template.js b/examples/videoroom-ms/src/config.template.js
new file mode 100644
index 0000000..a82aafe
--- /dev/null
+++ b/examples/videoroom-ms/src/config.template.js
@@ -0,0 +1,16 @@
+export default {
+ janode: {
+ address: [{
+ url: 'ws://127.0.0.1:8188/',
+ apisecret: 'secret'
+ }],
+ // seconds between retries after a connection setup error
+ retry_time_secs: 10
+ },
+ web: {
+ port: 4443,
+ bind: '0.0.0.0',
+ key: '/path/to/key.pem',
+ cert: '/path/to/cert.pem'
+ }
+};
\ No newline at end of file
diff --git a/examples/videoroom-ms/src/index.js b/examples/videoroom-ms/src/index.js
new file mode 100644
index 0000000..effa1ee
--- /dev/null
+++ b/examples/videoroom-ms/src/index.js
@@ -0,0 +1,638 @@
+'use strict';
+
+import { readFileSync } from 'fs';
+import Janode from '../../../src/janode.js';
+import config from './config.js';
+const { janode: janodeConfig, web: serverConfig } = config;
+
+import { fileURLToPath } from 'url';
+import { dirname, basename } from 'path';
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const { Logger } = Janode;
+const LOG_NS = `[${basename(__filename)}]`;
+import VideoRoomPlugin from '../../../src/plugins/videoroom-plugin.js';
+
+import express from 'express';
+const app = express();
+const options = {
+ key: serverConfig.key ? readFileSync(serverConfig.key) : null,
+ cert: serverConfig.cert ? readFileSync(serverConfig.cert) : null,
+};
+import { createServer as createHttpsServer } from 'https';
+import { createServer as createHttpServer } from 'http';
+const httpServer = (options.key && options.cert) ? createHttpsServer(options, app) : createHttpServer(app);
+import { Server } from 'socket.io';
+const io = new Server(httpServer);
+
+const scheduleBackEndConnection = (function () {
+ let task = null;
+
+ return (function (del = 10) {
+ if (task) return;
+ Logger.info(`${LOG_NS} scheduled connection in ${del} seconds`);
+ task = setTimeout(() => {
+ initBackEnd()
+ .then(() => task = null)
+ .catch(() => {
+ task = null;
+ scheduleBackEndConnection();
+ });
+ }, del * 1000);
+ });
+})();
+
+let janodeSession;
+let janodeManagerHandle;
+
+(function main() {
+
+ initFrontEnd().catch(({ message }) => Logger.error(`${LOG_NS} failure initializing front-end: ${message}`));
+
+ scheduleBackEndConnection(1);
+
+})();
+
+async function initBackEnd() {
+ Logger.info(`${LOG_NS} connecting Janode...`);
+ let connection;
+
+ try {
+ connection = await Janode.connect(janodeConfig);
+ Logger.info(`${LOG_NS} connection with Janus created`);
+
+ connection.once(Janode.EVENT.CONNECTION_CLOSED, () => {
+ Logger.info(`${LOG_NS} connection with Janus closed`);
+ });
+
+ connection.once(Janode.EVENT.CONNECTION_ERROR, error => {
+ Logger.error(`${LOG_NS} connection with Janus error: ${error.message}`);
+
+ replyError(io, 'backend-failure');
+
+ scheduleBackEndConnection();
+ });
+
+ const session = await connection.create();
+ Logger.info(`${LOG_NS} session ${session.id} with Janus created`);
+ janodeSession = session;
+
+ session.once(Janode.EVENT.SESSION_DESTROYED, () => {
+ Logger.info(`${LOG_NS} session ${session.id} destroyed`);
+ janodeSession = null;
+ });
+
+ const handle = await session.attach(VideoRoomPlugin);
+ Logger.info(`${LOG_NS} manager handle ${handle.id} attached`);
+ janodeManagerHandle = handle;
+
+ // generic handle events
+ handle.once(Janode.EVENT.HANDLE_DETACHED, () => {
+ Logger.info(`${LOG_NS} ${handle.name} manager handle detached event`);
+ });
+ }
+ catch (error) {
+ Logger.error(`${LOG_NS} Janode setup error: ${error.message}`);
+ if (connection) connection.close().catch(() => { });
+
+ // notify clients
+ replyError(io, 'backend-failure');
+
+ throw error;
+ }
+}
+
+function initFrontEnd() {
+ if (httpServer.listening) return Promise.reject(new Error('Server already listening'));
+
+ Logger.info(`${LOG_NS} initializing socketio front end...`);
+
+ io.on('connection', function (socket) {
+ const remote = `[${socket.request.connection.remoteAddress}:${socket.request.connection.remotePort}]`;
+ Logger.info(`${LOG_NS} ${remote} connection with client established`);
+
+ const msHandles = (function () {
+ const handles = {
+ pub: null,
+ sub: null,
+ };
+
+ return {
+ setPubHandle: handle => {
+ handles.pub = handle;
+ },
+ setSubHandle: handle => {
+ handles.sub = handle;
+ },
+ getPubHandle: _ => {
+ return handles.pub;
+ },
+ getSubHandle: _ => {
+ return handles.sub;
+ },
+ getHandleByFeed: feed => {
+ if (feed && handles.pub && feed === handles.pub.feed) return handles.pub;
+ if (!feed && handles.sub) return handles.sub;
+ return null;
+ },
+ detachPubHandle: async _ => {
+ if (handles.pub)
+ await handles.pub.detach().catch(() => { });
+ handles.pub = null;
+ },
+ detachSubHandle: async _ => {
+ if (handles.sub)
+ await handles.sub.detach().catch(() => { });
+ handles.sub = null;
+ },
+ detachAll: async _ => {
+ const detaches = Object.values(handles).map(h => h && h.detach().catch(() => { }));
+ await Promise.all(detaches);
+ handles.pub = null;
+ handles.sub = null;
+ },
+ };
+ })();
+
+ /*----------*/
+ /* USER API */
+ /*----------*/
+
+ socket.on('join', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} join received`);
+ const { _id, data: joindata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, true, socket, evtdata)) return;
+
+ let pubHandle;
+
+ try {
+ pubHandle = await janodeSession.attach(VideoRoomPlugin);
+ Logger.info(`${LOG_NS} ${remote} videoroom publisher handle ${pubHandle.id} attached`);
+ msHandles.setPubHandle(pubHandle);
+
+ // custom videoroom publisher/manager events
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_DESTROYED, evtdata => {
+ replyEvent(socket, 'destroyed', evtdata);
+ });
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_PUB_LIST, evtdata => {
+ replyEvent(socket, 'feed-list', evtdata);
+ });
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_PUB_PEER_JOINED, evtdata => {
+ replyEvent(socket, 'feed-joined', evtdata);
+ });
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_UNPUBLISHED, evtdata => {
+ replyEvent(socket, 'unpublished', evtdata);
+ });
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_LEAVING, async evtdata => {
+ if (pubHandle.feed === evtdata.feed) {
+ await msHandles.detachPubHandle();
+ }
+ replyEvent(socket, 'leaving', evtdata);
+ });
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_DISPLAY, evtdata => {
+ replyEvent(socket, 'display', evtdata);
+ });
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_TALKING, evtdata => {
+ replyEvent(socket, 'talking', evtdata);
+ });
+
+ pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_KICKED, async evtdata => {
+ replyEvent(socket, 'kicked', evtdata);
+ });
+
+ // generic videoroom events
+ pubHandle.on(Janode.EVENT.HANDLE_WEBRTCUP, () => Logger.info(`${LOG_NS} ${pubHandle.name} webrtcup event`));
+ pubHandle.on(Janode.EVENT.HANDLE_MEDIA, evtdata => Logger.info(`${LOG_NS} ${pubHandle.name} media event ${JSON.stringify(evtdata)}`));
+ pubHandle.on(Janode.EVENT.HANDLE_SLOWLINK, evtdata => Logger.info(`${LOG_NS} ${pubHandle.name} slowlink event ${JSON.stringify(evtdata)}`));
+ pubHandle.on(Janode.EVENT.HANDLE_HANGUP, evtdata => Logger.info(`${LOG_NS} ${pubHandle.name} hangup event ${JSON.stringify(evtdata)}`));
+ pubHandle.on(Janode.EVENT.HANDLE_DETACHED, () => {
+ Logger.info(`${LOG_NS} ${pubHandle.name} detached event`);
+ msHandles.setPubHandle(null);
+ });
+ pubHandle.on(Janode.EVENT.HANDLE_TRICKLE, evtdata => Logger.info(`${LOG_NS} ${pubHandle.name} trickle event ${JSON.stringify(evtdata)}`));
+
+ const response = await pubHandle.joinPublisher(joindata);
+
+ replyEvent(socket, 'joined', response, _id);
+
+ Logger.info(`${LOG_NS} ${remote} joined sent`);
+ } catch ({ message }) {
+ if (pubHandle) pubHandle.detach().catch(() => { });
+ replyError(socket, message, joindata, _id);
+ }
+ });
+
+ socket.on('subscribe', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} subscribe received`);
+ const { _id, data: subscribedata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, true, socket, evtdata)) return;
+
+ let subHandle = msHandles.getSubHandle();
+ let response;
+
+ try {
+ if (!subHandle) {
+ subHandle = await janodeSession.attach(VideoRoomPlugin);
+ Logger.info(`${LOG_NS} ${remote} videoroom listener handle ${subHandle.id} attached`);
+ msHandles.setSubHandle(subHandle);
+ // generic videoroom events
+ subHandle.on(Janode.EVENT.HANDLE_WEBRTCUP, () => Logger.info(`${LOG_NS} ${subHandle.name} webrtcup event`));
+ subHandle.on(Janode.EVENT.HANDLE_SLOWLINK, evtdata => Logger.info(`${LOG_NS} ${subHandle.name} slowlink event ${JSON.stringify(evtdata)}`));
+ subHandle.on(Janode.EVENT.HANDLE_HANGUP, evtdata => Logger.info(`${LOG_NS} ${subHandle.name} hangup event ${JSON.stringify(evtdata)}`));
+ subHandle.once(Janode.EVENT.HANDLE_DETACHED, () => {
+ Logger.info(`${LOG_NS} ${subHandle.name} detached event`);
+ msHandles.setSubHandle(null);
+ });
+ subHandle.on(Janode.EVENT.HANDLE_TRICKLE, evtdata => Logger.info(`${LOG_NS} ${subHandle.name} trickle event ${JSON.stringify(evtdata)}`));
+
+ // specific videoroom events
+ subHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_UPDATED, evtdata => {
+ Logger.info(`${LOG_NS} ${subHandle.name} updated event`);
+ replyEvent(socket, 'updated', evtdata);
+ });
+ response = await subHandle.joinSubscriber(subscribedata);
+ }
+ else {
+ response = await subHandle.update({
+ subscribe: subscribedata.streams,
+ });
+ }
+
+ replyEvent(socket, 'subscribed', response, _id);
+ Logger.info(`${LOG_NS} ${remote} subscribed sent`);
+ } catch ({ message }) {
+ replyError(socket, message, subscribedata, _id);
+ }
+ });
+
+ socket.on('unsubscribe', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} unsubscribe received`);
+ const { _id, data: unsubscribedata = {} } = evtdata;
+
+ let subHandle = msHandles.getSubHandle();
+ if (!checkSessions(janodeSession, subHandle, socket, evtdata)) return;
+ let response;
+
+ try {
+ response = await subHandle.update({
+ unsubscribe: unsubscribedata.streams,
+ });
+
+ replyEvent(socket, 'unsubscribed', response, _id);
+ Logger.info(`${LOG_NS} ${remote} unsubscribed sent`);
+ } catch ({ message }) {
+ replyError(socket, message, unsubscribedata, _id);
+ }
+ });
+
+ socket.on('configure', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} configure received`);
+ const { _id, data: confdata = {} } = evtdata;
+
+ const handle = msHandles.getHandleByFeed(confdata.feed);
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ try {
+ const response = await handle.configure(confdata);
+ delete response.configured;
+ replyEvent(socket, 'configured', response, _id);
+ Logger.info(`${LOG_NS} ${remote} configured sent`);
+ } catch ({ message }) {
+ replyError(socket, message, confdata, _id);
+ }
+ });
+
+ socket.on('unpublish', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} unpublish received`);
+ const { _id, data: unpubdata = {} } = evtdata;
+
+ const handle = msHandles.getPubHandle();
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ try {
+ const response = await handle.unpublish();
+ replyEvent(socket, 'unpublished', response, _id);
+ Logger.info(`${LOG_NS} ${remote} unpublished sent`);
+ } catch ({ message }) {
+ replyError(socket, message, unpubdata, _id);
+ }
+ });
+
+ socket.on('leave', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} leave received`);
+ const { _id, data: leavedata = {} } = evtdata;
+
+ const handle = msHandles.getHandleByFeed(leavedata.feed);
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ try {
+ const response = await handle.leave();
+ replyEvent(socket, 'leaving', response, _id);
+ Logger.info(`${LOG_NS} ${remote} leaving sent`);
+ handle.detach().catch(() => { });
+ } catch ({ message }) {
+ replyError(socket, message, leavedata, _id);
+ }
+ });
+
+ socket.on('start', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} start received`);
+ const { _id, data: startdata = {} } = evtdata;
+
+ const handle = msHandles.getSubHandle();
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ try {
+ const response = await handle.start(startdata);
+ replyEvent(socket, 'started', response, _id);
+ Logger.info(`${LOG_NS} ${remote} started sent`);
+ } catch ({ message }) {
+ replyError(socket, message, startdata, _id);
+ }
+ });
+
+ socket.on('pause', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} pause received`);
+ const { _id, data: pausedata = {} } = evtdata;
+
+ const handle = msHandles.getSubHandle();
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ try {
+ const response = await handle.pause();
+ replyEvent(socket, 'paused', response, _id);
+ Logger.info(`${LOG_NS} ${remote} paused sent`);
+ } catch ({ message }) {
+ replyError(socket, message, pausedata, _id);
+ }
+ });
+
+ socket.on('switch', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} switch received`);
+ const { _id, data: switchdata = {} } = evtdata;
+
+ const handle = msHandles.getSubHandle();
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ try {
+ const response = await handle.switch(switchdata);
+ replyEvent(socket, 'switched', response, _id);
+ Logger.info(`${LOG_NS} ${remote} switched sent`);
+ } catch ({ message }) {
+ replyError(socket, message, switchdata, _id);
+ }
+ });
+
+ // trickle candidate from the client
+ socket.on('trickle', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} trickle received`);
+ const { _id, data: trickledata = {} } = evtdata;
+
+ const handle = msHandles.getHandleByFeed(trickledata.feed);
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ handle.trickle(trickledata.candidate).catch(({ message }) => replyError(socket, message, trickledata, _id));
+ });
+
+ // trickle complete signal from the client
+ socket.on('trickle-complete', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} trickle-complete received`);
+ const { _id, data: trickledata = {} } = evtdata;
+
+ const handle = msHandles.getHandleByFeed(trickledata.feed);
+ if (!checkSessions(janodeSession, handle, socket, evtdata)) return;
+
+ handle.trickleComplete(trickledata.candidate).catch(({ message }) => replyError(socket, message, trickledata, _id));
+ });
+
+ // socket disconnection event
+ socket.on('disconnect', async () => {
+ Logger.info(`${LOG_NS} ${remote} disconnected socket`);
+
+ await msHandles.detachAll();
+ });
+
+
+ /*----------------*/
+ /* Management API */
+ /*----------------*/
+
+
+ socket.on('list-participants', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} list_participants received`);
+ const { _id, data: listdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.listParticipants(listdata);
+ replyEvent(socket, 'participants-list', response, _id);
+ Logger.info(`${LOG_NS} ${remote} participants-list sent`);
+ } catch ({ message }) {
+ replyError(socket, message, listdata, _id);
+ }
+ });
+
+ socket.on('kick', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} kick received`);
+ const { _id, data: kickdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.kick(kickdata);
+ replyEvent(socket, 'kicked', response, _id);
+ Logger.info(`${LOG_NS} ${remote} kicked sent`);
+ } catch ({ message }) {
+ replyError(socket, message, kickdata, _id);
+ }
+ });
+
+ socket.on('exists', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} exists received`);
+ const { _id, data: existsdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.exists(existsdata);
+ replyEvent(socket, 'exists', response, _id);
+ Logger.info(`${LOG_NS} ${remote} exists sent`);
+ } catch ({ message }) {
+ replyError(socket, message, existsdata, _id);
+ }
+ });
+
+ socket.on('list-rooms', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} list-rooms received`);
+ const { _id, data: listdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.list();
+ replyEvent(socket, 'rooms-list', response, _id);
+ Logger.info(`${LOG_NS} ${remote} rooms-list sent`);
+ } catch ({ message }) {
+ replyError(socket, message, listdata, _id);
+ }
+ });
+
+ socket.on('create', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} create received`);
+ const { _id, data: createdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.create(createdata);
+ replyEvent(socket, 'created', response, _id);
+ Logger.info(`${LOG_NS} ${remote} created sent`);
+ } catch ({ message }) {
+ replyError(socket, message, createdata, _id);
+ }
+ });
+
+ socket.on('destroy', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} destroy received`);
+ const { _id, data: destroydata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.destroy(destroydata);
+ replyEvent(socket, 'destroyed', response, _id);
+ Logger.info(`${LOG_NS} ${remote} destroyed sent`);
+ } catch ({ message }) {
+ replyError(socket, message, destroydata, _id);
+ }
+ });
+
+ socket.on('allow', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} allow received`);
+ const { _id, data: allowdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.allow(allowdata);
+ replyEvent(socket, 'allowed', response, _id);
+ Logger.info(`${LOG_NS} ${remote} allowed sent`);
+ } catch ({ message }) {
+ replyError(socket, message, allowdata, _id);
+ }
+ });
+
+ socket.on('rtp-fwd-start', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} rtp-fwd-start received`);
+ const { _id, data: rtpstartdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.startForward(rtpstartdata);
+ replyEvent(socket, 'rtp-fwd-started', response, _id);
+ Logger.info(`${LOG_NS} ${remote} rtp-fwd-started sent`);
+ } catch ({ message }) {
+ replyError(socket, message, rtpstartdata, _id);
+ }
+ });
+
+ socket.on('rtp-fwd-stop', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} rtp-fwd-stop received`);
+ const { _id, data: rtpstopdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.stopForward(rtpstopdata);
+ replyEvent(socket, 'rtp-fwd-stopped', response, _id);
+ Logger.info(`${LOG_NS} ${remote} rtp-fwd-stopped sent`);
+ } catch ({ message }) {
+ replyError(socket, message, rtpstopdata, _id);
+ }
+ });
+
+ socket.on('rtp-fwd-list', async (evtdata = {}) => {
+ Logger.info(`${LOG_NS} ${remote} rtp_fwd_list received`);
+ const { _id, data: rtplistdata = {} } = evtdata;
+
+ if (!checkSessions(janodeSession, janodeManagerHandle, socket, evtdata)) return;
+
+ try {
+ const response = await janodeManagerHandle.listForward(rtplistdata);
+ replyEvent(socket, 'rtp-fwd-list', response, _id);
+ Logger.info(`${LOG_NS} ${remote} rtp-fwd-list sent`);
+ } catch ({ message }) {
+ replyError(socket, message, rtplistdata, _id);
+ }
+ });
+
+ });
+
+ // disable caching for all app
+ app.set('etag', false).set('view cache', false);
+
+ // static content
+ app.use('/janode', express.static(__dirname + '/../html/', {
+ etag: false,
+ lastModified: false,
+ maxAge: 0,
+ }));
+
+ // http server binding
+ return new Promise((resolve, reject) => {
+ // web server binding
+ httpServer.listen(
+ serverConfig.port,
+ serverConfig.bind,
+ () => {
+ Logger.info(`${LOG_NS} server listening on ${(options.key && options.cert) ? 'https' : 'http'}://${serverConfig.bind}:${serverConfig.port}/janode`);
+ resolve();
+ }
+ );
+
+ httpServer.on('error', e => reject(e));
+ });
+}
+
+function checkSessions(session, handle, socket, { data, _id }) {
+ if (!session) {
+ replyError(socket, 'session-not-available', data, _id);
+ return false;
+ }
+ if (!handle) {
+ replyError(socket, 'handle-not-available', data, _id);
+ return false;
+ }
+ return true;
+}
+
+function replyEvent(socket, evtname, data, _id) {
+ const evtdata = {
+ data,
+ };
+ if (_id) evtdata._id = _id;
+
+ socket.emit(evtname, evtdata);
+}
+
+function replyError(socket, message, request, _id) {
+ const evtdata = {
+ error: message,
+ };
+ if (request) evtdata.request = request;
+ if (_id) evtdata._id = _id;
+
+ socket.emit('videoroom-error', evtdata);
+}
diff --git a/src/handle.js b/src/handle.js
index 92ac64d..56edebd 100644
--- a/src/handle.js
+++ b/src/handle.js
@@ -294,6 +294,9 @@ class Handle extends EventEmitter {
case JANUS.EVENT.MEDIA: {
if (typeof janus_message.type !== 'undefined') janode_event_data.type = janus_message.type;
if (typeof janus_message.receiving !== 'undefined') janode_event_data.receiving = janus_message.receiving;
+ if (typeof janus_message.mid !== 'undefined') janode_event_data.mid = janus_message.mid;
+ if (typeof janus_message.substream !== 'undefined') janode_event_data.substream = janus_message.substream;
+ if (typeof janus_message.seconds !== 'undefined') janode_event_data.substream = janus_message.seconds;
/**
* The handle received a media notification.
*
@@ -301,6 +304,9 @@ class Handle extends EventEmitter {
* @type {object}
* @property {string} type - The kind of media (audio/video)
* @property {boolean} receiving - True if Janus is receiving media
+ * @property {string} [mid] - The involved mid
+ * @property {number} [substream] - The involved simulcast substream
+ * @property {number} [seconds] - Time, in seconds, with no media
*/
this.emit(JANODE.EVENT.HANDLE_MEDIA, janode_event_data);
break;
@@ -321,14 +327,18 @@ class Handle extends EventEmitter {
/* In this case the janus message has "uplink" and "nacks" fields */
case JANUS.EVENT.SLOWLINK: {
if (typeof janus_message.uplink !== 'undefined') janode_event_data.uplink = janus_message.uplink;
- if (typeof janus_message.nacks !== 'undefined') janode_event_data.nacks = janus_message.nacks;
+ if (typeof janus_message.mid !== 'undefined') janode_event_data.mid = janus_message.mid;
+ if (typeof janus_message.media !== 'undefined') janode_event_data.media = janus_message.media;
+ if (typeof janus_message.lost !== 'undefined') janode_event_data.lost = janus_message.lost;
/**
* The handle has received a slowlink notification.
*
* @event module:handle~Handle#event:HANDLE_SLOWLINK
* @type {object}
* @property {boolean} uplink - The direction of the slow link
- * @property {number} nacks - Number of nacks in the last time slot
+ * @property {string} media - The media kind (audio/video)
+ * @property {string} [mid] - The involved stream mid
+ * @property {number} lost - Number of missing packets in the last time slot
*/
this.emit(JANODE.EVENT.HANDLE_SLOWLINK, janode_event_data);
break;
diff --git a/src/plugins/videoroom-plugin.js b/src/plugins/videoroom-plugin.js
index 805bf23..b448aa6 100644
--- a/src/plugins/videoroom-plugin.js
+++ b/src/plugins/videoroom-plugin.js
@@ -98,6 +98,14 @@ class VideoRoomHandle extends Handle {
*/
this.feed = null;
+ /**
+ * [multistream]
+ * Either the streams assigned to this publisher handle or the streams subscribed to in case this handle is a subscriber.
+ *
+ * @type {object[]}
+ */
+ this.streams = null;
+
/**
* The identifier of the videoroom the handle has joined.
*
@@ -205,18 +213,24 @@ class VideoRoomHandle extends Handle {
case 'attached':
/* Store room and feed id */
this.room = room;
- this.feed = message_data.id;
+ if (typeof message_data.id !== 'undefined') {
+ this.feed = message_data.id;
+ janode_event.data.feed = message_data.id;
+ janode_event.data.display = message_data.display;
+ }
- janode_event.data.feed = message_data.id;
- janode_event.data.display = message_data.display;
/* [multistream] add streams info to the subscriber joined event */
- if (typeof message_data.streams !== 'undefined') janode_event.data.streams = message_data.streams;
+ if (typeof message_data.streams !== 'undefined') {
+ this.streams = message_data.streams;
+ janode_event.data.streams = message_data.streams;
+ }
+
janode_event.event = PLUGIN_EVENT.SUB_JOINED;
break;
/* Slow-link event */
case 'slow_link':
- janode_event.data.feed = this.feed;
+ if (this.feed) janode_event.data.feed = this.feed;
janode_event.data.bitrate = message_data['current-bitrate'];
janode_event.event = PLUGIN_EVENT.SLOW_LINK;
break;
@@ -249,31 +263,81 @@ class VideoRoomHandle extends Handle {
/* RTP forwarding started */
case 'rtp_forward':
janode_event.data.feed = message_data.publisher_id;
- janode_event.data.forwarder = {
- host: message_data.rtp_stream.host,
- };
- if (message_data.rtp_stream.audio) {
- janode_event.data.forwarder.audio_port = message_data.rtp_stream.audio;
- janode_event.data.forwarder.audio_rtcp_port = message_data.rtp_stream.audio_rtcp;
- janode_event.data.forwarder.audio_stream = message_data.rtp_stream.audio_stream_id;
- }
- if (message_data.rtp_stream.video) {
- janode_event.data.forwarder.video_port = message_data.rtp_stream.video;
- janode_event.data.forwarder.video_rtcp_port = message_data.rtp_stream.video_rtcp;
- janode_event.data.forwarder.video_stream = message_data.rtp_stream.video_stream_id;
- if (message_data.rtp_stream.video_stream_id_2) {
- janode_event.data.forwarder.video_port_2 = message_data.rtp_stream.video_2;
- janode_event.data.forwarder.video_stream_2 = message_data.rtp_stream.video_stream_id_2;
+ if (message_data.rtp_stream) {
+ const f = message_data.rtp_stream;
+ const fwd = {
+ host: f.host,
+ };
+ if (f.audio_stream_id) {
+ fwd.audio_stream = f.audio_stream_id;
+ fwd.audio_port = f.audio;
+ if (typeof f.audio_rtcp === 'number') {
+ fwd.audio_rtcp_port = f.audio_rtcp;
+ }
+ }
+ if (f.video_stream_id) {
+ fwd.video_stream = f.video_stream_id;
+ fwd.video_port = f.video;
+ if (typeof f.video_rtcp === 'number') {
+ fwd.video_rtcp_port = f.video_rtcp;
+ }
+ if (f.video_stream_id_2) {
+ fwd.video_stream_2 = f.video_stream_id_2;
+ fwd.video_port_2 = f.video_2;
+ }
+ if (f.video_stream_id_3) {
+ fwd.video_stream_3 = f.video_stream_id_3;
+ fwd.video_port_3 = f.video_3;
+ }
}
- if (message_data.rtp_stream.video_stream_id_3) {
- janode_event.data.forwarder.video_port_3 = message_data.rtp_stream.video_3;
- janode_event.data.forwarder.video_stream_3 = message_data.rtp_stream.video_stream_id_3;
+ if (f.data_stream_id) {
+ fwd.data_stream = f.data_stream_id;
+ fwd.data_port = f.data;
}
+
+ janode_event.data.forwarder = fwd;
}
- if (message_data.rtp_stream.data) {
- janode_event.data.forwarder.data_port = message_data.rtp_stream.data;
- janode_event.data.forwarder.data_stream = message_data.rtp_stream.data_stream_id;
+ /* [multistream] */
+ else if (message_data.forwarders) {
+ janode_event.data.forwarders = message_data.forwarders.map(f => {
+ const fwd = {
+ host: f.host,
+ };
+ if (f.type === 'audio') {
+ fwd.audio_stream = f.stream_id;
+ fwd.audio_port = f.port;
+ if (typeof f.remote_rtcp_port === 'number') {
+ fwd.audio_rtcp_port = f.remote_rtcp_port;
+ }
+ }
+ if (f.type === 'video') {
+ fwd.video_stream = f.stream_id;
+ fwd.video_port = f.port;
+ if (typeof f.remote_rtcp_port === 'number') {
+ fwd.video_rtcp_port = f.remote_rtcp_port;
+ }
+ if (typeof f.substream === 'number') {
+ fwd.sc_substream_layer = f.substream;
+ }
+ }
+ if (f.type === 'data') {
+ fwd.data_stream = f.stream_id;
+ fwd.data_port = f.port;
+ }
+ if (typeof f.ssrc === 'number') {
+ fwd.ssrc = f.ssrc;
+ }
+ if (typeof f.pt === 'number') {
+ fwd.pt = f.pt;
+ }
+ if (typeof f.srtp === 'boolean') {
+ fwd.srtp = f.srtp;
+ }
+
+ return fwd;
+ });
}
+
janode_event.event = PLUGIN_EVENT.RTP_FWD_STARTED;
break;
@@ -292,69 +356,89 @@ class VideoRoomHandle extends Handle {
feed: publisher_id,
};
- pub.forwarders = rtp_forwarder.map(forw => {
- const forwarder = {
- host: forw.ip,
+ pub.forwarders = rtp_forwarder.map(f => {
+ const fwd = {
+ host: f.ip,
};
-
- if (forw.audio_stream_id) {
- forwarder.audio_port = forw.port;
- forwarder.audio_rtcp_port = forw.remote_rtcp_port;
- forwarder.audio_stream = forw.audio_stream_id;
+ if (f.audio_stream_id) {
+ fwd.audio_stream = f.audio_stream_id;
+ fwd.audio_port = f.port;
+ if (typeof f.remote_rtcp_port === 'number') {
+ fwd.audio_rtcp_port = f.remote_rtcp_port;
+ }
+ }
+ if (f.video_stream_id) {
+ fwd.video_stream = f.video_stream_id;
+ fwd.video_port = f.port;
+ if (typeof f.remote_rtcp_port === 'number') {
+ fwd.video_rtcp_port = f.remote_rtcp_port;
+ }
+ if (typeof f.substream === 'number') {
+ fwd.sc_substream_layer = f.substream;
+ }
}
- if (forw.video_stream_id) {
- forwarder.video_port = forw.port;
- forwarder.video_rtcp_port = forw.remote_rtcp_port;
- forwarder.video_stream = forw.video_stream_id;
+ if (f.data_stream_id) {
+ fwd.data_stream = f.data_stream_id;
+ fwd.data_port = f.port;
}
- if (forw.data_stream_id) {
- forwarder.data_port = forw.port;
- forwarder.data_stream = forw.data_stream_id;
+ if (typeof f.ssrc === 'number') {
+ fwd.ssrc = f.ssrc;
+ }
+ if (typeof f.pt === 'number') {
+ fwd.pt = f.pt;
+ }
+ if (typeof f.srtp === 'boolean') {
+ fwd.srtp = f.srtp;
}
- return forwarder;
+ return fwd;
});
return pub;
});
}
+ /* [multistream] */
else if (message_data.publishers) {
janode_event.data.forwarders = message_data.publishers.map(({ publisher_id, forwarders }) => {
const pub = {
feed: publisher_id,
};
- pub.forwarders = forwarders.map(forw => {
- const forwarder = {
- host: forw.host,
+ pub.forwarders = forwarders.map(f => {
+ const fwd = {
+ host: f.host,
};
-
- if (forw.type === 'audio') {
- forwarder.audio_port = forw.port;
- forwarder.audio_rtcp_port = forw.remote_rtcp_port;
- forwarder.audio_stream = forw.stream_id;
+ if (f.type === 'audio') {
+ fwd.audio_stream = f.stream_id;
+ fwd.audio_port = f.port;
+ if (typeof f.remote_rtcp_port === 'number') {
+ fwd.audio_rtcp_port = f.remote_rtcp_port;
+ }
}
- if (forw.type === 'video') {
- forwarder.video_port = forw.port;
- forwarder.video_rtcp_port = forw.remote_rtcp_port;
- forwarder.video_stream = forw.stream_id;
- if (typeof forw.substream !== 'undefined') {
- forwarder.sc_substream_layer = forw.substream;
+ if (f.type === 'video') {
+ fwd.video_stream = f.stream_id;
+ fwd.video_port = f.port;
+ if (typeof f.remote_rtcp_port === 'number') {
+ fwd.video_rtcp_port = f.remote_rtcp_port;
+ }
+ if (typeof f.substream === 'number') {
+ fwd.sc_substream_layer = f.substream;
}
}
- if (forw.type === 'data') {
- forwarder.data_port = forw.port;
- forwarder.data_stream = forw.stream_id;
+ if (f.type === 'data') {
+ fwd.data_stream = f.stream_id;
+ fwd.data_port = f.port;
}
-
- if (typeof forw.ssrc !== 'undefined')
- forwarder.ssrc = forw.ssrc;
- if (typeof forw.pt !== 'undefined')
- forwarder.pt = forw.pt;
- if (typeof forw.srtp !== 'undefined')
- forwarder.srtp = forw.srtp;
-
- return forwarder;
+ if (typeof f.ssrc === 'number') {
+ fwd.ssrc = f.ssrc;
+ }
+ if (typeof f.pt === 'number') {
+ fwd.pt = f.pt;
+ }
+ if (typeof f.srtp === 'boolean') {
+ fwd.srtp = f.srtp;
+ }
+ return fwd;
});
return pub;
@@ -369,6 +453,8 @@ class VideoRoomHandle extends Handle {
case 'stopped-talking':
janode_event.data.feed = message_data.id;
janode_event.data.talking = (videoroom === 'talking');
+ /* [multistream] */
+ if (typeof message_data.mid !== 'undefined') janode_event.data.mid = message_data.mid;
janode_event.data.audio_level = message_data['audio-level-dBov-avg'];
janode_event.event = PLUGIN_EVENT.TALKING;
break;
@@ -424,7 +510,7 @@ class VideoRoomHandle extends Handle {
/* Configuration events (publishing, general configuration) */
if (typeof message_data.configured !== 'undefined') {
janode_event.event = PLUGIN_EVENT.CONFIGURED;
- janode_event.data.feed = this.feed;
+ if (this.feed) janode_event.data.feed = this.feed;
/* [multistream] add streams info */
if (typeof message_data.streams !== 'undefined') janode_event.data.streams = message_data.streams;
janode_event.data.configured = message_data.configured;
@@ -440,14 +526,14 @@ class VideoRoomHandle extends Handle {
/* Subscribed feed started */
if (typeof message_data.started !== 'undefined') {
janode_event.event = PLUGIN_EVENT.STARTED;
- janode_event.data.feed = this.feed;
+ if (this.feed) janode_event.data.feed = this.feed;
janode_event.data.started = message_data.started;
break;
}
/* Subscribed feed paused */
if (typeof message_data.paused !== 'undefined') {
janode_event.event = PLUGIN_EVENT.PAUSED;
- janode_event.data.feed = this.feed;
+ if (this.feed) janode_event.data.feed = this.feed;
janode_event.data.paused = message_data.paused;
break;
}
@@ -455,11 +541,17 @@ class VideoRoomHandle extends Handle {
if (typeof message_data.switched !== 'undefined') {
janode_event.event = PLUGIN_EVENT.SWITCHED;
janode_event.data.switched = message_data.switched;
- if (message_data.switched === 'ok' && typeof message_data.id !== 'undefined') {
- janode_event.data.from_feed = this.feed;
- this.feed = message_data.id;
- janode_event.data.to_feed = this.feed;
- janode_event.data.display = message_data.display;
+ if (message_data.switched === 'ok') {
+ if (typeof message_data.id !== 'undefined') {
+ janode_event.data.from_feed = this.feed;
+ this.feed = message_data.id;
+ janode_event.data.to_feed = this.feed;
+ janode_event.data.display = message_data.display;
+ }
+ if (typeof message_data.streams != 'undefined') {
+ this.streams = message_data.streams;
+ janode_event.data.streams = message_data.streams;
+ }
}
break;
}
@@ -485,20 +577,24 @@ class VideoRoomHandle extends Handle {
/* Participant left (for subscribers "leave") */
if (typeof message_data.left !== 'undefined') {
janode_event.event = PLUGIN_EVENT.LEAVING;
- janode_event.data.feed = this.feed;
+ if (this.feed) janode_event.data.feed = this.feed;
break;
}
/* Simulcast substream layer switch */
if (typeof message_data.substream !== 'undefined') {
janode_event.event = PLUGIN_EVENT.SC_SUBSTREAM_LAYER;
- janode_event.data.feed = this.feed;
+ if (this.feed) janode_event.data.feed = this.feed;
+ /* [multistream] */
+ if (typeof message_data.mid !== 'undefined') janode_event.data.mid = message_data.mid;
janode_event.data.sc_substream_layer = message_data.substream;
break;
}
/* Simulcast temporal layers switch */
if (typeof message_data.temporal !== 'undefined') {
janode_event.event = PLUGIN_EVENT.SC_TEMPORAL_LAYERS;
- janode_event.data.feed = this.feed;
+ if (this.feed) janode_event.data.feed = this.feed;
+ /* [multistream] */
+ if (typeof message_data.mid !== 'undefined') janode_event.data.mid = message_data.mid;
janode_event.data.sc_temporal_layers = message_data.temporal;
break;
}
@@ -539,9 +635,10 @@ class VideoRoomHandle extends Handle {
* @param {string} [params.pin] - The optional pin needed to join the room
* @param {boolean} [params.record] - Enable the recording
* @param {string} [params.filename] - If recording, the base path/file to use for the recording
+ * @param {object[]} [params.descriptions] - [multistream] The descriptions object, can define a description for the tracks separately e.g. track mid:0 'Video Camera', track mid:1 'Screen'
* @returns {Promise}
*/
- async joinPublisher({ room, feed, audio, video, data, bitrate, record, filename, display, token, pin }) {
+ async joinPublisher({ room, feed, audio, video, data, bitrate, record, filename, display, token, pin, descriptions }) {
const body = {
request: REQUEST_JOIN,
ptype: PTYPE_PUBLISHER,
@@ -558,6 +655,9 @@ class VideoRoomHandle extends Handle {
if (typeof token === 'string') body.token = token;
if (typeof pin === 'string') body.pin = pin;
+ /* [multistream] */
+ if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
+
const response = await this.message(body);
const { event, data: evtdata } = response._janode || {};
if (event === PLUGIN_EVENT.PUB_JOINED) {
@@ -584,10 +684,11 @@ class VideoRoomHandle extends Handle {
* @param {boolean} [params.record] - Enable the recording
* @param {string} [params.filename] - If recording, the base path/file to use for the recording
* @param {boolean} [params.e2ee] - True to notify end-to-end encryption for this connection
+ * @param {object[]} [params.descriptions] - [multistream] The descriptions object, can define a description for the tracks separately e.g. track mid:0 'Video Camera', track mid:1 'Screen'
* @param {RTCSessionDescription} [params.jsep] - The JSEP offer
* @returns {Promise}
*/
- async joinConfigurePublisher({ room, feed, audio, video, data, bitrate, record, filename, display, token, pin, e2ee, jsep }) {
+ async joinConfigurePublisher({ room, feed, audio, video, data, bitrate, record, filename, display, token, pin, e2ee, descriptions, jsep }) {
const body = {
request: REQUEST_JOIN_CONFIGURE,
ptype: PTYPE_PUBLISHER,
@@ -605,6 +706,9 @@ class VideoRoomHandle extends Handle {
if (typeof pin === 'string') body.pin = pin;
if (typeof e2ee === 'boolean' && jsep) jsep.e2ee = e2ee;
+ /* [multistream] */
+ if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
+
const response = await this.message(body, jsep).catch(e => {
/* Cleanup the WebRTC status in Janus in case of errors when publishing */
/*
@@ -667,22 +771,31 @@ class VideoRoomHandle extends Handle {
const body = {
request: REQUEST_CONFIGURE,
};
- if (typeof audio === 'boolean') body.audio = audio;
- if (typeof video === 'boolean') body.video = video;
- if (typeof data === 'boolean') body.data = data;
+
+ /* [multistream] */
+ if (streams && Array.isArray(streams)) {
+ body.streams = streams;
+ }
+ else {
+ if (typeof audio === 'boolean') body.audio = audio;
+ if (typeof video === 'boolean') body.video = video;
+ if (typeof data === 'boolean') body.data = data;
+ if (typeof sc_substream_layer === 'number') body.substream = sc_substream_layer;
+ if (typeof sc_substream_fallback_ms === 'number') body.fallback = 1000 * sc_substream_fallback_ms;
+ if (typeof sc_temporal_layers === 'number') body.temporal = sc_temporal_layers;
+ }
+
if (typeof bitrate === 'number') body.bitrate = bitrate;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof display === 'string') body.display = display;
if (typeof restart === 'boolean') body.restart = restart;
if (typeof update === 'boolean') body.update = update;
- if (streams && Array.isArray(streams)) body.streams = streams;
- if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
- if (typeof sc_substream_layer === 'number') body.substream = sc_substream_layer;
- if (typeof sc_substream_fallback_ms === 'number') body.fallback = 1000 * sc_substream_fallback_ms;
- if (typeof sc_temporal_layers === 'number') body.temporal = sc_temporal_layers;
if (typeof e2ee === 'boolean' && jsep) jsep.e2ee = e2ee;
+ /* [multistream] */
+ if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
+
const response = await this.message(body, jsep).catch(e => {
/* Cleanup the WebRTC status in Janus in case of errors when publishing */
/*
@@ -727,13 +840,12 @@ class VideoRoomHandle extends Handle {
* @param {number} [params.bitrate] - Bitrate cap
* @param {boolean} [params.record] - True to record the feed
* @param {string} [params.filename] - If recording, the base path/file to use for the recording
- * @param {object[]} [params.streams] - [multistream] The streams object, each stream includes type, mid, description, disabled, simulcast
* @param {object[]} [params.descriptions] - [multistream] The descriptions object, for each stream you can define description
* @param {boolean} [params.e2ee] - True to notify end-to-end encryption for this connection
* @param {RTCSessionDescription} params.jsep - The JSEP offer
* @returns {Promise}
*/
- async publish({ audio, video, data, bitrate, record, filename, display, streams, descriptions, e2ee, jsep }) {
+ async publish({ audio, video, data, bitrate, record, filename, display, descriptions, e2ee, jsep }) {
if (typeof jsep === 'object' && jsep && jsep.type !== 'offer') {
const error = new Error('jsep must be an offer');
return Promise.reject(error);
@@ -741,17 +853,22 @@ class VideoRoomHandle extends Handle {
const body = {
request: REQUEST_PUBLISH,
};
+
if (typeof audio === 'boolean') body.audio = audio;
if (typeof video === 'boolean') body.video = video;
if (typeof data === 'boolean') body.data = data;
+
if (typeof bitrate === 'number') body.bitrate = bitrate;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof display === 'string') body.display = display;
- if (streams && Array.isArray(streams)) body.streams = streams;
- if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
if (typeof e2ee === 'boolean' && jsep) jsep.e2ee = e2ee;
+ /* [multistream] */
+ if (descriptions && Array.isArray(descriptions)) {
+ body.descriptions = descriptions;
+ }
+
const response = await this.message(body, jsep).catch(e => {
/* Cleanup the WebRTC status in Janus in case of errors when publishing */
/*
@@ -813,27 +930,38 @@ class VideoRoomHandle extends Handle {
* @param {number} [params.sc_substream_layer] - Substream layer to receive (0-2), in case simulcasting is enabled
* @param {number} [params.sc_substream_fallback_ms] - How much time in ms without receiving packets will make janus drop to the substream below
* @param {number} [params.sc_temporal_layers] - Temporal layers to receive (0-2), in case VP8 simulcasting is enabled
+ * @param {object[]} [params.streams] - [multistream] The streams object, each stream includes feed, mid, send, ...
* @param {boolean} [params.autoupdate] - [multistream] Whether a new SDP offer is sent automatically when a subscribed publisher leaves
+ * @param {boolean} [params.use_msid] - [multistream] Whether subscriptions should include an msid that references the publisher
* @param {string} [params.token] - The optional token needed
* @returns {Promise}
*/
- async joinSubscriber({ room, feed, audio, video, data, private_id, sc_substream_layer, sc_substream_fallback_ms, sc_temporal_layers, autoupdate, token }) {
+ async joinSubscriber({ room, feed, audio, video, data, private_id, sc_substream_layer, sc_substream_fallback_ms, sc_temporal_layers, streams, autoupdate, use_msid, token }) {
const body = {
request: REQUEST_JOIN,
ptype: PTYPE_LISTENER,
room,
- feed,
};
- if (typeof audio === 'boolean') body.audio = audio;
- if (typeof video === 'boolean') body.video = video;
- if (typeof data === 'boolean') body.data = data;
+
+ /* [multistream] */
+ if (streams && Array.isArray(streams)) {
+ body.streams = streams;
+ }
+ else {
+ body.feed = feed;
+ if (typeof audio === 'boolean') body.audio = audio;
+ if (typeof video === 'boolean') body.video = video;
+ if (typeof data === 'boolean') body.data = data;
+ if (typeof sc_substream_layer === 'number') body.substream = sc_substream_layer;
+ if (typeof sc_substream_fallback_ms === 'number') body.fallback = 1000 * sc_substream_fallback_ms;
+ if (typeof sc_temporal_layers === 'number') body.temporal = sc_temporal_layers;
+ }
if (typeof private_id === 'number') body.private_id = private_id;
if (typeof token === 'string') body.token = token;
- if (typeof sc_substream_layer === 'number') body.substream = sc_substream_layer;
- if (typeof sc_substream_fallback_ms === 'number') body.fallback = 1000 * sc_substream_fallback_ms;
- if (typeof sc_temporal_layers === 'number') body.temporal = sc_temporal_layers;
+
/* [multistream] */
if (typeof autoupdate === 'boolean') body.autoupdate = autoupdate;
+ if (typeof use_msid === 'boolean') body.use_msid = use_msid;
const response = await this.message(body);
const { event, data: evtdata } = response._janode || {};
@@ -864,7 +992,8 @@ class VideoRoomHandle extends Handle {
const body = {
request: REQUEST_START,
};
- jsep.e2ee = (typeof e2ee === 'boolean') ? e2ee : jsep.e2ee;
+ if (jsep)
+ jsep.e2ee = (typeof e2ee === 'boolean') ? e2ee : jsep.e2ee;
const response = await this.message(body, jsep);
const { event, data: evtdata } = response._janode || {};
@@ -896,20 +1025,28 @@ class VideoRoomHandle extends Handle {
* Switch to another feed.
*
* @param {object} params
- * @param {number|string} params.to_feed - The feed id of the new publisher to switch to
+ * @param {number|string} [params.to_feed] - The feed id of the new publisher to switch to
* @param {boolean} [params.audio] - True to subscribe to the audio feed
* @param {boolean} [params.video] - True to subscribe to the video feed
* @param {boolean} [params.data] - True to subscribe to the datachannels of the feed
+ * @param {object[]} [params.streams] - [multistream] streams array containing feed, mid, sub_mid ...
* @returns {Promise}
*/
- async switch({ to_feed, audio, video, data }) {
+ async switch({ to_feed, audio, video, data, streams }) {
const body = {
request: REQUEST_SWITCH,
- feed: to_feed,
};
- if (typeof audio === 'boolean') body.audio = audio;
- if (typeof video === 'boolean') body.video = video;
- if (typeof data === 'boolean') body.data = data;
+
+ /* [multistream] */
+ if (streams && Array.isArray(streams)) {
+ body.streams = streams;
+ }
+ else {
+ body.feed = to_feed;
+ if (typeof audio === 'boolean') body.audio = audio;
+ if (typeof video === 'boolean') body.video = video;
+ if (typeof data === 'boolean') body.data = data;
+ }
const response = await this.message(body);
const { event, data: evtdata } = response._janode || {};
@@ -951,8 +1088,8 @@ class VideoRoomHandle extends Handle {
const body = {
request: REQUEST_UPDATE,
};
- if (Array.isArray(subscribe)) body.subscribe = subscribe;
- if (Array.isArray(unsubscribe)) body.unsubscribe = unsubscribe;
+ if (subscribe && Array.isArray(subscribe)) body.subscribe = subscribe;
+ if (unsubscribe && Array.isArray(unsubscribe)) body.unsubscribe = unsubscribe;
const response = await this.message(body);
const { event, data: evtdata } = response._janode || {};
@@ -1215,6 +1352,7 @@ class VideoRoomHandle extends Handle {
* @param {number|string} params.room - The room where to start a forwarder
* @param {number|string} params.feed - The feed identifier to forward (must be published)
* @param {string} params.host - The target host for the forwarder
+ * @param {object[]} [params.streams] - [multistream] The streams array containing mid, port, rtcp_port, port_2 ...
* @param {number} [params.audio_port] - The target audio RTP port, if audio is to be forwarded
* @param {number} [params.audio_rtcp_port] - The target audio RTCP port, if audio is to be forwarded
* @param {number} [params.audio_ssrc] - The SSRC that will be used for audio RTP
@@ -1230,24 +1368,31 @@ class VideoRoomHandle extends Handle {
* @param {string} [params.admin_key] - The admin key needed for invoking the API
* @returns {Promise}
*/
- async startForward({ room, feed, host, audio_port, audio_rtcp_port, audio_ssrc, video_port, video_rtcp_port, video_ssrc, video_port_2, video_ssrc_2, video_port_3, video_ssrc_3, data_port, secret, admin_key }) {
+ async startForward({ room, feed, host, streams, audio_port, audio_rtcp_port, audio_ssrc, video_port, video_rtcp_port, video_ssrc, video_port_2, video_ssrc_2, video_port_3, video_ssrc_3, data_port, secret, admin_key }) {
const body = {
request: REQUEST_RTP_FWD_START,
room,
publisher_id: feed,
};
if (typeof host === 'string') body.host = host;
- if (typeof audio_port === 'number') body.audio_port = audio_port;
- if (typeof audio_rtcp_port === 'number') body.audio_rtcp_port = audio_rtcp_port;
- if (typeof audio_ssrc === 'number') body.audio_ssrc = audio_ssrc;
- if (typeof video_port === 'number') body.video_port = video_port;
- if (typeof video_rtcp_port === 'number') body.video_rtcp_port = video_rtcp_port;
- if (typeof video_ssrc === 'number') body.video_ssrc = video_ssrc;
- if (typeof video_port_2 === 'number') body.video_port_2 = video_port_2;
- if (typeof video_ssrc_2 === 'number') body.video_ssrc_2 = video_ssrc_2;
- if (typeof video_port_3 === 'number') body.video_port_3 = video_port_3;
- if (typeof video_ssrc_3 === 'number') body.video_ssrc_3 = video_ssrc_3;
- if (typeof data_port === 'number') body.data_port = data_port;
+ /* [multistream] */
+ if (streams && Array.isArray(streams)) {
+ body.streams = streams;
+ }
+ else {
+ if (typeof audio_port === 'number') body.audio_port = audio_port;
+ if (typeof audio_rtcp_port === 'number') body.audio_rtcp_port = audio_rtcp_port;
+ if (typeof audio_ssrc === 'number') body.audio_ssrc = audio_ssrc;
+ if (typeof video_port === 'number') body.video_port = video_port;
+ if (typeof video_rtcp_port === 'number') body.video_rtcp_port = video_rtcp_port;
+ if (typeof video_ssrc === 'number') body.video_ssrc = video_ssrc;
+ if (typeof video_port_2 === 'number') body.video_port_2 = video_port_2;
+ if (typeof video_ssrc_2 === 'number') body.video_ssrc_2 = video_ssrc_2;
+ if (typeof video_port_3 === 'number') body.video_port_3 = video_port_3;
+ if (typeof video_ssrc_3 === 'number') body.video_ssrc_3 = video_ssrc_3;
+ if (typeof data_port === 'number') body.data_port = data_port;
+ }
+
if (typeof secret === 'string') body.secret = secret;
if (typeof admin_key === 'string') body.admin_key = admin_key;
@@ -1347,8 +1492,8 @@ class VideoRoomHandle extends Handle {
*
* @typedef {object} VIDEOROOM_EVENT_SUB_JOINED
* @property {number|string} room - The involved room
- * @property {number|string} feed - The published feed identifier
- * @property {string} display - The published feed display name
+ * @property {number|string} [feed] - The published feed identifier
+ * @property {string} [display] - The published feed display name
* @property {object[]} [streams] - [multistream] Streams description as returned by Janus
*/
@@ -1416,7 +1561,8 @@ class VideoRoomHandle extends Handle {
*
* @typedef {object} VIDEOROOM_EVENT_RTP_FWD_STARTED
* @property {number|string} room - The involved room
- * @property {RtpForwarder} forwarder - The forwarder object
+ * @property {RtpForwarder} [forwarder] - The forwarder object
+ * @property {RtpForwarder[]} [forwarders] - [multistream] The array of forwarders
*/
/**
@@ -1472,7 +1618,7 @@ class VideoRoomHandle extends Handle {
*
* @typedef {object} VIDEOROOM_EVENT_STARTED
* @property {number|string} room - The involved room
- * @property {number|string} feed - The feed that started
+ * @property {number|string} [feed] - The feed that started
* @property {boolean} [e2ee] - True if started stream is e2ee
* @property {string} started - A string with the value returned by Janus
*/
@@ -1491,10 +1637,11 @@ class VideoRoomHandle extends Handle {
*
* @typedef {object} VIDEOROOM_EVENT_SWITCHED
* @property {number|string} room - The involved room
- * @property {number|string} from_feed - The feed that has been switched from
- * @property {number|string} to_feed - The feed that has been switched to
+ * @property {number|string} [from_feed] - The feed that has been switched from
+ * @property {number|string} [to_feed] - The feed that has been switched to
* @property {string} switched - A string with the value returned by Janus
- * @property {string} display - The display name of the new feed
+ * @property {string} [display] - The display name of the new feed
+ * @property {object[]} [streams] - [multistream] The updated streams array
*/
/**