// We import the settings.js file to know which address we should contact
// to talk to Janus, and optionally which STUN/TURN servers should be
// used as well. Specifically, that file defines the "server" and
// "iceServers" properties we'll pass when creating the Janus session.
/* global iceServers:readonly, Janus:readonly, server:readonly */
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 demo
$("#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,
// Should the Janus API require authentication, you can specify either the API secret or user token here too
// token: "mytoken",
// or
// apisecret: "serversecret",
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"
);
if (on) {
// Darken screen and show hint
$.blockUI({
message: '
',
baseZ: 3001,
css: {
border: "none",
padding: "15px",
backgroundColor: "transparent",
color: "#aaa",
top: "10px",
left: "100px",
},
});
} else {
// Restore screen
$.unblockUI();
}
},
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(
"
Apparently room " +
myroom +
" (the one this demo uses as a test room) " +
"does not exist...
Do you have an updated janus.plugin.videoroom.cfg " +
"configuration file? If not, make sure you copy the details of room " +
myroom +
" " +
"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(
'
' +
'' +
'Video rejected, no webcam' +
"
"
);
}
}
},
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(
'
' +
'' +
'No webcam available' +
"
"
);
}
}
}
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(
''
);
$("#mute").click(toggleMute);
// Add an 'unpublish' button
$("#videolocal").append(
''
);
$("#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(
'
' +
'' +
'No webcam available' +
"
"
);
}
}
} 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(
''
);
Janus.attachMediaStream(
$("#myvideo" + trackId).get(0),
stream
);
}
if (
sfutest.webrtcStuff.pc.iceConnectionState !== "completed" &&
sfutest.webrtcStuff.pc.iceConnectionState !== "connected"
) {
$("#videolocal")
.parent()
.parent()
.block({
message: "Publishing...",
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(
''
);
$("#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(
'
' +
'' +
'No remote video available' +
"
"
);
}
}
}
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