mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
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');
|
|
}
|
|
}
|