From c61e12f3b406748414ea2af6c672dd96e758596b Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Wed, 20 Mar 2024 15:09:57 +0100 Subject: [PATCH 1/8] WIP --- examples/videoroom-ms/html/.eslintrc.json | 5 + examples/videoroom-ms/html/index.html | 29 + .../videoroom-ms/html/videoroom-ms-client.js | 715 ++++++++++++++++++ examples/videoroom-ms/package.json | 30 + examples/videoroom-ms/src/config.template.js | 16 + examples/videoroom-ms/src/index.js | 620 +++++++++++++++ src/plugins/videoroom-plugin.js | 178 +++-- 7 files changed, 1539 insertions(+), 54 deletions(-) create mode 100644 examples/videoroom-ms/html/.eslintrc.json 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.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/.eslintrc.json b/examples/videoroom-ms/html/.eslintrc.json new file mode 100644 index 0000000..5c3f02e --- /dev/null +++ b/examples/videoroom-ms/html/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "browser": true + } +} \ No newline at end of file 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..247d6bb --- /dev/null +++ b/examples/videoroom-ms/html/videoroom-ms-client.js @@ -0,0 +1,715 @@ +/* eslint-disable no-sparse-arrays */ +/* global io */ + +'use strict'; + +const RTCPeerConnection = (window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection).bind(window); + +const pcMap = new Map(); +let 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; + +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(({ id: feed, streams }) => { + streams.forEach(({ mid }) => { + newStreams.push({ + feed, + mid, + }); + }); + }); + + subscribe({ + streams: newStreams, + room, + }); +} + +function trickle({ feed, candidate }) { + const trickleData = candidate ? { candidate } : {}; + trickleData.feed = feed; + const trickleEvent = candidate ? 'trickle' : 'trickle-complete'; + + socket.emit(trickleEvent, { + data: trickleData, + _id: getId(), + }); +} + +function configure({ feed, jsep, restart }) { + const configureData = { + feed, + audio: true, + video: true, + data: true, + }; + if (jsep) configureData.jsep = jsep; + if (typeof restart === 'boolean') configureData.restart = restart; + + const configId = getId(); + + socket.emit('configure', { + data: configureData, + _id: configId, + }); + + if (jsep) pendingOfferMap.set(configId, { feed }); +} + +function _unpublish({ feed }) { + const unpublishData = { + feed, + }; + + socket.emit('unpublish', { + data: unpublishData, + _id: getId(), + }); +} + +function _leave({ feed }) { + 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({ feed }) { + const pauseData = { + feed, + }; + + socket.emit('start', { + data: pauseData, + _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, room = myRoom, host = 'localhost', audio_port, video_port, data_port = null, secret = 'adminpwd' }) { + socket.emit('rtp-fwd-start', { + data: { + room, + feed, + host, + audio_port, + video_port, + data_port, + 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(); + removeAllVideoElements(); + 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)) { + const { feed } = pendingOfferMap.get(_id); + removeVideoElementByFeed(feed); + closePC(feed); + pendingOfferMap.delete(_id); + return; + } +}); + +socket.on('joined', async ({ data }) => { + console.log('joined to room', data); + setLocalVideoElement(null, null, null, data.room); + + try { + const offer = await doOffer(data.feed, data.display); + configure({ feed: data.feed, jsep: offer }); + subscribeTo(data.publishers, data.room); + } catch (e) { + console.log('error while doing offer', e); + } +}); + +socket.on('subscribed', async ({ data }) => { + console.log('subscribed to feed', data); + + try { + const answer = await doAnswer(null, data.streams, 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) { + removeVideoElementByFeed(data.feed); + closePC(data.feed); + } +}); + +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 = pcMap.get(data.feed); + if (pc && data.jsep) { + try { + await pc.setRemoteDescription(data.jsep); + console.log('configure remote sdp OK'); + if (data.jsep.type === 'offer') { + const answer = await doAnswer(null, data.streams, data.jsep); + start({ jsep: answer }); + } + } catch (e) { + console.log('error setting remote sdp', e); + } + } +}); + +socket.on('display', ({ data }) => { + console.log('feed changed display name', data); + setRemoteVideoElement(null, data.feed, data.display); +}); + +socket.on('started', ({ data }) => { + console.log('subscribed feed started', data); +}); + +socket.on('paused', ({ data }) => { + console.log('feed paused', data); +}); + +socket.on('switched', ({ data }) => { + console.log(`feed switched from ${data.from_feed} to ${data.to_feed} (${data.display})`); + /* !!! This will actually break the DOM management since IDs are feed based !!! */ + setRemoteVideoElement(null, data.from_feed, data.display); +}); + +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) { + removeVideoElementByFeed(data.feed); + closePC(data.feed); + } +}); + +socket.on('leaving', ({ data }) => { + console.log('feed leaving', data); + if (data.feed) { + removeVideoElementByFeed(data.feed); + closePC(data.feed); + } +}); + +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) { + const offer = await doOffer(feed, null); + configure({ feed, jsep: offer }); +} + +async function _restartSubscriber() { + configure({ restart: true }); +} + +async function doOffer(feed, display) { + if (!pcMap.has(feed)) { + 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') { + removeVideoElementByFeed(feed); + closePC(feed); + } + }; + /* This one below should not be fired, cause the PC is used just to send */ + pc.ontrack = event => console.log('pc.ontrack', event); + + pcMap.set(feed, 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); + }); + setLocalVideoElement(localStream, feed, display); + } catch (e) { + console.log('error while doing offer', e); + removeVideoElementByFeed(feed); + closePC(feed); + return; + } + } + else { + console.log('Performing ICE restart'); + pcMap.get(feed).restartIce(); + } + + try { + const pc = pcMap.get(feed); + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + console.log('set local sdp OK'); + return offer; + } catch (e) { + console.log('error while doing offer', e); + removeVideoElementByFeed(feed); + closePC(feed); + return; + } + +} + +async function doAnswer(feed, streams, offer) { + if (!pcMap.has(feed)) { + 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') { + removeVideoElementByFeed(feed); + closePC(feed); + } + }; + pc.ontrack = event => { + console.log('pc.ontrack', event); + + event.track.onunmute = evt => { + console.log('track.onunmute', evt); + /* TODO set srcObject in this callback */ + }; + event.track.onmute = evt => { + console.log('track.onmute', evt); + }; + event.track.onended = evt => { + console.log('track.onended', evt); + }; + + const remoteStream = event.streams[0]; + setRemoteVideoElement(remoteStream, feed, display); + }; + + pcMap.set(feed, pc); + } + + const pc = pcMap.get(feed); + + try { + await pc.setRemoteDescription(offer); + console.log('set remote sdp OK'); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + console.log('set local sdp OK'); + return answer; + } catch (e) { + console.log('error creating subscriber answer', e); + removeVideoElementByFeed(feed); + closePC(feed); + throw e; + } +} + +function setLocalVideoElement(localStream, feed, display, room) { + if (room) document.getElementById('videos').getElementsByTagName('span')[0].innerHTML = ' --- VIDEOROOM (' + room + ') --- '; + if (!feed) return; + + if (!document.getElementById('video_' + feed)) { + const nameElem = document.createElement('span'); + nameElem.innerHTML = display + ' (' + feed + ')'; + nameElem.style.display = 'table'; + + if (localStream) { + const localVideoStreamElem = document.createElement('video'); + //localVideo.id = 'video_'+feed; + localVideoStreamElem.width = 320; + localVideoStreamElem.height = 240; + localVideoStreamElem.autoplay = true; + localVideoStreamElem.muted = 'muted'; + localVideoStreamElem.style.cssText = '-moz-transform: scale(-1, 1); -webkit-transform: scale(-1, 1); -o-transform: scale(-1, 1); transform: scale(-1, 1); filter: FlipH;'; + localVideoStreamElem.srcObject = localStream; + + const localVideoContainer = document.createElement('div'); + localVideoContainer.id = 'video_' + feed; + localVideoContainer.appendChild(nameElem); + localVideoContainer.appendChild(localVideoStreamElem); + + document.getElementById('locals').appendChild(localVideoContainer); + } + } + else { + const localVideoContainer = document.getElementById('video_' + feed); + if (display) { + const nameElem = localVideoContainer.getElementsByTagName('span')[0]; + nameElem.innerHTML = display + ' (' + feed + ')'; + } + const localVideoStreamElem = localVideoContainer.getElementsByTagName('video')[0]; + if (localStream) + localVideoStreamElem.srcObject = localStream; + } +} + +function setRemoteVideoElement(remoteStream, feed, display) { + if (!feed) return; + + if (!document.getElementById('video_' + feed)) { + const nameElem = document.createElement('span'); + nameElem.innerHTML = display + ' (' + feed + ')'; + 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;'; + if (remoteStream) + remoteVideoStreamElem.srcObject = remoteStream; + + const remoteVideoContainer = document.createElement('div'); + remoteVideoContainer.id = 'video_' + feed; + remoteVideoContainer.appendChild(nameElem); + remoteVideoContainer.appendChild(remoteVideoStreamElem); + + document.getElementById('remotes').appendChild(remoteVideoContainer); + } + else { + const remoteVideoContainer = document.getElementById('video_' + feed); + if (display) { + const nameElem = remoteVideoContainer.getElementsByTagName('span')[0]; + nameElem.innerHTML = display + ' (' + feed + ')'; + } + if (remoteStream) { + const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0]; + remoteVideoStreamElem.srcObject = remoteStream; + } + } +} + +function removeVideoElementByFeed(feed, stopTracks = true) { + const videoContainer = document.getElementById(`video_${feed}`); + if (videoContainer) removeVideoElement(videoContainer, stopTracks); +} + +function removeVideoElement(container, stopTracks = true) { + let videoStreamElem = container.getElementsByTagName('video').length > 0 ? container.getElementsByTagName('video')[0] : null; + if (videoStreamElem && videoStreamElem.srcObject && stopTracks) { + videoStreamElem.srcObject.getTracks().forEach(track => track.stop()); + videoStreamElem.srcObject = null; + } + container.remove(); +} + +function removeAllVideoElements() { + const locals = document.getElementById('locals'); + const localVideoContainers = locals.getElementsByTagName('div'); + for (let i = 0; localVideoContainers && i < localVideoContainers.length; i++) + removeVideoElement(localVideoContainers[i]); + while (locals.firstChild) + locals.removeChild(locals.firstChild); + + var remotes = document.getElementById('remotes'); + const remoteVideoContainers = remotes.getElementsByTagName('div'); + for (let i = 0; remoteVideoContainers && i < remoteVideoContainers.length; i++) + removeVideoElement(remoteVideoContainers[i]); + while (remotes.firstChild) + remotes.removeChild(remotes.firstChild); + document.getElementById('videos').getElementsByTagName('span')[0].innerHTML = ' --- VIDEOROOM () --- '; +} + +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; + pc.close(); +} + +function closePC(feed) { + if (!feed) return; + let pc = pcMap.get(feed); + console.log('closing pc for feed', feed); + _closePC(pc); + pcMap.delete(feed); +} + +function closeAllPCs() { + console.log('closing all pcs'); + + pcMap.forEach((pc, feed) => { + console.log('closing pc for feed', feed); + _closePC(pc); + }); + + pcMap.clear(); +} 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..5f7711f --- /dev/null +++ b/examples/videoroom-ms/src/index.js @@ -0,0 +1,620 @@ +'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 => { + if (pubHandle.feed === evtdata.feed) { + await msHandles.detachPubHandle(); + } + 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_SC_SUBSTREAM_LAYER, evtdata => Logger.info(`${LOG_NS} ${subHandle.name} simulcast substream layer switched to ${evtdata.sc_substream_layer}`)); + subHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_SC_TEMPORAL_LAYERS, evtdata => Logger.info(`${LOG_NS} ${subHandle.name} simulcast temporal layers switched to ${evtdata.sc_temporal_layers}`)); + 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 }) { + if (subHandle) subHandle.detach().catch(() => { }); + replyError(socket, message, subscribedata, _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/plugins/videoroom-plugin.js b/src/plugins/videoroom-plugin.js index 805bf23..e419b57 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; @@ -424,7 +438,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 +454,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 +469,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 +505,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 +563,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 +583,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 +612,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 +634,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 +699,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,7 +768,7 @@ 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.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 @@ -741,17 +782,28 @@ 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; + + /* [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 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,25 +865,34 @@ 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 {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, 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; @@ -896,20 +957,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 +1020,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 || {}; @@ -1347,8 +1416,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 */ @@ -1472,7 +1541,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 +1560,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 */ /** From 3d8cf3dd0307ce62df93ca93b5ecfa17c8021220 Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Mon, 25 Mar 2024 17:23:09 +0100 Subject: [PATCH 2/8] Working example --- .../videoroom-ms/html/videoroom-ms-client.js | 422 +++++--- examples/videoroom-ms/package-lock.json | 930 ++++++++++++++++++ examples/videoroom-ms/src/index.js | 6 +- src/plugins/videoroom-plugin.js | 3 +- 4 files changed, 1203 insertions(+), 158 deletions(-) create mode 100644 examples/videoroom-ms/package-lock.json diff --git a/examples/videoroom-ms/html/videoroom-ms-client.js b/examples/videoroom-ms/html/videoroom-ms-client.js index 247d6bb..8ea26f1 100644 --- a/examples/videoroom-ms/html/videoroom-ms-client.js +++ b/examples/videoroom-ms/html/videoroom-ms-client.js @@ -5,8 +5,9 @@ const RTCPeerConnection = (window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection).bind(window); -const pcMap = new Map(); -let pendingOfferMap = new Map(); +let pubPc, subPc; +const 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; @@ -75,24 +76,34 @@ function subscribe({ streams, room = myRoom }) { function subscribeTo(publishers, room = myRoom) { const newStreams = []; - publishers.forEach(({ id: feed, streams }) => { - streams.forEach(({ mid }) => { - newStreams.push({ - feed, - mid, - }); + publishers.forEach(({ feed, streams }) => { + streams.forEach(s => { + const mid = s.mid; + if (!subscriptions.has(feed)) { + subscriptions.set(feed, new Map()); + } + const feedSubs = subscriptions.get(feed); + if (!feedSubs.has(mid)) { + feedSubs.set(mid, s); + newStreams.push({ + feed, + mid, + }); + } }); }); - subscribe({ - streams: newStreams, - room, - }); + if (newStreams.length > 0) { + subscribe({ + streams: newStreams, + room, + }); + } } function trickle({ feed, candidate }) { const trickleData = candidate ? { candidate } : {}; - trickleData.feed = feed; + if (feed) trickleData.feed = feed; const trickleEvent = candidate ? 'trickle' : 'trickle-complete'; socket.emit(trickleEvent, { @@ -101,14 +112,12 @@ function trickle({ feed, candidate }) { }); } -function configure({ feed, jsep, restart }) { - const configureData = { - feed, - audio: true, - video: true, - data: true, - }; +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(); @@ -118,10 +127,11 @@ function configure({ feed, jsep, restart }) { _id: configId, }); - if (jsep) pendingOfferMap.set(configId, { feed }); + if (jsep) + pendingOfferMap.set(configId, { feed }); } -function _unpublish({ feed }) { +function _unpublish({ feed = pubPc._feed }) { const unpublishData = { feed, }; @@ -132,7 +142,7 @@ function _unpublish({ feed }) { }); } -function _leave({ feed }) { +function _leave({ feed = pubPc._feed }) { const leaveData = { feed, }; @@ -167,7 +177,7 @@ function _kick({ feed, room = myRoom, secret = 'adminpwd' }) { }); } -function start({ jsep = null }) { +function start({ jsep = null } = {}) { const startData = { jsep, }; @@ -178,12 +188,10 @@ function start({ jsep = null }) { }); } -function _pause({ feed }) { - const pauseData = { - feed, - }; +function _pause() { + const pauseData = {}; - socket.emit('start', { + socket.emit('pause', { data: pauseData, _id: getId(), }); @@ -304,7 +312,8 @@ socket.on('connect', () => { socket.on('disconnect', () => { console.log('socket disconnected'); pendingOfferMap.clear(); - removeAllVideoElements(); + subscriptions.clear(); + removeAllMediaElements(); closeAllPCs(); }); @@ -315,9 +324,8 @@ socket.on('videoroom-error', ({ error, _id }) => { return; } if (pendingOfferMap.has(_id)) { - const { feed } = pendingOfferMap.get(_id); - removeVideoElementByFeed(feed); - closePC(feed); + removeLocalMediaElements(); + closePubPc(); pendingOfferMap.delete(_id); return; } @@ -325,7 +333,7 @@ socket.on('videoroom-error', ({ error, _id }) => { socket.on('joined', async ({ data }) => { console.log('joined to room', data); - setLocalVideoElement(null, null, null, data.room); + setLocalMediaElement(null, null, null, data.room); try { const offer = await doOffer(data.feed, data.display); @@ -340,8 +348,21 @@ socket.on('subscribed', async ({ data }) => { console.log('subscribed to feed', data); try { - const answer = await doAnswer(null, data.streams, data.jsep); - start({ jsep: answer }); + if (data.jsep) { + const answer = await doAnswer(data.streams, data.jsep); + start({ jsep: answer }); + } + } catch (e) { console.log('error while doing answer', e); } +}); + +socket.on('updated', async ({ data }) => { + console.log('updated subscription', data); + + try { + if (data.jsep) { + const answer = await doAnswer(data.streams, data.jsep); + start({ jsep: answer }); + } } catch (e) { console.log('error while doing answer', e); } }); @@ -356,8 +377,12 @@ socket.on('talking', ({ data }) => { socket.on('kicked', ({ data }) => { console.log('participant kicked', data); if (data.feed) { - removeVideoElementByFeed(data.feed); - closePC(data.feed); + removeMediaElementsByFeed(data.feed, false); + subscriptions.delete(data.feed); + if (data.feed === pubPc?._feed) { + closePubPc(); + subscriptions.clear(); + } } }); @@ -368,28 +393,34 @@ socket.on('allowed', ({ data }) => { socket.on('configured', async ({ data, _id }) => { console.log('feed configured', data); pendingOfferMap.delete(_id); - const pc = pcMap.get(data.feed); + const pc = data.feed ? pubPc : subPc; if (pc && data.jsep) { try { await pc.setRemoteDescription(data.jsep); console.log('configure remote sdp OK'); if (data.jsep.type === 'offer') { - const answer = await doAnswer(null, data.streams, data.jsep); + const answer = await doAnswer(data.streams, data.jsep); start({ jsep: answer }); } } catch (e) { console.log('error setting remote sdp', e); } + return; + } + if (data.display) { + setLocalMediaElement(null, data.feed, data.display); + return; } }); socket.on('display', ({ data }) => { console.log('feed changed display name', data); - setRemoteVideoElement(null, data.feed, data.display); + setRemoteVideoElement(null, data.feed, null, data.display); + }); socket.on('started', ({ data }) => { - console.log('subscribed feed started', data); + console.log('subscriber feed started', data); }); socket.on('paused', ({ data }) => { @@ -397,9 +428,8 @@ socket.on('paused', ({ data }) => { }); socket.on('switched', ({ data }) => { - console.log(`feed switched from ${data.from_feed} to ${data.to_feed} (${data.display})`); - /* !!! This will actually break the DOM management since IDs are feed based !!! */ - setRemoteVideoElement(null, data.from_feed, data.display); + console.log('feed switched', data); + //TODO }); socket.on('feed-list', ({ data }) => { @@ -410,16 +440,24 @@ socket.on('feed-list', ({ data }) => { socket.on('unpublished', ({ data }) => { console.log('feed unpublished', data); if (data.feed) { - removeVideoElementByFeed(data.feed); - closePC(data.feed); + removeMediaElementsByFeed(data.feed, false); + subscriptions.delete(data.feed); + if (data.feed === pubPc?._feed) { + closePubPc(); + subscriptions.clear(); + } } }); socket.on('leaving', ({ data }) => { console.log('feed leaving', data); if (data.feed) { - removeVideoElementByFeed(data.feed); - closePC(data.feed); + removeMediaElementsByFeed(data.feed, false); + subscriptions.delete(data.feed); + if (data.feed === pubPc?._feed) { + closePubPc(); + subscriptions.clear(); + } } }); @@ -464,7 +502,7 @@ async function _restartSubscriber() { } async function doOffer(feed, display) { - if (!pcMap.has(feed)) { + if (!pubPc) { const pc = new RTCPeerConnection({ 'iceServers': [{ urls: 'stun:stun.l.google.com:19302' @@ -475,14 +513,14 @@ async function doOffer(feed, display) { pc.onicecandidate = event => trickle({ feed, candidate: event.candidate }); pc.oniceconnectionstatechange = () => { if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') { - removeVideoElementByFeed(feed); - closePC(feed); + removeLocalMediaElements(); + closePubPc(); } }; /* This one below should not be fired, cause the PC is used just to send */ pc.ontrack = event => console.log('pc.ontrack', event); - pcMap.set(feed, pc); + pubPc = pc; try { const localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); @@ -490,48 +528,50 @@ async function doOffer(feed, display) { console.log('adding track', track); pc.addTrack(track, localStream); }); - setLocalVideoElement(localStream, feed, display); + setLocalMediaElement(localStream, feed, display, null); } catch (e) { console.log('error while doing offer', e); - removeVideoElementByFeed(feed); - closePC(feed); + removeLocalMediaElements(); + closePubPc(); return; } } else { console.log('Performing ICE restart'); - pcMap.get(feed).restartIce(); + pubPc.restartIce(); } + pubPc._feed = feed; try { - const pc = pcMap.get(feed); - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); + 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); - removeVideoElementByFeed(feed); - closePC(feed); + removeLocalMediaElements(); + closePubPc(); return; } } -async function doAnswer(feed, streams, offer) { - if (!pcMap.has(feed)) { +async function doAnswer(streams, 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({ feed, candidate: event.candidate }); + pc.onicecandidate = event => trickle({ candidate: event.candidate }); pc.oniceconnectionstatechange = () => { if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') { - removeVideoElementByFeed(feed); - closePC(feed); + removeRemoteMediaElements(); + closeSubPc(); } }; pc.ontrack = event => { @@ -539,7 +579,6 @@ async function doAnswer(feed, streams, offer) { event.track.onunmute = evt => { console.log('track.onunmute', evt); - /* TODO set srcObject in this callback */ }; event.track.onmute = evt => { console.log('track.onmute', evt); @@ -548,75 +587,98 @@ async function doAnswer(feed, streams, offer) { console.log('track.onended', evt); }; - const remoteStream = event.streams[0]; - setRemoteVideoElement(remoteStream, feed, display); + const submid = event.transceiver?.mid || event.receiver.mid; + const stream = subPc._streams.filter(({ mid }) => mid === submid)[0]; + const feed = stream.feed_id; + const display = stream.feed_display; + const type = stream.type; + /* avoid latching tracks */ + const remoteStream = event.streams[0].id === 'janus' ? (new MediaStream([event.track])) : event.streams[0]; + if (type === 'video') + setRemoteVideoElement(remoteStream, feed, submid, display); + if (type === 'audio') + setRemoteAudioElement(remoteStream, feed, submid); }; - - pcMap.set(feed, pc); } - - const pc = pcMap.get(feed); + subPc._streams = streams; try { - await pc.setRemoteDescription(offer); + await subPc.setRemoteDescription(offer); console.log('set remote sdp OK'); - const answer = await pc.createAnswer(); - await pc.setLocalDescription(answer); + 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); - removeVideoElementByFeed(feed); - closePC(feed); + removeRemoteMediaElements(); + closeSubPc(); throw e; } } -function setLocalVideoElement(localStream, feed, display, room) { +function setLocalMediaElement(localStream, feed, display, room) { if (room) document.getElementById('videos').getElementsByTagName('span')[0].innerHTML = ' --- VIDEOROOM (' + room + ') --- '; if (!feed) return; - if (!document.getElementById('video_' + feed)) { + const id = `video_${feed}_local`; + let localVideoContainer = document.getElementById(id); + if (!localVideoContainer) { const nameElem = document.createElement('span'); - nameElem.innerHTML = display + ' (' + feed + ')'; nameElem.style.display = 'table'; - if (localStream) { - const localVideoStreamElem = document.createElement('video'); - //localVideo.id = 'video_'+feed; - localVideoStreamElem.width = 320; - localVideoStreamElem.height = 240; - localVideoStreamElem.autoplay = true; - localVideoStreamElem.muted = 'muted'; - localVideoStreamElem.style.cssText = '-moz-transform: scale(-1, 1); -webkit-transform: scale(-1, 1); -o-transform: scale(-1, 1); transform: scale(-1, 1); filter: FlipH;'; - localVideoStreamElem.srcObject = localStream; - - const localVideoContainer = document.createElement('div'); - localVideoContainer.id = 'video_' + feed; - localVideoContainer.appendChild(nameElem); - localVideoContainer.appendChild(localVideoStreamElem); - - document.getElementById('locals').appendChild(localVideoContainer); - } + 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); } - else { - const localVideoContainer = document.getElementById('video_' + feed); - if (display) { - const nameElem = localVideoContainer.getElementsByTagName('span')[0]; - nameElem.innerHTML = display + ' (' + feed + ')'; - } + if (display) { + const nameElem = localVideoContainer.getElementsByTagName('span')[0]; + nameElem.innerHTML = `${display}|${feed}`; + } + if (localStream) { const localVideoStreamElem = localVideoContainer.getElementsByTagName('video')[0]; - if (localStream) - localVideoStreamElem.srcObject = localStream; + localVideoStreamElem.srcObject = localStream; } } -function setRemoteVideoElement(remoteStream, feed, display) { +function setRemoteVideoElement(remoteStream, feed, mid, display) { if (!feed) return; - if (!document.getElementById('video_' + feed)) { + /* Target all streams related to feed */ + if (!remoteStream && !mid && display) { + const videoIdStartsWith = `video_${feed}`; + const videoContainers = document.querySelectorAll(`[id^=${videoIdStartsWith}]`); + videoContainers.forEach(container => { + if (remoteStream) { + const remoteVideoStreamElem = container.getElementsByTagName('video')[0]; + remoteVideoStreamElem.srcObject = remoteStream; + } + if (display) { + const nameElem = container.getElementsByTagName('span')[0]; + mid = nameElem.innerHTML.split('|')[2]; + nameElem.innerHTML = `${display}|${feed}|${mid}`; + } + }); + return; + } + + /* Target specific feed/mid */ + const id = `video_${feed}_${mid}_remote`; + let remoteVideoContainer = document.getElementById(id); + if (!remoteVideoContainer) { + /* Non existing */ const nameElem = document.createElement('span'); - nameElem.innerHTML = display + ' (' + feed + ')'; + nameElem.innerHTML = `${display}|${feed}|${mid}`; nameElem.style.display = 'table'; const remoteVideoStreamElem = document.createElement('video'); @@ -624,60 +686,121 @@ function setRemoteVideoElement(remoteStream, feed, display) { 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;'; - if (remoteStream) - remoteVideoStreamElem.srcObject = remoteStream; - const remoteVideoContainer = document.createElement('div'); - remoteVideoContainer.id = 'video_' + feed; + remoteVideoContainer = document.createElement('div'); + remoteVideoContainer.id = id; remoteVideoContainer.appendChild(nameElem); remoteVideoContainer.appendChild(remoteVideoStreamElem); document.getElementById('remotes').appendChild(remoteVideoContainer); } - else { - const remoteVideoContainer = document.getElementById('video_' + feed); - if (display) { - const nameElem = remoteVideoContainer.getElementsByTagName('span')[0]; - nameElem.innerHTML = display + ' (' + feed + ')'; - } - if (remoteStream) { - const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0]; - remoteVideoStreamElem.srcObject = remoteStream; - } + if (display) { + const nameElem = remoteVideoContainer.getElementsByTagName('span')[0]; + nameElem.innerHTML = `${display}|${feed}|${mid}`; + } + if (remoteStream) { + const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0]; + remoteVideoStreamElem.srcObject = remoteStream; } } -function removeVideoElementByFeed(feed, stopTracks = true) { - const videoContainer = document.getElementById(`video_${feed}`); - if (videoContainer) removeVideoElement(videoContainer, stopTracks); +function setRemoteAudioElement(remoteStream, feed, mid) { + if (!feed) return; + + /* Target all streams related to feed */ + if (!remoteStream && !mid) { + const audioIdStartsWith = `audio_${feed}`; + const audioContainers = document.querySelectorAll(`[id^=${audioIdStartsWith}]`); + audioContainers.forEach(container => { + if (remoteStream) { + const remoteAudioStreamElem = container.getElementsByTagName('audio')[0]; + remoteAudioStreamElem.srcObject = remoteStream; + } + }); + return; + } + + const id = `audio_${feed}_${mid}_remote`; + 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 removeVideoElement(container, stopTracks = true) { - let videoStreamElem = container.getElementsByTagName('video').length > 0 ? container.getElementsByTagName('video')[0] : null; - if (videoStreamElem && videoStreamElem.srcObject && stopTracks) { - videoStreamElem.srcObject.getTracks().forEach(track => track.stop()); - videoStreamElem.srcObject = null; +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 removeAllVideoElements() { +function removeMediaElementsByFeed(feed, stopTracks) { + const audioIdStartsWith = `audio_${feed}`; + const audioContainers = document.querySelectorAll(`[id^=${audioIdStartsWith}]`); + audioContainers.forEach(container => removeMediaElement(container, stopTracks)); + + const videoIdStartsWith = `video_${feed}`; + const videoContainers = document.querySelectorAll(`[id^=${videoIdStartsWith}]`); + videoContainers.forEach(container => removeMediaElement(container, stopTracks)); +} + +function removeLocalMediaElements() { const locals = document.getElementById('locals'); - const localVideoContainers = locals.getElementsByTagName('div'); - for (let i = 0; localVideoContainers && i < localVideoContainers.length; i++) - removeVideoElement(localVideoContainers[i]); + 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 removeRemoteMediaElements() { var remotes = document.getElementById('remotes'); - const remoteVideoContainers = remotes.getElementsByTagName('div'); - for (let i = 0; remoteVideoContainers && i < remoteVideoContainers.length; i++) - removeVideoElement(remoteVideoContainers[i]); + 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() { + removeLocalMediaElements(); + removeRemoteMediaElements(); 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 => { @@ -692,24 +815,13 @@ function _closePC(pc) { pc.onicecandidate = null; pc.oniceconnectionstatechange = null; pc.ontrack = null; - pc.close(); -} - -function closePC(feed) { - if (!feed) return; - let pc = pcMap.get(feed); - console.log('closing pc for feed', feed); - _closePC(pc); - pcMap.delete(feed); + try { + pc.close(); + } catch (e) { } } function closeAllPCs() { console.log('closing all pcs'); - - pcMap.forEach((pc, feed) => { - console.log('closing pc for feed', feed); - _closePC(pc); - }); - - pcMap.clear(); + closePubPc(); + closeSubPc(); } diff --git a/examples/videoroom-ms/package-lock.json b/examples/videoroom-ms/package-lock.json new file mode 100644 index 0000000..b8548a4 --- /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.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "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.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "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.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz", + "integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==", + "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.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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/src/index.js b/examples/videoroom-ms/src/index.js index 5f7711f..05b71a4 100644 --- a/examples/videoroom-ms/src/index.js +++ b/examples/videoroom-ms/src/index.js @@ -259,8 +259,10 @@ function initFrontEnd() { 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_SC_SUBSTREAM_LAYER, evtdata => Logger.info(`${LOG_NS} ${subHandle.name} simulcast substream layer switched to ${evtdata.sc_substream_layer}`)); - subHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_SC_TEMPORAL_LAYERS, evtdata => Logger.info(`${LOG_NS} ${subHandle.name} simulcast temporal layers switched to ${evtdata.sc_temporal_layers}`)); + 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 { diff --git a/src/plugins/videoroom-plugin.js b/src/plugins/videoroom-plugin.js index e419b57..8ca1405 100644 --- a/src/plugins/videoroom-plugin.js +++ b/src/plugins/videoroom-plugin.js @@ -925,7 +925,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 || {}; From 37e72361f01c0f5cb1d9e31df94c3b6a866c0652 Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Wed, 27 Mar 2024 10:53:21 +0100 Subject: [PATCH 3/8] Implement unsubscribe and switch. Refactor client to index subscriptions by sub mid. --- .../videoroom-ms/html/videoroom-ms-client.js | 337 ++++++++++++------ examples/videoroom-ms/src/index.js | 24 +- src/plugins/videoroom-plugin.js | 19 +- 3 files changed, 256 insertions(+), 124 deletions(-) diff --git a/examples/videoroom-ms/html/videoroom-ms-client.js b/examples/videoroom-ms/html/videoroom-ms-client.js index 8ea26f1..4cd8509 100644 --- a/examples/videoroom-ms/html/videoroom-ms-client.js +++ b/examples/videoroom-ms/html/videoroom-ms-client.js @@ -6,11 +6,12 @@ const RTCPeerConnection = (window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection).bind(window); let pubPc, subPc; -const subscriptions = new Map(); +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 = () => { @@ -78,16 +79,10 @@ function subscribeTo(publishers, room = myRoom) { const newStreams = []; publishers.forEach(({ feed, streams }) => { streams.forEach(s => { - const mid = s.mid; - if (!subscriptions.has(feed)) { - subscriptions.set(feed, new Map()); - } - const feedSubs = subscriptions.get(feed); - if (!feedSubs.has(mid)) { - feedSubs.set(mid, s); + if (!hasFeedMidSubscription(feed, s.mid)) { newStreams.push({ feed, - mid, + mid: s.mid, }); } }); @@ -131,7 +126,16 @@ function configure({ feed, display, jsep, restart, streams }) { pendingOfferMap.set(configId, { feed }); } -function _unpublish({ feed = pubPc._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, }; @@ -142,7 +146,7 @@ function _unpublish({ feed = pubPc._feed }) { }); } -function _leave({ feed = pubPc._feed }) { +function _leave({ feed = myFeed } = {}) { const leaveData = { feed, }; @@ -197,6 +201,17 @@ function _pause() { }); } +function _unsubscribe({ streams, room = myRoom }) { + const unsubscribeData = { + room, + streams, + }; + + socket.emit('unsubscribe', { + data: unsubscribeData, + _id: getId(), + }); +} function _switch({ streams }) { const switchData = { @@ -324,7 +339,7 @@ socket.on('videoroom-error', ({ error, _id }) => { return; } if (pendingOfferMap.has(_id)) { - removeLocalMediaElements(); + removeAllLocalMediaElements(); closePubPc(); pendingOfferMap.delete(_id); return; @@ -336,20 +351,86 @@ socket.on('joined', async ({ data }) => { setLocalMediaElement(null, null, null, data.room); try { - const offer = await doOffer(data.feed, data.display); - configure({ feed: data.feed, jsep: offer }); + await _publish({ feed: data.feed, display: data.display}); subscribeTo(data.publishers, data.room); } catch (e) { - console.log('error while doing offer', 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.streams, data.jsep); + const answer = await doAnswer(data.jsep); start({ jsep: answer }); } } catch (e) { console.log('error while doing answer', e); } @@ -357,10 +438,32 @@ socket.on('subscribed', async ({ data }) => { 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.streams, data.jsep); + const answer = await doAnswer(data.jsep); start({ jsep: answer }); } } catch (e) { console.log('error while doing answer', e); } @@ -377,12 +480,7 @@ socket.on('talking', ({ data }) => { socket.on('kicked', ({ data }) => { console.log('participant kicked', data); if (data.feed) { - removeMediaElementsByFeed(data.feed, false); - subscriptions.delete(data.feed); - if (data.feed === pubPc?._feed) { - closePubPc(); - subscriptions.clear(); - } + deleteSubscriptionByFeed(data.feed); } }); @@ -393,30 +491,33 @@ socket.on('allowed', ({ data }) => { socket.on('configured', async ({ data, _id }) => { console.log('feed configured', data); pendingOfferMap.delete(_id); + const pc = data.feed ? pubPc : subPc; - if (pc && data.jsep) { + if (data.jsep) { try { await pc.setRemoteDescription(data.jsep); - console.log('configure remote sdp OK'); if (data.jsep.type === 'offer') { - const answer = await doAnswer(data.streams, data.jsep); + const answer = await doAnswer(data.jsep); start({ jsep: answer }); } + console.log('configure remote sdp OK'); } catch (e) { console.log('error setting remote sdp', e); } - return; } if (data.display) { setLocalMediaElement(null, data.feed, data.display); - return; } }); socket.on('display', ({ data }) => { console.log('feed changed display name', data); - setRemoteVideoElement(null, data.feed, null, data.display); - + for (let [_, stream] of subscriptions) { + if (stream.feed === data.feed) { + stream.display = data.display; + } + } + refreshRemoteMediaElements(); }); socket.on('started', ({ data }) => { @@ -429,7 +530,7 @@ socket.on('paused', ({ data }) => { socket.on('switched', ({ data }) => { console.log('feed switched', data); - //TODO + updateSubscriptions(data.streams); }); socket.on('feed-list', ({ data }) => { @@ -440,11 +541,9 @@ socket.on('feed-list', ({ data }) => { socket.on('unpublished', ({ data }) => { console.log('feed unpublished', data); if (data.feed) { - removeMediaElementsByFeed(data.feed, false); - subscriptions.delete(data.feed); - if (data.feed === pubPc?._feed) { + if (data.feed === myFeed) { + removeAllLocalMediaElements(); closePubPc(); - subscriptions.clear(); } } }); @@ -452,11 +551,12 @@ socket.on('unpublished', ({ data }) => { socket.on('leaving', ({ data }) => { console.log('feed leaving', data); if (data.feed) { - removeMediaElementsByFeed(data.feed, false); - subscriptions.delete(data.feed); - if (data.feed === pubPc?._feed) { + if (data.feed === myFeed) { + removeAllLocalMediaElements(); closePubPc(); - subscriptions.clear(); + } + else { + deleteSubscriptionByFeed(data.feed); } } }); @@ -513,7 +613,7 @@ async function doOffer(feed, display) { pc.onicecandidate = event => trickle({ feed, candidate: event.candidate }); pc.oniceconnectionstatechange = () => { if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') { - removeLocalMediaElements(); + removeAllLocalMediaElements(); closePubPc(); } }; @@ -531,7 +631,7 @@ async function doOffer(feed, display) { setLocalMediaElement(localStream, feed, display, null); } catch (e) { console.log('error while doing offer', e); - removeLocalMediaElements(); + removeAllLocalMediaElements(); closePubPc(); return; } @@ -540,7 +640,7 @@ async function doOffer(feed, display) { console.log('Performing ICE restart'); pubPc.restartIce(); } - pubPc._feed = feed; + myFeed = feed; try { const offer = await pubPc.createOffer(); @@ -549,14 +649,66 @@ async function doOffer(feed, display) { return offer; } catch (e) { console.log('error while doing offer', e); - removeLocalMediaElements(); + removeAllLocalMediaElements(); closePubPc(); return; } +} + +function hasFeedMidSubscription(feed, mid) { + for (let [_, s] of subscriptions) { + if (s.feed === feed && s.mid === mid) return true; + } + return false; +} + +function deleteSubscriptionByFeed(feed) { + removeRemoteMediaElementsByFeedMid(feed, false); + for (let [sub_mid, s] of subscriptions) { + if (s.feed === feed) subscriptions.delete(sub_mid); + } +} +function updateSubscriptions(streams) { + if (!streams) return; + removeRemoteMediaElements(streams); + const newSubscriptions = new Map(); + streams.forEach(({ feed_id: feed, feed_mid: mid, mid: sub_mid, feed_display: display, type, active }) => { + const newStream = { + feed, + mid, + sub_mid, + display, + type, + ms: subscriptions.get(sub_mid)?.ms, + }; + if (active) + newSubscriptions.set(sub_mid, newStream); + }); + subscriptions = newSubscriptions; + refreshRemoteMediaElements(); +} + +function removeRemoteMediaElements(new_streams) { + if (!new_streams) return; + const oldSubscriptions = subscriptions; + const oldSubMids = oldSubscriptions.keys().toArray(); + const newSubMids = new_streams.values().toArray().map(s => s.mid && s.active); + const deletedSubMids = oldSubMids.filter(mid => !newSubMids.includes(mid)); + deletedSubMids.forEach(mid => removeRemoteMediaElementsBySubMid(mid, false)); +} + +function refreshRemoteMediaElements() { + for (let [sub_mid, s] of subscriptions) { + const { display, type, feed, mid, ms } = s; + if (type === 'video') + setRemoteVideoElement(ms, feed, mid, sub_mid, display); + if (type === 'audio') + setRemoteAudioElement(ms, feed, mid, sub_mid); + } } -async function doAnswer(streams, offer) { +async function doAnswer(offer) { if (!subPc) { const pc = new RTCPeerConnection({ 'iceServers': [{ @@ -570,7 +722,7 @@ async function doAnswer(streams, offer) { pc.onicecandidate = event => trickle({ candidate: event.candidate }); pc.oniceconnectionstatechange = () => { if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') { - removeRemoteMediaElements(); + removeAllRemoteMediaElements(); closeSubPc(); } }; @@ -587,20 +739,16 @@ async function doAnswer(streams, offer) { console.log('track.onended', evt); }; - const submid = event.transceiver?.mid || event.receiver.mid; - const stream = subPc._streams.filter(({ mid }) => mid === submid)[0]; - const feed = stream.feed_id; - const display = stream.feed_display; - const type = stream.type; /* 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 (type === 'video') - setRemoteVideoElement(remoteStream, feed, submid, display); - if (type === 'audio') - setRemoteAudioElement(remoteStream, feed, submid); + if (subscriptions.has(submid)) { + const stream = subscriptions.get(submid); + stream.ms = remoteStream; + refreshRemoteMediaElements(); + } }; } - subPc._streams = streams; try { await subPc.setRemoteDescription(offer); @@ -611,7 +759,7 @@ async function doAnswer(streams, offer) { return answer; } catch (e) { console.log('error creating subscriber answer', e); - removeRemoteMediaElements(); + removeAllRemoteMediaElements(); closeSubPc(); throw e; } @@ -643,7 +791,7 @@ function setLocalMediaElement(localStream, feed, display, room) { } if (display) { const nameElem = localVideoContainer.getElementsByTagName('span')[0]; - nameElem.innerHTML = `${display}|${feed}`; + nameElem.innerHTML = [display, feed].join('|'); } if (localStream) { const localVideoStreamElem = localVideoContainer.getElementsByTagName('video')[0]; @@ -651,34 +799,16 @@ function setLocalMediaElement(localStream, feed, display, room) { } } -function setRemoteVideoElement(remoteStream, feed, mid, display) { - if (!feed) return; - - /* Target all streams related to feed */ - if (!remoteStream && !mid && display) { - const videoIdStartsWith = `video_${feed}`; - const videoContainers = document.querySelectorAll(`[id^=${videoIdStartsWith}]`); - videoContainers.forEach(container => { - if (remoteStream) { - const remoteVideoStreamElem = container.getElementsByTagName('video')[0]; - remoteVideoStreamElem.srcObject = remoteStream; - } - if (display) { - const nameElem = container.getElementsByTagName('span')[0]; - mid = nameElem.innerHTML.split('|')[2]; - nameElem.innerHTML = `${display}|${feed}|${mid}`; - } - }); - return; - } +function setRemoteVideoElement(remoteStream, feed, feed_mid, sub_mid, display) { + if (!feed || !feed_mid || !sub_mid) return; - /* Target specific feed/mid */ - const id = `video_${feed}_${mid}_remote`; + /* Target specific sub_mid/feed/mid */ + const id = `video_${feed}_${feed_mid}_remote_${sub_mid}`; let remoteVideoContainer = document.getElementById(id); if (!remoteVideoContainer) { /* Non existing */ const nameElem = document.createElement('span'); - nameElem.innerHTML = `${display}|${feed}|${mid}`; + nameElem.innerHTML = [display, feed, feed_mid, sub_mid].join('|'); nameElem.style.display = 'table'; const remoteVideoStreamElem = document.createElement('video'); @@ -696,7 +826,7 @@ function setRemoteVideoElement(remoteStream, feed, mid, display) { } if (display) { const nameElem = remoteVideoContainer.getElementsByTagName('span')[0]; - nameElem.innerHTML = `${display}|${feed}|${mid}`; + nameElem.innerHTML = [display, feed, feed_mid, sub_mid].join('|'); } if (remoteStream) { const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0]; @@ -704,23 +834,11 @@ function setRemoteVideoElement(remoteStream, feed, mid, display) { } } -function setRemoteAudioElement(remoteStream, feed, mid) { - if (!feed) return; +function setRemoteAudioElement(remoteStream, feed, feed_mid, sub_mid) { + if (!feed || !feed_mid || !sub_mid) return; - /* Target all streams related to feed */ - if (!remoteStream && !mid) { - const audioIdStartsWith = `audio_${feed}`; - const audioContainers = document.querySelectorAll(`[id^=${audioIdStartsWith}]`); - audioContainers.forEach(container => { - if (remoteStream) { - const remoteAudioStreamElem = container.getElementsByTagName('audio')[0]; - remoteAudioStreamElem.srcObject = remoteStream; - } - }); - return; - } - - const id = `audio_${feed}_${mid}_remote`; + /* Target specific sub_mid/feed/mid */ + const id = `audio_${feed}_${feed_mid}_remote_${sub_mid}`; let remoteAudioContainer = document.getElementById(id); if (!remoteAudioContainer) { const remoteAudioStreamElem = document.createElement('audio'); @@ -751,17 +869,20 @@ function removeMediaElement(container, stopTracks = true) { container.remove(); } -function removeMediaElementsByFeed(feed, stopTracks) { - const audioIdStartsWith = `audio_${feed}`; - const audioContainers = document.querySelectorAll(`[id^=${audioIdStartsWith}]`); - audioContainers.forEach(container => removeMediaElement(container, stopTracks)); +function removeRemoteMediaElementsBySubMid(sub_mid, stopTracks) { + const idEndsWith = `_remote_${sub_mid}`; + const containers = document.querySelectorAll(`[id$=${idEndsWith}]`); + containers.forEach(container => removeMediaElement(container, stopTracks)); +} - const videoIdStartsWith = `video_${feed}`; - const videoContainers = document.querySelectorAll(`[id^=${videoIdStartsWith}]`); - videoContainers.forEach(container => removeMediaElement(container, stopTracks)); +function removeRemoteMediaElementsByFeedMid(feed_mid, stopTracks) { + [`audio_${feed_mid}`, `video_${feed_mid}`].forEach(idStartsWith => { + const containers = document.querySelectorAll(`[id^=${idStartsWith}]`); + containers.forEach(container => removeMediaElement(container, stopTracks)); + }); } -function removeLocalMediaElements() { +function removeAllLocalMediaElements() { const locals = document.getElementById('locals'); const localMediaContainers = locals.getElementsByTagName('div'); for (let i = 0; localMediaContainers && i < localMediaContainers.length; i++) @@ -770,7 +891,7 @@ function removeLocalMediaElements() { locals.removeChild(locals.firstChild); } -function removeRemoteMediaElements() { +function removeAllRemoteMediaElements() { var remotes = document.getElementById('remotes'); const remoteMediaContainers = remotes.getElementsByTagName('div'); for (let i = 0; remoteMediaContainers && i < remoteMediaContainers.length; i++) @@ -780,8 +901,8 @@ function removeRemoteMediaElements() { } function removeAllMediaElements() { - removeLocalMediaElements(); - removeRemoteMediaElements(); + removeAllLocalMediaElements(); + removeAllRemoteMediaElements(); document.getElementById('videos').getElementsByTagName('span')[0].innerHTML = ' --- VIDEOROOM () --- '; } diff --git a/examples/videoroom-ms/src/index.js b/examples/videoroom-ms/src/index.js index 05b71a4..effa1ee 100644 --- a/examples/videoroom-ms/src/index.js +++ b/examples/videoroom-ms/src/index.js @@ -206,9 +206,6 @@ function initFrontEnd() { }); pubHandle.on(VideoRoomPlugin.EVENT.VIDEOROOM_KICKED, async evtdata => { - if (pubHandle.feed === evtdata.feed) { - await msHandles.detachPubHandle(); - } replyEvent(socket, 'kicked', evtdata); }); @@ -274,11 +271,30 @@ function initFrontEnd() { replyEvent(socket, 'subscribed', response, _id); Logger.info(`${LOG_NS} ${remote} subscribed sent`); } catch ({ message }) { - if (subHandle) subHandle.detach().catch(() => { }); 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; diff --git a/src/plugins/videoroom-plugin.js b/src/plugins/videoroom-plugin.js index 8ca1405..0a9ae37 100644 --- a/src/plugins/videoroom-plugin.js +++ b/src/plugins/videoroom-plugin.js @@ -768,13 +768,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); @@ -783,15 +782,9 @@ class VideoRoomHandle extends Handle { request: REQUEST_PUBLISH, }; - /* [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 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; @@ -867,10 +860,11 @@ class VideoRoomHandle extends Handle { * @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, streams, 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, @@ -895,6 +889,7 @@ class VideoRoomHandle extends Handle { /* [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 || {}; From 7bedbb2903617471de0c794dcbe965642673412b Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Wed, 27 Mar 2024 20:21:50 +0100 Subject: [PATCH 4/8] Use a local "streams" array for kicked, display and leaving events --- .../videoroom-ms/html/videoroom-ms-client.js | 108 +++++++++--------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/examples/videoroom-ms/html/videoroom-ms-client.js b/examples/videoroom-ms/html/videoroom-ms-client.js index 4cd8509..98bbd9e 100644 --- a/examples/videoroom-ms/html/videoroom-ms-client.js +++ b/examples/videoroom-ms/html/videoroom-ms-client.js @@ -351,7 +351,7 @@ socket.on('joined', async ({ data }) => { setLocalMediaElement(null, null, null, data.room); try { - await _publish({ feed: data.feed, display: data.display}); + await _publish({ feed: data.feed, display: data.display }); subscribeTo(data.publishers, data.room); } catch (e) { console.log('error while publishing', e); @@ -480,7 +480,20 @@ socket.on('talking', ({ data }) => { socket.on('kicked', ({ data }) => { console.log('participant kicked', data); if (data.feed) { - deleteSubscriptionByFeed(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); } }); @@ -512,12 +525,13 @@ socket.on('configured', async ({ data, _id }) => { socket.on('display', ({ data }) => { console.log('feed changed display name', data); - for (let [_, stream] of subscriptions) { - if (stream.feed === data.feed) { - stream.display = data.display; + const streams = subscriptions.values().toArray().map(s => { + if (s.feed_id === data.feed) { + s.feed_display = data.display; } - } - refreshRemoteMediaElements(); + return s; + }); + updateSubscriptions(streams); }); socket.on('started', ({ data }) => { @@ -556,7 +570,20 @@ socket.on('leaving', ({ data }) => { closePubPc(); } else { - deleteSubscriptionByFeed(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); } } }); @@ -592,9 +619,8 @@ socket.on('rtp-fwd-list', ({ data }) => { console.log('rtp forwarders list', data); }); -async function _restartPublisher(feed) { - const offer = await doOffer(feed, null); - configure({ feed, jsep: offer }); +async function _restartPublisher({ feed = myFeed } = {}) { + return _publish({ feed }); } async function _restartSubscriber() { @@ -662,28 +688,13 @@ function hasFeedMidSubscription(feed, mid) { return false; } -function deleteSubscriptionByFeed(feed) { - removeRemoteMediaElementsByFeedMid(feed, false); - for (let [sub_mid, s] of subscriptions) { - if (s.feed === feed) subscriptions.delete(sub_mid); - } -} - function updateSubscriptions(streams) { if (!streams) return; removeRemoteMediaElements(streams); const newSubscriptions = new Map(); - streams.forEach(({ feed_id: feed, feed_mid: mid, mid: sub_mid, feed_display: display, type, active }) => { - const newStream = { - feed, - mid, - sub_mid, - display, - type, - ms: subscriptions.get(sub_mid)?.ms, - }; - if (active) - newSubscriptions.set(sub_mid, newStream); + streams.forEach(s => { + s.ms = subscriptions.get(s.mid)?.ms; + newSubscriptions.set(s.mid, s); }); subscriptions = newSubscriptions; refreshRemoteMediaElements(); @@ -692,19 +703,21 @@ function updateSubscriptions(streams) { function removeRemoteMediaElements(new_streams) { if (!new_streams) return; const oldSubscriptions = subscriptions; - const oldSubMids = oldSubscriptions.keys().toArray(); - const newSubMids = new_streams.values().toArray().map(s => s.mid && s.active); + 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 { display, type, feed, mid, ms } = s; - if (type === 'video') - setRemoteVideoElement(ms, feed, mid, sub_mid, display); - if (type === 'audio') - setRemoteAudioElement(ms, feed, mid, sub_mid); + 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); + } } } @@ -799,16 +812,16 @@ function setLocalMediaElement(localStream, feed, display, room) { } } -function setRemoteVideoElement(remoteStream, feed, feed_mid, sub_mid, display) { - if (!feed || !feed_mid || !sub_mid) return; +function setRemoteVideoElement(remoteStream, sub_mid, display) { + if (!sub_mid) return; /* Target specific sub_mid/feed/mid */ - const id = `video_${feed}_${feed_mid}_remote_${sub_mid}`; + const id = `video_remote_${sub_mid}`; let remoteVideoContainer = document.getElementById(id); if (!remoteVideoContainer) { /* Non existing */ const nameElem = document.createElement('span'); - nameElem.innerHTML = [display, feed, feed_mid, sub_mid].join('|'); + nameElem.innerHTML = display; nameElem.style.display = 'table'; const remoteVideoStreamElem = document.createElement('video'); @@ -826,7 +839,7 @@ function setRemoteVideoElement(remoteStream, feed, feed_mid, sub_mid, display) { } if (display) { const nameElem = remoteVideoContainer.getElementsByTagName('span')[0]; - nameElem.innerHTML = [display, feed, feed_mid, sub_mid].join('|'); + nameElem.innerHTML = display; } if (remoteStream) { const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0]; @@ -834,11 +847,11 @@ function setRemoteVideoElement(remoteStream, feed, feed_mid, sub_mid, display) { } } -function setRemoteAudioElement(remoteStream, feed, feed_mid, sub_mid) { - if (!feed || !feed_mid || !sub_mid) return; +function setRemoteAudioElement(remoteStream, sub_mid) { + if (!sub_mid) return; /* Target specific sub_mid/feed/mid */ - const id = `audio_${feed}_${feed_mid}_remote_${sub_mid}`; + const id = `audio_remote_${sub_mid}`; let remoteAudioContainer = document.getElementById(id); if (!remoteAudioContainer) { const remoteAudioStreamElem = document.createElement('audio'); @@ -875,13 +888,6 @@ function removeRemoteMediaElementsBySubMid(sub_mid, stopTracks) { containers.forEach(container => removeMediaElement(container, stopTracks)); } -function removeRemoteMediaElementsByFeedMid(feed_mid, stopTracks) { - [`audio_${feed_mid}`, `video_${feed_mid}`].forEach(idStartsWith => { - const containers = document.querySelectorAll(`[id^=${idStartsWith}]`); - containers.forEach(container => removeMediaElement(container, stopTracks)); - }); -} - function removeAllLocalMediaElements() { const locals = document.getElementById('locals'); const localMediaContainers = locals.getElementsByTagName('div'); From 7c44d43564d127fb4a08a318fe592893933e4720 Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Thu, 28 Mar 2024 11:37:53 +0100 Subject: [PATCH 5/8] Update core media and slowlink events --- src/handle.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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; From 14d352bb666bcdd36057a118cbaf796c7b776bee Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Thu, 28 Mar 2024 12:57:39 +0100 Subject: [PATCH 6/8] Add mid to talking events --- src/plugins/videoroom-plugin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/videoroom-plugin.js b/src/plugins/videoroom-plugin.js index 0a9ae37..9303354 100644 --- a/src/plugins/videoroom-plugin.js +++ b/src/plugins/videoroom-plugin.js @@ -383,6 +383,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; From 573d7ec33aa4aaaae24da3d8ac6e5783fa44bf4c Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Thu, 28 Mar 2024 17:37:29 +0100 Subject: [PATCH 7/8] Support rtp forwarding --- .../videoroom-ms/html/videoroom-ms-client.js | 8 +- src/plugins/videoroom-plugin.js | 231 ++++++++++++------ 2 files changed, 158 insertions(+), 81 deletions(-) diff --git a/examples/videoroom-ms/html/videoroom-ms-client.js b/examples/videoroom-ms/html/videoroom-ms-client.js index 98bbd9e..79f0ede 100644 --- a/examples/videoroom-ms/html/videoroom-ms-client.js +++ b/examples/videoroom-ms/html/videoroom-ms-client.js @@ -284,15 +284,13 @@ function _allow({ room = myRoom, action, token, secret = 'adminpwd' }) { }); } -function _startForward({ feed, room = myRoom, host = 'localhost', audio_port, video_port, data_port = null, secret = 'adminpwd' }) { +function _startForward({ feed = myFeed, host, room = myRoom, streams, secret = 'adminpwd' }) { socket.emit('rtp-fwd-start', { data: { room, feed, host, - audio_port, - video_port, - data_port, + streams, secret, }, _id: getId(), @@ -311,7 +309,7 @@ function _stopForward({ stream, feed, room = myRoom, secret = 'adminpwd' }) { }); } -function _listForward({ room = myRoom, secret = 'adminpwd' }) { +function _listForward({ room = myRoom, secret = 'adminpwd' } = {}) { socket.emit('rtp-fwd-list', { data: { room, secret }, _id: getId(), diff --git a/src/plugins/videoroom-plugin.js b/src/plugins/videoroom-plugin.js index 9303354..b448aa6 100644 --- a/src/plugins/videoroom-plugin.js +++ b/src/plugins/videoroom-plugin.js @@ -263,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; @@ -306,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 (f.data_stream_id) { + fwd.data_stream = f.data_stream_id; + fwd.data_port = f.port; + } + if (typeof f.ssrc === 'number') { + fwd.ssrc = f.ssrc; } - 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 (typeof f.pt === 'number') { + fwd.pt = f.pt; } - if (forw.data_stream_id) { - forwarder.data_port = forw.port; - forwarder.data_stream = forw.data_stream_id; + 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; @@ -1282,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 @@ -1297,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; @@ -1483,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 */ /** From 3f8b43024b6890f54cdd70bdf706de689d762ed7 Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Thu, 13 Jun 2024 14:03:20 +0200 Subject: [PATCH 8/8] Align to eslint v9 and update deps --- examples/videoroom-ms/html/.eslintrc.json | 5 --- .../videoroom-ms/html/videoroom-ms-client.js | 2 +- examples/videoroom-ms/package-lock.json | 42 +++++++++---------- 3 files changed, 22 insertions(+), 27 deletions(-) delete mode 100644 examples/videoroom-ms/html/.eslintrc.json diff --git a/examples/videoroom-ms/html/.eslintrc.json b/examples/videoroom-ms/html/.eslintrc.json deleted file mode 100644 index 5c3f02e..0000000 --- a/examples/videoroom-ms/html/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "browser": true - } -} \ 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 index 79f0ede..102eb54 100644 --- a/examples/videoroom-ms/html/videoroom-ms-client.js +++ b/examples/videoroom-ms/html/videoroom-ms-client.js @@ -942,7 +942,7 @@ function _closePC(pc) { pc.ontrack = null; try { pc.close(); - } catch (e) { } + } catch (_e) { } } function closeAllPCs() { diff --git a/examples/videoroom-ms/package-lock.json b/examples/videoroom-ms/package-lock.json index b8548a4..e42c5ef 100644 --- a/examples/videoroom-ms/package-lock.json +++ b/examples/videoroom-ms/package-lock.json @@ -12,9 +12,9 @@ } }, "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + "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", @@ -30,9 +30,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", - "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "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" } @@ -246,9 +246,9 @@ } }, "node_modules/engine.io/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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" }, @@ -299,9 +299,9 @@ } }, "node_modules/express": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz", - "integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==", + "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", @@ -775,9 +775,9 @@ } }, "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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" }, @@ -808,9 +808,9 @@ } }, "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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" }, @@ -829,9 +829,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/socket.io/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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" },