mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-29 10:10:19 +00:00
- Bootswatch is a theme library for bootstrap. In Debian, only 3.x version of the package is available. It is compatible with bootstrap 3.x but not bootstrap 5. Drop the theming altogether and use the basic bootstrap style (which is already very close to the theme). - Updated copyright year, mention the video room files in debian/copyright. - Drop libjs-spin.js which is no longer used by the updated code. - Change bootstrap version to 5.x from the earlier 4.x. Also add node-popper2 library (needed by bootstrap5 and video room code) as explicit dependency. - Add missing style for btn-default class dropped in bootstrap 5. - .simulcast-button CSS style is not longer needed as updated code used flex box with .d-flex bootstrap class. Tests: - Compare the files in janus source code around Mar 2022 with the files in FreedomBox source code before this patch. Compare latest janus source code with the files after this patch. Both sets of changes are very similar. - Connect to video room using two browser windows. Connection is successful and 2 video streams are shown in each of the browser windows. - Styling looks close to the demo on janus website and is acceptable. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
1088 lines
62 KiB
JavaScript
1088 lines
62 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
/*
|
|
#
|
|
# This file is part of FreedomBox.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# This file based on example code from Janus Demos which is licensed as
|
|
# follows.
|
|
#
|
|
# 2014-2025 Meetecho
|
|
#
|
|
# GPL-3 with OpenSSL exception
|
|
# If you modify this Program, or any covered work,
|
|
# by linking or combining it with OpenSSL
|
|
# (or a modified version of that library),
|
|
# containing parts covered by the terms of OpenSSL License,
|
|
# the licensors of this Program grant you
|
|
# additional permission to convey the resulting work.
|
|
# Corresponding Source for a non-source form of such a combination
|
|
# shall include the source code for the parts of openssl used
|
|
# as well as that of the covered work.
|
|
*/
|
|
|
|
var janus = null;
|
|
var sfutest = null;
|
|
var opaqueId = "videoroomtest-"+Janus.randomString(12);
|
|
|
|
var myroom = 1234; // Demo room
|
|
if(getQueryStringValue("room") !== "")
|
|
myroom = parseInt(getQueryStringValue("room"));
|
|
var myusername = null;
|
|
var myid = null;
|
|
var mystream = null;
|
|
// We use this other ID just to map our subscriptions to us
|
|
var mypvtid = null;
|
|
|
|
var remoteFeed = null;
|
|
var feeds = {}, feedStreams = {}, subStreams = {}, slots = {}, mids = {}, subscriptions = {};
|
|
var localTracks = {}, localVideos = 0, remoteTracks = {};
|
|
var bitrateTimer = [], simulcastStarted = {}, svcStarted = {};
|
|
|
|
var doSimulcast = (getQueryStringValue("simulcast") === "yes" || getQueryStringValue("simulcast") === "true");
|
|
var doSvc = getQueryStringValue("svc");
|
|
if(doSvc === "")
|
|
doSvc = null;
|
|
var acodec = (getQueryStringValue("acodec") !== "" ? getQueryStringValue("acodec") : null);
|
|
var vcodec = (getQueryStringValue("vcodec") !== "" ? getQueryStringValue("vcodec") : null);
|
|
var subscriber_mode = (getQueryStringValue("subscriber-mode") === "yes" || getQueryStringValue("subscriber-mode") === "true");
|
|
var use_msid = (getQueryStringValue("msid") === "yes" || getQueryStringValue("msid") === "true");
|
|
|
|
$(document).ready(function() {
|
|
// Initialize the library (all console debuggers enabled)
|
|
Janus.init({debug: "all", callback: function() {
|
|
// Use a button to start the video room
|
|
$('#start').one('click', function() {
|
|
$(this).attr('disabled', true).unbind('click');
|
|
// Make sure the browser supports WebRTC
|
|
if(!Janus.isWebrtcSupported()) {
|
|
bootbox.alert("No WebRTC support... ");
|
|
return;
|
|
}
|
|
// Create session
|
|
janus = new Janus(
|
|
{
|
|
server: server,
|
|
iceServers: iceServers,
|
|
token: token,
|
|
apisecret: apisecret,
|
|
success: function() {
|
|
// Attach to video room test plugin
|
|
janus.attach(
|
|
{
|
|
plugin: "janus.plugin.videoroom",
|
|
opaqueId: opaqueId,
|
|
success: function(pluginHandle) {
|
|
$('#details').remove();
|
|
sfutest = pluginHandle;
|
|
Janus.log("Plugin attached! (" + sfutest.getPlugin() + ", id=" + sfutest.getId() + ")");
|
|
Janus.log(" -- This is a publisher/manager");
|
|
// Prepare the username registration
|
|
$('#videojoin').removeClass('hide');
|
|
$('#registernow').removeClass('hide');
|
|
$('#register').click(registerUsername);
|
|
$('#username').focus();
|
|
$('#start').removeAttr('disabled').html("Stop")
|
|
.click(function() {
|
|
$(this).attr('disabled', true);
|
|
janus.destroy();
|
|
});
|
|
},
|
|
error: function(error) {
|
|
Janus.error(" -- Error attaching plugin...", error);
|
|
bootbox.alert("Error attaching plugin... " + error);
|
|
},
|
|
consentDialog: function(on) {
|
|
Janus.debug("Consent dialog should be " + (on ? "on" : "off") + " now");
|
|
},
|
|
iceState: function(state) {
|
|
Janus.log("ICE state changed to " + state);
|
|
},
|
|
mediaState: function(medium, on, mid) {
|
|
Janus.log("Janus " + (on ? "started" : "stopped") + " receiving our " + medium + " (mid=" + mid + ")");
|
|
},
|
|
webrtcState: function(on) {
|
|
Janus.log("Janus says our WebRTC PeerConnection is " + (on ? "up" : "down") + " now");
|
|
$("#videolocal").parent().parent().unblock();
|
|
if(!on)
|
|
return;
|
|
$('#publish').remove();
|
|
// This controls allows us to override the global room bitrate cap
|
|
$('#bitrate').parent().parent().removeClass('hide');
|
|
$('#bitrate a').click(function() {
|
|
$('.dropdown-toggle').dropdown('hide');
|
|
let id = $(this).attr("id");
|
|
let bitrate = parseInt(id)*1000;
|
|
if(bitrate === 0) {
|
|
Janus.log("Not limiting bandwidth via REMB");
|
|
} else {
|
|
Janus.log("Capping bandwidth to " + bitrate + " via REMB");
|
|
}
|
|
$('#bitrateset').text($(this).text()).parent().removeClass('open');
|
|
sfutest.send({ message: { request: "configure", bitrate: bitrate }});
|
|
return false;
|
|
});
|
|
},
|
|
slowLink: function(uplink, lost, mid) {
|
|
Janus.warn("Janus reports problems " + (uplink ? "sending" : "receiving") +
|
|
" packets on mid " + mid + " (" + lost + " lost packets)");
|
|
},
|
|
onmessage: function(msg, jsep) {
|
|
Janus.debug(" ::: Got a message (publisher) :::", msg);
|
|
let event = msg["videoroom"];
|
|
Janus.debug("Event: " + event);
|
|
if(event != undefined && event != null) {
|
|
if(event === "joined") {
|
|
// Publisher/manager created, negotiate WebRTC and attach to existing feeds, if any
|
|
myid = msg["id"];
|
|
mypvtid = msg["private_id"];
|
|
Janus.log("Successfully joined room " + msg["room"] + " with ID " + myid);
|
|
if(subscriber_mode) {
|
|
$('#videojoin').addClass('hide');
|
|
$('#videos').removeClass('hide');
|
|
} else {
|
|
publishOwnFeed(true);
|
|
}
|
|
// Any new feed to attach to?
|
|
if(msg["publishers"]) {
|
|
let list = msg["publishers"];
|
|
Janus.debug("Got a list of available publishers/feeds:", list);
|
|
let sources = null;
|
|
for(let f in list) {
|
|
if(list[f]["dummy"])
|
|
continue;
|
|
let id = list[f]["id"];
|
|
let display = list[f]["display"];
|
|
let streams = list[f]["streams"];
|
|
for(let i in streams) {
|
|
let stream = streams[i];
|
|
stream["id"] = id;
|
|
stream["display"] = display;
|
|
}
|
|
let slot = feedStreams[id] ? feedStreams[id].slot : null;
|
|
let remoteVideos = feedStreams[id] ? feedStreams[id].remoteVideos : 0;
|
|
feedStreams[id] = {
|
|
id: id,
|
|
display: display,
|
|
streams: streams,
|
|
slot: slot,
|
|
remoteVideos: remoteVideos
|
|
}
|
|
Janus.debug(" >> [" + id + "] " + display + ":", streams);
|
|
if(!sources)
|
|
sources = [];
|
|
sources.push(streams);
|
|
}
|
|
if(sources)
|
|
subscribeTo(sources);
|
|
}
|
|
} else if(event === "destroyed") {
|
|
// The room has been destroyed
|
|
Janus.warn("The room has been destroyed!");
|
|
bootbox.alert("The room has been destroyed", function() {
|
|
window.location.reload();
|
|
});
|
|
} else if(event === "event") {
|
|
// Any info on our streams or a new feed to attach to?
|
|
if(msg["streams"]) {
|
|
let streams = msg["streams"];
|
|
for(let i in streams) {
|
|
let stream = streams[i];
|
|
stream["id"] = myid;
|
|
stream["display"] = myusername;
|
|
}
|
|
feedStreams[myid] = {
|
|
id: myid,
|
|
display: myusername,
|
|
streams: streams
|
|
}
|
|
} else if(msg["publishers"]) {
|
|
let list = msg["publishers"];
|
|
Janus.debug("Got a list of available publishers/feeds:", list);
|
|
let sources = null;
|
|
for(let f in list) {
|
|
if(list[f]["dummy"])
|
|
continue;
|
|
let id = list[f]["id"];
|
|
let display = list[f]["display"];
|
|
let streams = list[f]["streams"];
|
|
for(let i in streams) {
|
|
let stream = streams[i];
|
|
stream["id"] = id;
|
|
stream["display"] = display;
|
|
}
|
|
let slot = feedStreams[id] ? feedStreams[id].slot : null;
|
|
let remoteVideos = feedStreams[id] ? feedStreams[id].remoteVideos : 0;
|
|
feedStreams[id] = {
|
|
id: id,
|
|
display: display,
|
|
streams: streams,
|
|
slot: slot,
|
|
remoteVideos: remoteVideos
|
|
}
|
|
Janus.debug(" >> [" + id + "] " + display + ":", streams);
|
|
if(!sources)
|
|
sources = [];
|
|
sources.push(streams);
|
|
}
|
|
if(sources)
|
|
subscribeTo(sources);
|
|
} else if(msg["leaving"]) {
|
|
// One of the publishers has gone away?
|
|
let leaving = msg["leaving"];
|
|
Janus.log("Publisher left: " + leaving);
|
|
unsubscribeFrom(leaving);
|
|
} else if(msg["unpublished"]) {
|
|
// One of the publishers has unpublished?
|
|
let unpublished = msg["unpublished"];
|
|
Janus.log("Publisher left: " + unpublished);
|
|
if(unpublished === 'ok') {
|
|
// That's us
|
|
sfutest.hangup();
|
|
return;
|
|
}
|
|
unsubscribeFrom(unpublished);
|
|
} else if(msg["error"]) {
|
|
if(msg["error_code"] === 426) {
|
|
// This is a "no such room" error: give a more meaningful description
|
|
bootbox.alert(
|
|
"<p>Apparently room <code>" + myroom + "</code> (the one this page uses as a test room) " +
|
|
"does not exist...</p><p>Do you have an updated <code>janus.plugin.videoroom.cfg</code> " +
|
|
"configuration file? If not, make sure you copy the details of room <code>" + myroom + "</code> " +
|
|
"from that sample in your current configuration file, then restart Janus and try again."
|
|
);
|
|
} else {
|
|
bootbox.alert(msg["error"]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(jsep) {
|
|
Janus.debug("Handling SDP as well...", jsep);
|
|
sfutest.handleRemoteJsep({ jsep: jsep });
|
|
// Check if any of the media we wanted to publish has
|
|
// been rejected (e.g., wrong or unsupported codec)
|
|
let audio = msg["audio_codec"];
|
|
if(mystream && mystream.getAudioTracks() && mystream.getAudioTracks().length > 0 && !audio) {
|
|
// Audio has been rejected
|
|
toastr.warning("Our audio stream has been rejected, viewers won't hear us");
|
|
}
|
|
let video = msg["video_codec"];
|
|
if(mystream && mystream.getVideoTracks() && mystream.getVideoTracks().length > 0 && !video) {
|
|
// Video has been rejected
|
|
toastr.warning("Our video stream has been rejected, viewers won't see us");
|
|
// Hide the webcam video
|
|
$('#myvideo').addClass('hide');
|
|
$('#videolocal').prepend(
|
|
'<div class="no-video-container">' +
|
|
'<span class="no-video-text-sm">Video rejected, no webcam</span>' +
|
|
'</div>');
|
|
}
|
|
}
|
|
},
|
|
onlocaltrack: function(track, on) {
|
|
Janus.debug(" ::: Got a local track event :::");
|
|
Janus.debug("Local track " + (on ? "added" : "removed") + ":", track);
|
|
// We use the track ID as name of the element, but it may contain invalid characters
|
|
let trackId = track.id.replace(/[{}]/g, "");
|
|
if(!on) {
|
|
// Track removed, get rid of the stream and the rendering
|
|
let stream = localTracks[trackId];
|
|
if(stream) {
|
|
try {
|
|
let tracks = stream.getTracks();
|
|
for(let i in tracks) {
|
|
let mst = tracks[i];
|
|
if(mst)
|
|
mst.stop();
|
|
}
|
|
// eslint-disable-next-line no-unused-vars
|
|
} catch(e) {}
|
|
}
|
|
if(track.kind === "video") {
|
|
$('#myvideo' + trackId).remove();
|
|
localVideos--;
|
|
if(localVideos === 0) {
|
|
// No video, at least for now: show a placeholder
|
|
if($('#videolocal .no-video-container').length === 0) {
|
|
$('#videolocal').prepend(
|
|
'<div class="no-video-container">' +
|
|
'<span class="no-video-text">No webcam available</span>' +
|
|
'</div>');
|
|
}
|
|
}
|
|
}
|
|
delete localTracks[trackId];
|
|
return;
|
|
}
|
|
// If we're here, a new track was added
|
|
let stream = localTracks[trackId];
|
|
if(stream) {
|
|
// We've been here already
|
|
return;
|
|
}
|
|
$('#videos').removeClass('hide');
|
|
if($('#mute').length === 0) {
|
|
// Add a 'mute' button
|
|
$('#videolocal').append('<button class="btn btn-warning btn-xs mute-button" id="mute">Mute</button>');
|
|
$('#mute').click(toggleMute);
|
|
// Add an 'unpublish' button
|
|
$('#videolocal').append('<button class="btn btn-warning btn-xs unpublish-button" id="unpublish">Unpublish</button>');
|
|
$('#unpublish').click(unpublishOwnFeed);
|
|
}
|
|
if(track.kind === "audio") {
|
|
// We ignore local audio tracks, they'd generate echo anyway
|
|
if(localVideos === 0) {
|
|
// No video, at least for now: show a placeholder
|
|
if($('#videolocal .no-video-container').length === 0) {
|
|
$('#videolocal').prepend(
|
|
'<div class="no-video-container">' +
|
|
'<span class="no-video-text">No webcam available</span>' +
|
|
'</div>');
|
|
}
|
|
}
|
|
} else {
|
|
// New video track: create a stream out of it
|
|
localVideos++;
|
|
$('#videolocal .no-video-container').remove();
|
|
let stream = new MediaStream([track]);
|
|
localTracks[trackId] = stream;
|
|
Janus.log("Created local stream:", stream);
|
|
Janus.log(stream.getTracks());
|
|
Janus.log(stream.getVideoTracks());
|
|
$('#videolocal').prepend('<video class="rounded centered" id="myvideo' + trackId + '" width=100% autoplay playsinline muted="muted"/>');
|
|
Janus.attachMediaStream($('#myvideo' + trackId).get(0), stream);
|
|
}
|
|
if(sfutest.webrtcStuff.pc.iceConnectionState !== "completed" &&
|
|
sfutest.webrtcStuff.pc.iceConnectionState !== "connected") {
|
|
$("#videolocal").parent().parent().block({
|
|
message: '<b>Publishing...</b>',
|
|
css: {
|
|
border: 'none',
|
|
backgroundColor: 'transparent',
|
|
color: 'white'
|
|
}
|
|
});
|
|
}
|
|
},
|
|
// eslint-disable-next-line no-unused-vars
|
|
onremotetrack: function(track, mid, on) {
|
|
// The publisher stream is sendonly, we don't expect anything here
|
|
},
|
|
oncleanup: function() {
|
|
Janus.log(" ::: Got a cleanup notification: we are unpublished now :::");
|
|
mystream = null;
|
|
delete feedStreams[myid];
|
|
$('#videolocal').html('<button id="publish" class="btn btn-primary">Publish</button>');
|
|
$('#publish').click(function() { publishOwnFeed(true); });
|
|
$("#videolocal").parent().parent().unblock();
|
|
$('#bitrate').parent().parent().addClass('hide');
|
|
$('#bitrate a').unbind('click');
|
|
localTracks = {};
|
|
localVideos = 0;
|
|
}
|
|
});
|
|
},
|
|
error: function(error) {
|
|
Janus.error(error);
|
|
bootbox.alert(error, function() {
|
|
window.location.reload();
|
|
});
|
|
},
|
|
destroyed: function() {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
});
|
|
}});
|
|
});
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function checkEnter(field, event) {
|
|
let theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
|
|
if(theCode == 13) {
|
|
registerUsername();
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function registerUsername() {
|
|
if($('#username').length === 0) {
|
|
// Create fields to register
|
|
$('#register').click(registerUsername);
|
|
$('#username').focus();
|
|
} else {
|
|
// Try a registration
|
|
$('#username').attr('disabled', true);
|
|
$('#register').attr('disabled', true).unbind('click');
|
|
let username = $('#username').val();
|
|
if(username === "") {
|
|
$('#you')
|
|
.removeClass().addClass('badge bg-warning')
|
|
.html("Insert your display name (e.g., pippo)");
|
|
$('#username').removeAttr('disabled');
|
|
$('#register').removeAttr('disabled').click(registerUsername);
|
|
return;
|
|
}
|
|
if(/[^a-zA-Z0-9]/.test(username)) {
|
|
$('#you')
|
|
.removeClass().addClass('badge bg-warning')
|
|
.html('Input is not alphanumeric');
|
|
$('#username').removeAttr('disabled').val("");
|
|
$('#register').removeAttr('disabled').click(registerUsername);
|
|
return;
|
|
}
|
|
let register = {
|
|
request: "join",
|
|
room: myroom,
|
|
ptype: "publisher",
|
|
display: username
|
|
};
|
|
myusername = escapeXmlTags(username);
|
|
sfutest.send({ message: register });
|
|
}
|
|
}
|
|
|
|
function publishOwnFeed(useAudio) {
|
|
// Publish our stream
|
|
$('#publish').attr('disabled', true).unbind('click');
|
|
|
|
// We want sendonly audio and video (uncomment the data track
|
|
// too if you want to publish via datachannels as well)
|
|
let tracks = [];
|
|
if(useAudio)
|
|
tracks.push({ type: 'audio', capture: true, recv: false });
|
|
tracks.push({ type: 'video', capture: true, recv: false,
|
|
// We may need to enable simulcast or SVC on the video track
|
|
simulcast: doSimulcast,
|
|
// We only support SVC for VP9 and (still WIP) AV1
|
|
svc: ((vcodec === 'vp9' || vcodec === 'av1') && doSvc) ? doSvc : null
|
|
});
|
|
//~ tracks.push({ type: 'data' });
|
|
|
|
sfutest.createOffer(
|
|
{
|
|
tracks: tracks,
|
|
success: function(jsep) {
|
|
Janus.debug("Got publisher SDP!");
|
|
Janus.debug(jsep);
|
|
let publish = { request: "configure", audio: useAudio, video: true };
|
|
// You can force a specific codec to use when publishing by using the
|
|
// audiocodec and videocodec properties, for instance:
|
|
// publish["audiocodec"] = "opus"
|
|
// to force Opus as the audio codec to use, or:
|
|
// publish["videocodec"] = "vp9"
|
|
// to force VP9 as the videocodec to use. In both case, though, forcing
|
|
// a codec will only work if: (1) the codec is actually in the SDP (and
|
|
// so the browser supports it), and (2) the codec is in the list of
|
|
// allowed codecs in a room. With respect to the point (2) above,
|
|
// refer to the text in janus.plugin.videoroom.cfg for more details
|
|
if(acodec)
|
|
publish["audiocodec"] = acodec;
|
|
if(vcodec)
|
|
publish["videocodec"] = vcodec;
|
|
sfutest.send({ message: publish, jsep: jsep });
|
|
},
|
|
error: function(error) {
|
|
Janus.error("WebRTC error:", error);
|
|
if (useAudio) {
|
|
publishOwnFeed(false);
|
|
} else {
|
|
bootbox.alert("WebRTC error... " + error.message);
|
|
$('#publish').removeAttr('disabled').click(function() { publishOwnFeed(true); });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleMute() {
|
|
let muted = sfutest.isAudioMuted();
|
|
Janus.log((muted ? "Unmuting" : "Muting") + " local stream...");
|
|
if(muted)
|
|
sfutest.unmuteAudio();
|
|
else
|
|
sfutest.muteAudio();
|
|
muted = sfutest.isAudioMuted();
|
|
$('#mute').html(muted ? "Unmute" : "Mute");
|
|
}
|
|
|
|
function unpublishOwnFeed() {
|
|
// Unpublish our stream
|
|
$('#unpublish').attr('disabled', true).unbind('click');
|
|
let unpublish = { request: "unpublish" };
|
|
sfutest.send({ message: unpublish });
|
|
}
|
|
|
|
var creatingSubscription = false;
|
|
function subscribeTo(sources) {
|
|
// Check if we're still creating the subscription handle
|
|
if(creatingSubscription) {
|
|
// Still working on the handle, send this request later when it's ready
|
|
setTimeout(function() {
|
|
subscribeTo(sources);
|
|
}, 500);
|
|
return;
|
|
}
|
|
// If we already have a working subscription handle, just update that one
|
|
if(remoteFeed) {
|
|
// Prepare the streams to subscribe to, as an array: we have the list of
|
|
// streams the feeds are publishing, so we can choose what to pick or skip
|
|
let added = null, removed = null;
|
|
for(let s in sources) {
|
|
let streams = sources[s];
|
|
for(let i in streams) {
|
|
let stream = streams[i];
|
|
// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video
|
|
if(stream.type === "video" && Janus.webRTCAdapter.browserDetails.browser === "safari" &&
|
|
((stream.codec === "vp9" && !Janus.safariVp9) || (stream.codec === "vp8" && !Janus.safariVp8))) {
|
|
toastr.warning("Publisher is using " + stream.codec.toUpperCase +
|
|
", but Safari doesn't support it: disabling video stream #" + stream.mindex);
|
|
continue;
|
|
}
|
|
if(stream.disabled) {
|
|
Janus.log("Disabled stream:", stream);
|
|
// Unsubscribe
|
|
if(!removed)
|
|
removed = [];
|
|
removed.push({
|
|
feed: stream.id, // This is mandatory
|
|
mid: stream.mid // This is optional (all streams, if missing)
|
|
});
|
|
delete subscriptions[stream.id][stream.mid];
|
|
continue;
|
|
}
|
|
if(subscriptions[stream.id] && subscriptions[stream.id][stream.mid]) {
|
|
Janus.log("Already subscribed to stream, skipping:", stream);
|
|
continue;
|
|
}
|
|
// Find an empty slot in the UI for each new source
|
|
if(!feedStreams[stream.id].slot) {
|
|
let slot;
|
|
for(let i=1;i<6;i++) {
|
|
if(!feeds[i]) {
|
|
slot = i;
|
|
feeds[slot] = stream.id;
|
|
feedStreams[stream.id].slot = slot;
|
|
feedStreams[stream.id].remoteVideos = 0;
|
|
$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).removeClass('hide');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Subscribe
|
|
if(!added)
|
|
added = [];
|
|
added.push({
|
|
feed: stream.id, // This is mandatory
|
|
mid: stream.mid // This is optional (all streams, if missing)
|
|
});
|
|
if(!subscriptions[stream.id])
|
|
subscriptions[stream.id] = {};
|
|
subscriptions[stream.id][stream.mid] = true;
|
|
}
|
|
}
|
|
if((!added || added.length === 0) && (!removed || removed.length === 0)) {
|
|
// Nothing to do
|
|
return;
|
|
}
|
|
let update = { request: 'update' };
|
|
if(added)
|
|
update.subscribe = added;
|
|
if(removed)
|
|
update.unsubscribe = removed;
|
|
remoteFeed.send({ message: update });
|
|
// Nothing else we need to do
|
|
return;
|
|
}
|
|
// If we got here, we're creating a new handle for the subscriptions (we only need one)
|
|
creatingSubscription = true;
|
|
janus.attach(
|
|
{
|
|
plugin: "janus.plugin.videoroom",
|
|
opaqueId: opaqueId,
|
|
success: function(pluginHandle) {
|
|
remoteFeed = pluginHandle;
|
|
remoteTracks = {};
|
|
Janus.log("Plugin attached! (" + remoteFeed.getPlugin() + ", id=" + remoteFeed.getId() + ")");
|
|
Janus.log(" -- This is a multistream subscriber");
|
|
// Prepare the streams to subscribe to, as an array: we have the list of
|
|
// streams the feed is publishing, so we can choose what to pick or skip
|
|
let subscription = [];
|
|
for(let s in sources) {
|
|
let streams = sources[s];
|
|
for(let i in streams) {
|
|
let stream = streams[i];
|
|
// If the publisher is VP8/VP9 and this is an older Safari, let's avoid video
|
|
if(stream.type === "video" && Janus.webRTCAdapter.browserDetails.browser === "safari" &&
|
|
((stream.codec === "vp9" && !Janus.safariVp9) || (stream.codec === "vp8" && !Janus.safariVp8))) {
|
|
toastr.warning("Publisher is using " + stream.codec.toUpperCase +
|
|
", but Safari doesn't support it: disabling video stream #" + stream.mindex);
|
|
continue;
|
|
}
|
|
if(stream.disabled) {
|
|
Janus.log("Disabled stream:", stream);
|
|
// TODO Skipping for now, we should unsubscribe
|
|
continue;
|
|
}
|
|
Janus.log("Subscribed to " + stream.id + "/" + stream.mid + "?", subscriptions);
|
|
if(subscriptions[stream.id] && subscriptions[stream.id][stream.mid]) {
|
|
Janus.log("Already subscribed to stream, skipping:", stream);
|
|
continue;
|
|
}
|
|
// Find an empty slot in the UI for each new source
|
|
if(!feedStreams[stream.id].slot) {
|
|
let slot;
|
|
for(let i=1;i<6;i++) {
|
|
if(!feeds[i]) {
|
|
slot = i;
|
|
feeds[slot] = stream.id;
|
|
feedStreams[stream.id].slot = slot;
|
|
feedStreams[stream.id].remoteVideos = 0;
|
|
$('#remote' + slot).removeClass('hide').html(escapeXmlTags(stream.display)).removeClass('hide');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
subscription.push({
|
|
feed: stream.id, // This is mandatory
|
|
mid: stream.mid // This is optional (all streams, if missing)
|
|
});
|
|
if(!subscriptions[stream.id])
|
|
subscriptions[stream.id] = {};
|
|
subscriptions[stream.id][stream.mid] = true;
|
|
}
|
|
}
|
|
// We wait for the plugin to send us an offer
|
|
let subscribe = {
|
|
request: "join",
|
|
room: myroom,
|
|
ptype: "subscriber",
|
|
streams: subscription,
|
|
use_msid: use_msid,
|
|
private_id: mypvtid
|
|
};
|
|
remoteFeed.send({ message: subscribe });
|
|
},
|
|
error: function(error) {
|
|
Janus.error(" -- Error attaching plugin...", error);
|
|
bootbox.alert("Error attaching plugin... " + error);
|
|
},
|
|
iceState: function(state) {
|
|
Janus.log("ICE state (remote feed) changed to " + state);
|
|
},
|
|
webrtcState: function(on) {
|
|
Janus.log("Janus says this WebRTC PeerConnection (remote feed) is " + (on ? "up" : "down") + " now");
|
|
},
|
|
slowLink: function(uplink, lost, mid) {
|
|
Janus.warn("Janus reports problems " + (uplink ? "sending" : "receiving") +
|
|
" packets on mid " + mid + " (" + lost + " lost packets)");
|
|
},
|
|
onmessage: function(msg, jsep) {
|
|
Janus.debug(" ::: Got a message (subscriber) :::", msg);
|
|
let event = msg["videoroom"];
|
|
Janus.debug("Event: " + event);
|
|
if(msg["error"]) {
|
|
bootbox.alert(msg["error"]);
|
|
} else if(event) {
|
|
if(event === "attached") {
|
|
// Now we have a working subscription, next requests will update this one
|
|
creatingSubscription = false;
|
|
Janus.log("Successfully attached to feed in room " + msg["room"]);
|
|
} else if(event === "event") {
|
|
// Check if we got an event on a simulcast-related event from this publisher
|
|
let mid = msg["mid"];
|
|
let substream = msg["substream"];
|
|
let temporal = msg["temporal"];
|
|
if((substream !== null && substream !== undefined) || (temporal !== null && temporal !== undefined)) {
|
|
// Check which this feed this refers to
|
|
let slot = slots[mid];
|
|
if(!simulcastStarted[slot]) {
|
|
simulcastStarted[slot] = true;
|
|
// Add some new buttons
|
|
addSimulcastSvcButtons(slot, true);
|
|
}
|
|
// We just received notice that there's been a switch, update the buttons
|
|
updateSimulcastSvcButtons(slot, substream, temporal);
|
|
}
|
|
// Or maybe SVC?
|
|
let spatial = msg["spatial_layer"];
|
|
temporal = msg["temporal_layer"];
|
|
if((spatial !== null && spatial !== undefined) || (temporal !== null && temporal !== undefined)) {
|
|
let slot = slots[mid];
|
|
if(!svcStarted[slot]) {
|
|
svcStarted[slot] = true;
|
|
// Add some new buttons
|
|
addSimulcastSvcButtons(slot, true);
|
|
}
|
|
// We just received notice that there's been a switch, update the buttons
|
|
updateSimulcastSvcButtons(slot, spatial, temporal);
|
|
}
|
|
} else {
|
|
// What has just happened?
|
|
}
|
|
}
|
|
if(msg["streams"]) {
|
|
// Update map of subscriptions by mid
|
|
for(let i in msg["streams"]) {
|
|
let mid = msg["streams"][i]["mid"];
|
|
subStreams[mid] = msg["streams"][i];
|
|
let feed = feedStreams[msg["streams"][i]["feed_id"]];
|
|
if(feed && feed.slot) {
|
|
slots[mid] = feed.slot;
|
|
mids[feed.slot] = mid;
|
|
}
|
|
}
|
|
}
|
|
if(jsep) {
|
|
Janus.debug("Handling SDP as well...", jsep);
|
|
// Answer and attach
|
|
remoteFeed.createAnswer(
|
|
{
|
|
jsep: jsep,
|
|
// We only specify data channels here, as this way in
|
|
// case they were offered we'll enable them. Since we
|
|
// don't mention audio or video tracks, we autoaccept them
|
|
// as recvonly (since we won't capture anything ourselves)
|
|
tracks: [
|
|
{ type: 'data' }
|
|
],
|
|
success: function(jsep) {
|
|
Janus.debug("Got SDP!");
|
|
Janus.debug(jsep);
|
|
let body = { request: "start", room: myroom };
|
|
remoteFeed.send({ message: body, jsep: jsep });
|
|
},
|
|
error: function(error) {
|
|
Janus.error("WebRTC error:", error);
|
|
bootbox.alert("WebRTC error... " + error.message);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
// eslint-disable-next-line no-unused-vars
|
|
onlocaltrack: function(track, on) {
|
|
// The subscriber stream is recvonly, we don't expect anything here
|
|
},
|
|
onremotetrack: function(track, mid, on, metadata) {
|
|
Janus.debug(
|
|
"Remote track (mid=" + mid + ") " +
|
|
(on ? "added" : "removed") +
|
|
(metadata ? " (" + metadata.reason + ") ": "") + ":", track
|
|
);
|
|
// Which publisher are we getting on this mid?
|
|
let sub = subStreams[mid];
|
|
let feed = feedStreams[sub.feed_id];
|
|
Janus.debug(" >> This track is coming from feed " + sub.feed_id + ":", feed);
|
|
let slot = slots[mid];
|
|
if(feed && !slot) {
|
|
slot = feed.slot;
|
|
slots[mid] = feed.slot;
|
|
mids[feed.slot] = mid;
|
|
}
|
|
Janus.debug(" >> mid " + mid + " is in slot " + slot);
|
|
if(!on) {
|
|
// Track removed, get rid of the stream and the rendering
|
|
$('#remotevideo' + slot + '-' + mid).remove();
|
|
if(track.kind === "video" && feed) {
|
|
feed.remoteVideos--;
|
|
if(feed.remoteVideos === 0) {
|
|
// No video, at least for now: show a placeholder
|
|
if($('#videoremote' + slot + ' .no-video-container').length === 0) {
|
|
$('#videoremote' + slot).append(
|
|
'<div class="no-video-container">' +
|
|
'<span class="no-video-text">No remote video available</span>' +
|
|
'</div>');
|
|
}
|
|
}
|
|
}
|
|
delete remoteTracks[mid];
|
|
delete slots[mid];
|
|
delete mids[slot];
|
|
return;
|
|
}
|
|
// If we're here, a new track was added
|
|
if($('#remotevideo' + slot + '-' + mid).length > 0)
|
|
return;
|
|
if(track.kind === "audio") {
|
|
// New audio track: create a stream out of it, and use a hidden <audio> element
|
|
let stream = new MediaStream([track]);
|
|
remoteTracks[mid] = stream;
|
|
Janus.log("Created remote audio stream:", stream);
|
|
$('#videoremote' + slot).append('<audio class="hide" id="remotevideo' + slot + '-' + mid + '" autoplay playsinline/>');
|
|
Janus.attachMediaStream($('#remotevideo' + slot + '-' + mid).get(0), stream);
|
|
if(feed.remoteVideos === 0) {
|
|
// No video, at least for now: show a placeholder
|
|
if($('#videoremote' + slot + ' .no-video-container').length === 0) {
|
|
$('#videoremote' + slot).append(
|
|
'<div class="no-video-container">' +
|
|
'<span class="no-video-text">No remote video available</span>' +
|
|
'</div>');
|
|
}
|
|
}
|
|
} else {
|
|
// New video track: create a stream out of it
|
|
feed.remoteVideos++;
|
|
$('#videoremote' + slot + ' .no-video-container').remove();
|
|
let stream = new MediaStream([track]);
|
|
remoteTracks[mid] = stream;
|
|
Janus.log("Created remote video stream:", stream);
|
|
$('#videoremote' + slot).append('<video class="rounded centered" id="remotevideo' + slot + '-' + mid + '" width=100% autoplay playsinline/>');
|
|
$('#videoremote' + slot).append(
|
|
'<span class="badge bg-primary hide resolution-label" id="curres'+slot+'"></span>' +
|
|
'<span class="badge bg-info hide bitrate-label" id="curbitrate'+slot+'"></span>');
|
|
Janus.attachMediaStream($('#remotevideo' + slot + '-' + mid).get(0), stream);
|
|
// Note: we'll need this for additional videos too
|
|
if(!bitrateTimer[slot]) {
|
|
$('#curbitrate' + slot).removeClass('hide');
|
|
bitrateTimer[slot] = setInterval(function() {
|
|
if(!$("#videoremote" + slot + ' video').get(0))
|
|
return;
|
|
// Display updated bitrate, if supported
|
|
let bitrate = remoteFeed.getBitrate(mid);
|
|
$('#curbitrate' + slot).text(bitrate);
|
|
// Check if the resolution changed too
|
|
let width = $("#videoremote" + slot + ' video').get(0).videoWidth;
|
|
let height = $("#videoremote" + slot + ' video').get(0).videoHeight;
|
|
if(width > 0 && height > 0) {
|
|
let res = width + 'x' + height;
|
|
if(simulcastStarted[slot])
|
|
res += ' (simulcast)';
|
|
else if(svcStarted[slot])
|
|
res += ' (SVC)';
|
|
$('#curres' + slot).removeClass('hide').text(res).removeClass('hide');
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
},
|
|
oncleanup: function() {
|
|
Janus.log(" ::: Got a cleanup notification (remote feed) :::");
|
|
for(let i=1;i<6;i++) {
|
|
$('#videoremote'+i).empty();
|
|
if(bitrateTimer[i])
|
|
clearInterval(bitrateTimer[i]);
|
|
bitrateTimer[i] = null;
|
|
feedStreams[i].simulcastStarted = false;
|
|
feedStreams[i].svcStarted = false;
|
|
feedStreams[i].remoteVideos = 0;
|
|
$('#simulcast'+i).remove();
|
|
}
|
|
remoteTracks = {};
|
|
}
|
|
});
|
|
}
|
|
|
|
function unsubscribeFrom(id) {
|
|
// Unsubscribe from this publisher
|
|
let feed = feedStreams[id];
|
|
if(!feed)
|
|
return;
|
|
Janus.debug("Feed " + id + " (" + feed.display + ") has left the room, detaching");
|
|
if(bitrateTimer[feed.slot])
|
|
clearInterval(bitrateTimer[feed.slot]);
|
|
bitrateTimer[feed.slot] = null;
|
|
$('#remote' + feed.slot).empty().addClass('hide');
|
|
$('#videoremote' + feed.slot).empty();
|
|
delete simulcastStarted[feed.slot];
|
|
delete svcStarted[feed.slot];
|
|
$('#simulcast' + feed.slot).remove();
|
|
delete feeds[feed.slot];
|
|
feeds.slot = 0;
|
|
delete feedStreams[id];
|
|
// Send an unsubscribe request
|
|
let unsubscribe = {
|
|
request: "unsubscribe",
|
|
streams: [{ feed: id }]
|
|
};
|
|
if(remoteFeed != null)
|
|
remoteFeed.send({ message: unsubscribe });
|
|
delete subscriptions[id];
|
|
}
|
|
|
|
// Helper to parse query string
|
|
function getQueryStringValue(name) {
|
|
name = name.replace(/[[]/, "\\[").replace(/[\]]/, "\\]");
|
|
let regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
|
results = regex.exec(location.search);
|
|
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
|
}
|
|
|
|
// Helper to escape XML tags
|
|
function escapeXmlTags(value) {
|
|
if(value) {
|
|
let escapedValue = value.replace(new RegExp('<', 'g'), '<');
|
|
escapedValue = escapedValue.replace(new RegExp('>', 'g'), '>');
|
|
return escapedValue;
|
|
}
|
|
}
|
|
|
|
// Helpers to create Simulcast- or SVC-related UI, if enabled
|
|
function addSimulcastSvcButtons(feed, temporal) {
|
|
let index = feed;
|
|
let simulcast = simulcastStarted[index];
|
|
let what = (simulcast ? 'simulcast' : 'SVC');
|
|
let layer = (simulcast ? 'substream' : 'layer');
|
|
$('#remote'+index).parent().append(
|
|
'<div id="simulcast'+index+'" class="btn-group-vertical btn-group-xs top-right">' +
|
|
' <div class="btn-group btn-group-xs d-flex simulcast-button-group">' +
|
|
' <button id="sl'+index+'-2" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Switch to higher quality">SL 2</button>' +
|
|
' <button id="sl'+index+'-1" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Switch to normal quality">SL 1</button>' +
|
|
' <button id="sl'+index+'-0" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Switch to lower quality">SL 0</button>' +
|
|
' </div>' +
|
|
' <div class="btn-group btn-group-xs d-flex hide simulcast-button-group">' +
|
|
' <button id="tl'+index+'-2" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Cap to temporal layer 2">TL 2</button>' +
|
|
' <button id="tl'+index+'-1" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Cap to temporal layer 1">TL 1</button>' +
|
|
' <button id="tl'+index+'-0" type="button" class="btn btn-primary" data-bs-toggle="tooltip" title="Cap to temporal layer 0">TL 0</button>' +
|
|
' </div>' +
|
|
'</div>'
|
|
);
|
|
if(simulcast && Janus.webRTCAdapter.browserDetails.browser !== "firefox") {
|
|
// Chromium-based browsers only have two temporal layers, when doing simulcast
|
|
$('#tl'+index+'-2').remove();
|
|
}
|
|
// Enable the simulcast selection buttons
|
|
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')
|
|
.unbind('click').click(function() {
|
|
let index = $(this).attr('id').split('sl')[1].split('-')[0];
|
|
toastr.info("Switching " + what + " " + layer + " (mid=" + mids[index] + "), wait for it... (lower quality)", null, {timeOut: 2000});
|
|
if(!$('#sl' + index + '-2').hasClass('btn-success'))
|
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(!$('#sl' + index + '-1').hasClass('btn-success'))
|
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
|
if(simulcastStarted[index])
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 0 }});
|
|
else
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], spatial_layer: 0 }});
|
|
});
|
|
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')
|
|
.unbind('click').click(function() {
|
|
let index = $(this).attr('id').split('sl')[1].split('-')[0];
|
|
toastr.info("Switching " + what + " " + layer + " (mid=" + mids[index] + "), wait for it... (normal quality)", null, {timeOut: 2000});
|
|
if(!$('#sl' + index + '-2').hasClass('btn-success'))
|
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
|
if(!$('#sl' + index + '-0').hasClass('btn-success'))
|
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(simulcastStarted[index])
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 1 }});
|
|
else
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], spatial_layer: 1 }});
|
|
});
|
|
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')
|
|
.unbind('click').click(function() {
|
|
let index = $(this).attr('id').split('sl')[1].split('-')[0];
|
|
toastr.info("Switching " + what + " " + layer + " (mid=" + mids[index] + "), wait for it... (higher quality)", null, {timeOut: 2000});
|
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
|
if(!$('#sl' + index + '-1').hasClass('btn-success'))
|
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(!$('#sl' + index + '-0').hasClass('btn-success'))
|
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(simulcastStarted[index])
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], substream: 2 }});
|
|
else
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], spatial_layer: 2 }});
|
|
});
|
|
if(!temporal) // No temporal layer support
|
|
return;
|
|
$('#tl' + index + '-0').parent().removeClass('hide');
|
|
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary')
|
|
.unbind('click').click(function() {
|
|
let index = $(this).attr('id').split('tl')[1].split('-')[0];
|
|
toastr.info("Capping " + what + " temporal layer (mid=" + mids[index] + "), wait for it... (lowest FPS)", null, {timeOut: 2000});
|
|
if(!$('#tl' + index + '-2').hasClass('btn-success'))
|
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(!$('#tl' + index + '-1').hasClass('btn-success'))
|
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
|
if(simulcastStarted[index])
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 0 }});
|
|
else
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal_layer: 0 }});
|
|
});
|
|
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary')
|
|
.unbind('click').click(function() {
|
|
let index = $(this).attr('id').split('tl')[1].split('-')[0];
|
|
toastr.info("Capping " + what + " temporal layer (mid=" + mids[index] + "), wait for it... (medium FPS)", null, {timeOut: 2000});
|
|
if(!$('#tl' + index + '-2').hasClass('btn-success'))
|
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-info');
|
|
if(!$('#tl' + index + '-0').hasClass('btn-success'))
|
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(simulcastStarted[index])
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 1 }});
|
|
else
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal_layer: 1 }});
|
|
});
|
|
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary')
|
|
.unbind('click').click(function() {
|
|
let index = $(this).attr('id').split('tl')[1].split('-')[0];
|
|
toastr.info("Capping " + what + " temporal layer (mid=" + mids[index] + "), wait for it... (highest FPS)", null, {timeOut: 2000});
|
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
|
|
if(!$('#tl' + index + '-1').hasClass('btn-success'))
|
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(!$('#tl' + index + '-0').hasClass('btn-success'))
|
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info').addClass('btn-primary');
|
|
if(simulcastStarted[index])
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal: 2 }});
|
|
else
|
|
remoteFeed.send({ message: { request: "configure", mid: mids[index], temporal_layer: 2 }});
|
|
});
|
|
}
|
|
|
|
function updateSimulcastSvcButtons(feed, substream, temporal) {
|
|
// Check the substream
|
|
let index = feed;
|
|
let simulcast = simulcastStarted[index];
|
|
let what = (simulcast ? 'simulcast' : 'SVC');
|
|
let layer = (simulcast ? 'substream' : 'layer');
|
|
if(substream === 0) {
|
|
toastr.success("Switched " + what + " " + layer + "! (lower quality)", null, {timeOut: 2000});
|
|
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#sl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
|
} else if(substream === 1) {
|
|
toastr.success("Switched " + what + " " + layer + "! (normal quality)", null, {timeOut: 2000});
|
|
$('#sl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#sl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
|
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
} else if(substream === 2) {
|
|
toastr.success("Switched " + what + " " + layer + "! (higher quality)", null, {timeOut: 2000});
|
|
$('#sl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
|
$('#sl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#sl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
}
|
|
// Check the temporal layer
|
|
if(temporal === 0) {
|
|
toastr.success("Capped " + what + " temporal layer! (lowest FPS)", null, {timeOut: 2000});
|
|
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#tl' + index + '-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
|
} else if(temporal === 1) {
|
|
toastr.success("Capped " + what + " temporal layer! (medium FPS)", null, {timeOut: 2000});
|
|
$('#tl' + index + '-2').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#tl' + index + '-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
|
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
} else if(temporal === 2) {
|
|
toastr.success("Capped " + what + " temporal layer! (highest FPS)", null, {timeOut: 2000});
|
|
$('#tl' + index + '-2').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
|
|
$('#tl' + index + '-1').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
$('#tl' + index + '-0').removeClass('btn-primary btn-success').addClass('btn-primary');
|
|
}
|
|
}
|