Skip to content

Commit

Permalink
videoroom: add switch API
Browse files Browse the repository at this point in the history
  • Loading branch information
atoppi committed Jan 3, 2023
1 parent 6cd2249 commit 372594b
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 56 deletions.
108 changes: 58 additions & 50 deletions examples/videoroom/html/videoroom-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,22 @@ function _pause({ feed }) {
});
}


function _switch({ from_feed, to_feed, audio = true, video = true, data = false }) {
const switchData = {
from_feed,
to_feed,
audio,
video,
data,
};

socket.emit('switch', {
data: switchData,
_id: getId(),
});
}

function _exists({ room = myRoom } = {}) {
const existsData = {
room,
Expand Down Expand Up @@ -299,7 +315,7 @@ socket.on('videoroom-error', ({ error, _id }) => {
}
if (pendingOfferMap.has(_id)) {
const { feed } = pendingOfferMap.get(_id);
removeVideoElement(feed);
removeVideoElementByFeed(feed);
closePC(feed);
pendingOfferMap.delete(_id);
return;
Expand Down Expand Up @@ -339,7 +355,7 @@ socket.on('talking', ({ data }) => {
socket.on('kicked', ({ data }) => {
console.log('participant kicked', data);
if (data.feed) {
removeVideoElement(data.feed);
removeVideoElementByFeed(data.feed);
closePC(data.feed);
}
});
Expand All @@ -351,7 +367,7 @@ socket.on('allowed', ({ data }) => {
socket.on('configured', async ({ data, _id }) => {
console.log('feed configured', data);
pendingOfferMap.delete(_id);
let pc = pcMap.get(data.feed);
const pc = pcMap.get(data.feed);
if (pc && data.jsep) {
try {
await pc.setRemoteDescription(data.jsep);
Expand All @@ -368,10 +384,7 @@ socket.on('configured', async ({ data, _id }) => {

socket.on('display', ({ data }) => {
console.log('feed changed display name', data);
const div = document.getElementById('video_' + data.feed);
if (div) {
div.getElementsByTagName('span')[0].innerHTML = data.display;
}
setRemoteVideoElement(null, data.feed, data.display);
});

socket.on('started', ({ data }) => {
Expand All @@ -382,6 +395,12 @@ 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);
Expand All @@ -390,15 +409,15 @@ socket.on('feed-list', ({ data }) => {
socket.on('unpublished', ({ data }) => {
console.log('feed unpublished', data);
if (data.feed) {
removeVideoElement(data.feed);
removeVideoElementByFeed(data.feed);
closePC(data.feed);
}
});

socket.on('leaving', ({ data }) => {
console.log('feed leaving', data);
if (data.feed) {
removeVideoElement(data.feed);
removeVideoElementByFeed(data.feed);
closePC(data.feed);
}
});
Expand Down Expand Up @@ -455,7 +474,7 @@ async function doOffer(feed, display) {
pc.onicecandidate = event => trickle({ feed, candidate: event.candidate });
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') {
removeVideoElement(feed);
removeVideoElementByFeed(feed);
closePC(feed);
}
};
Expand All @@ -473,7 +492,7 @@ async function doOffer(feed, display) {
setLocalVideoElement(localStream, feed, display);
} catch (e) {
console.log('error while doing offer', e);
removeVideoElement(feed);
removeVideoElementByFeed(feed);
closePC(feed);
return;
}
Expand All @@ -491,7 +510,7 @@ async function doOffer(feed, display) {
return offer;
} catch (e) {
console.log('error while doing offer', e);
removeVideoElement(feed);
removeVideoElementByFeed(feed);
closePC(feed);
return;
}
Expand All @@ -510,7 +529,7 @@ async function doAnswer(feed, display, offer) {
pc.onicecandidate = event => trickle({ feed, candidate: event.candidate });
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') {
removeVideoElement(feed);
removeVideoElementByFeed(feed);
closePC(feed);
}
};
Expand Down Expand Up @@ -546,7 +565,7 @@ async function doAnswer(feed, display, offer) {
return answer;
} catch (e) {
console.log('error creating subscriber answer', e);
removeVideoElement(feed);
removeVideoElementByFeed(feed);
closePC(feed);
throw e;
}
Expand Down Expand Up @@ -604,7 +623,8 @@ 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;';
remoteVideoStreamElem.srcObject = remoteStream;
if (remoteStream)
remoteVideoStreamElem.srcObject = remoteStream;

const remoteVideoContainer = document.createElement('div');
remoteVideoContainer.id = 'video_' + feed;
Expand All @@ -619,53 +639,41 @@ function setRemoteVideoElement(remoteStream, feed, display) {
const nameElem = remoteVideoContainer.getElementsByTagName('span')[0];
nameElem.innerHTML = display + ' (' + feed + ')';
}
const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0];
remoteVideoStreamElem.srcObject = remoteStream;
if (remoteStream) {
const remoteVideoStreamElem = remoteVideoContainer.getElementsByTagName('video')[0];
remoteVideoStreamElem.srcObject = remoteStream;
}
}
}

function removeVideoElement(feed) {
const videoContainer = document.getElementById('video_' + feed);
if (videoContainer) {
let videoStreamElem = videoContainer.getElementsByTagName('video').length > 0 ? videoContainer.getElementsByTagName('video')[0] : null;
if (videoStreamElem && videoStreamElem.srcObject) {
videoStreamElem.srcObject.getTracks().forEach(track => track.stop());
videoStreamElem.srcObject = null;
}
videoContainer.remove();
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');
let localVideoContainers = locals.getElementsByTagName('div');
for (let i = 0; i < localVideoContainers.length; i++) {
const videoContainer = localVideoContainers[i];
const videoStreamElem = videoContainer.getElementsByTagName('video')[0];
if (videoStreamElem && videoStreamElem.srcObject) {
videoStreamElem.srcObject.getTracks().forEach(track => track.stop());
videoStreamElem.srcObject = null;
}
videoContainer.remove();
}
while (locals.firstChild) {
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');
let remoteVideoContainers = remotes.getElementsByTagName('div');
for (let i = 0; i < remoteVideoContainers.length; i++) {
const videoContainer = remoteVideoContainers[i];
const videoStreamElem = videoContainer.getElementsByTagName('video')[0];
if (videoStreamElem && videoStreamElem.srcObject) {
videoStreamElem.srcObject.getTracks().forEach(track => track.stop());
videoStreamElem.srcObject = null;
}
videoContainer.remove();
}
while (remotes.firstChild) {
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 () --- ';
}

Expand Down
25 changes: 22 additions & 3 deletions examples/videoroom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ function initFrontEnd() {

try {
const response = await handle.start(startdata);
delete evtdata.started;
replyEvent(socket, 'started', response, _id);
Logger.info(`${LOG_NS} ${remote} started sent`);
} catch ({ message }) {
Expand All @@ -352,14 +351,34 @@ function initFrontEnd() {

try {
const response = await handle.pause();
delete evtdata.paused;
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 = clientHandles.getHandleByFeed(switchdata.from_feed);
if (!checkSessions(janodeSession, handle, socket, evtdata)) return;

try {
const response = await handle.switch({
to_feed: switchdata.to_feed,
audio: switchdata.audio,
video: switchdata.video,
data: switchdata.data,
});
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`);
Expand Down Expand Up @@ -603,4 +622,4 @@ function replyError(socket, message, request, _id) {
if (_id) evtdata._id = _id;

socket.emit('videoroom-error', evtdata);
}
}
59 changes: 56 additions & 3 deletions src/plugins/videoroom-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const REQUEST_ENABLE_RECORDING = 'enable_recording';
const REQUEST_KICK = 'kick';
const REQUEST_START = 'start';
const REQUEST_PAUSE = 'pause';
const REQUEST_SWITCH = 'switch';
const REQUEST_PUBLISH = 'publish';
const REQUEST_UNPUBLISH = 'unpublish';
const REQUEST_LEAVE = 'leave';
Expand Down Expand Up @@ -46,6 +47,7 @@ const PLUGIN_EVENT = {
PUB_PEER_JOINED: 'videoroom_publisher_joined',
STARTED: 'videoroom_started',
PAUSED: 'videoroom_paused',
SWITCHED: 'videoroom_switched',
CONFIGURED: 'videoroom_configured',
SLOW_LINK: 'videoroom_slowlink',
DISPLAY: 'videoroom_display',
Expand Down Expand Up @@ -398,7 +400,7 @@ class VideoRoomHandle extends Handle {
break;
}
/* Display name changed event */
if (typeof message_data.display !== 'undefined') {
if (typeof message_data.display !== 'undefined' && typeof message_data.switched === 'undefined') {
janode_event.event = PLUGIN_EVENT.DISPLAY;
janode_event.data.feed = message_data.id;
janode_event.data.display = message_data.display;
Expand All @@ -418,6 +420,18 @@ class VideoRoomHandle extends Handle {
janode_event.data.paused = message_data.paused;
break;
}
/* Subscribed feed switched */
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;
}
break;
}
/* Unpublished own or other feed */
if (typeof message_data.unpublished !== 'undefined') {
janode_event.event = PLUGIN_EVENT.UNPUBLISHED;
Expand Down Expand Up @@ -826,6 +840,34 @@ class VideoRoomHandle extends Handle {
throw (error);
}

/**
* Switch to another feed.
*
* @param {object} params
* @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
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_SWITCHED>}
*/
async switch({ to_feed, audio, video, data }) {
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;

const response = await this.message(body);
const { event, data: evtdata } = response._janode || {};
if (event === PLUGIN_EVENT.SWITCHED && evtdata.switched === 'ok') {
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}

/**
* Leave a room.
* Can be used by both publishers and subscribers.
Expand Down Expand Up @@ -883,7 +925,7 @@ class VideoRoomHandle extends Handle {
* @param {boolean} params.record - True starts recording for all participants in an already running conference, false stops the recording
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_RECORDING_ENABLED_STATE>}
*/
async enable_recording({ room, secret , record}) {
async enable_recording({ room, secret, record }) {
const body = {
request: REQUEST_ENABLE_RECORDING,
room,
Expand Down Expand Up @@ -1331,6 +1373,17 @@ class VideoRoomHandle extends Handle {
* @property {string} paused - A string with the value returned by Janus
*/

/**
* The response event for subscriber switch request.
*
* @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 {string} switched - A string with the value returned by Janus
* @property {string} display - The display name of the new feed
*/

/**
* The response event for publisher unpublish request.
*
Expand Down Expand Up @@ -1525,4 +1578,4 @@ export default {
*/
VIDEOROOM_ERROR: PLUGIN_EVENT.ERROR,
},
};
};

0 comments on commit 372594b

Please sign in to comment.