From 19ab36850595b9ed8169e57ccf12fc3e73d2748a Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Thu, 13 Jun 2024 14:05:16 +0200 Subject: [PATCH] videoroom: enhanced multistream support (#40) --- examples/videoroom-ms/html/index.html | 29 + .../videoroom-ms/html/videoroom-ms-client.js | 952 ++++++++++++++++++ examples/videoroom-ms/package-lock.json | 930 +++++++++++++++++ examples/videoroom-ms/package.json | 30 + examples/videoroom-ms/src/config.template.js | 16 + examples/videoroom-ms/src/index.js | 638 ++++++++++++ src/handle.js | 14 +- src/plugins/videoroom-plugin.js | 405 +++++--- 8 files changed, 2883 insertions(+), 131 deletions(-) create mode 100644 examples/videoroom-ms/html/index.html create mode 100644 examples/videoroom-ms/html/videoroom-ms-client.js create mode 100644 examples/videoroom-ms/package-lock.json create mode 100644 examples/videoroom-ms/package.json create mode 100644 examples/videoroom-ms/src/config.template.js create mode 100644 examples/videoroom-ms/src/index.js 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 */ /**