// 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 localTracks = {},
localVideos = 0;
var feeds = [],
feedStreams = {};
var bitrateTimer = [];
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 doDtx =
getQueryStringValue("dtx") === "yes" || getQueryStringValue("dtx") === "true";
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 VideoRoom 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").removeClass("hide");
$("#registernow").removeClass("hide").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")
.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) {
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").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
);
for (let f in list) {
if (list[f]["dummy"]) continue;
let id = list[f]["id"];
let streams = list[f]["streams"];
let display = list[f]["display"];
for (let i in streams) {
let stream = streams[i];
stream["id"] = id;
stream["display"] = display;
}
feedStreams[id] = streams;
Janus.debug(
" >> [" + id + "] " + display + ":",
streams
);
newRemoteFeed(id, display, streams);
}
}
} 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] = streams;
} else if (msg["publishers"]) {
let list = msg["publishers"];
Janus.debug(
"Got a list of available publishers/feeds:",
list
);
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;
}
feedStreams[id] = streams;
Janus.debug(
" >> [" + id + "] " + display + ":",
streams
);
newRemoteFeed(id, display, streams);
}
} else if (msg["leaving"]) {
// One of the publishers has gone away?
let leaving = msg["leaving"];
Janus.log("Publisher left: " + leaving);
let remoteFeed = null;
for (let i = 1; i < 6; i++) {
if (feeds[i] && feeds[i].rfid == leaving) {
remoteFeed = feeds[i];
break;
}
}
if (remoteFeed) {
Janus.debug(
"Feed " +
remoteFeed.rfid +
" (" +
remoteFeed.rfdisplay +
") has left the room, detaching"
);
$("#remote" + remoteFeed.rfindex)
.empty()
.addClass("hide");
$("#videoremote" + remoteFeed.rfindex).empty();
feeds[remoteFeed.rfindex] = null;
remoteFeed.detach();
}
delete feedStreams[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;
}
let remoteFeed = null;
for (let i = 1; i < 6; i++) {
if (feeds[i] && feeds[i].rfid == unpublished) {
remoteFeed = feeds[i];
break;
}
}
if (remoteFeed) {
Janus.debug(
"Feed " +
remoteFeed.rfid +
" (" +
remoteFeed.rfdisplay +
") has left the room, detaching"
);
$("#remote" + remoteFeed.rfindex)
.empty()
.addClass("hide");
$("#videoremote" + remoteFeed.rfindex).empty();
feeds[remoteFeed.rfindex] = null;
remoteFeed.detach();
}
delete feedStreams[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.jcfg " +
"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(
"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 !== null && mst !== undefined) 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").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();
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,
customizeSdp: function (jsep) {
// If DTX is enabled, munge the SDP
if (doDtx) {
jsep.sdp = jsep.sdp.replace(
"useinbandfec=1",
"useinbandfec=1;usedtx=1"
);
}
},
success: function (jsep) {
Janus.debug("Got publisher SDP!", 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.jcfg for more details.
// We allow people to specify a codec via query string, for demo purposes
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 });
}
// eslint-disable-next-line no-unused-vars
function newRemoteFeed(id, display, streams) {
// A new feed has been published, create a new plugin handle and attach to it as a subscriber
let remoteFeed = null;
if (!streams) streams = feedStreams[id];
janus.attach({
plugin: "janus.plugin.videoroom",
opaqueId: opaqueId,
success: function (pluginHandle) {
remoteFeed = pluginHandle;
remoteFeed.remoteTracks = {};
remoteFeed.remoteVideos = 0;
remoteFeed.simulcastStarted = false;
remoteFeed.svcStarted = false;
Janus.log(
"Plugin attached! (" +
remoteFeed.getPlugin() +
", id=" +
remoteFeed.getId() +
")"
);
Janus.log(" -- This is a 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 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;
}
subscription.push({
feed: stream.id, // This is mandatory
mid: stream.mid, // This is optional (all streams, if missing)
});
// FIXME Right now, this is always the same feed: in the future, it won't
remoteFeed.rfid = stream.id;
remoteFeed.rfdisplay = escapeXmlTags(stream.display);
}
// 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 (feed #" + remoteFeed.rfindex + ") changed to " + state
);
},
webrtcState: function (on) {
Janus.log(
"Janus says this WebRTC PeerConnection (feed #" +
remoteFeed.rfindex +
") 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") {
// Subscriber created and attached
for (let i = 1; i < 6; i++) {
if (!feeds[i]) {
feeds[i] = remoteFeed;
remoteFeed.rfindex = i;
break;
}
}
Janus.log("Successfully attached to feed in room " + msg["room"]);
$("#remote" + remoteFeed.rfindex)
.removeClass("hide")
.html(remoteFeed.rfdisplay)
.removeClass("hide");
} else if (event === "event") {
// Check if we got a simulcast-related event from this publisher
let substream = msg["substream"];
let temporal = msg["temporal"];
if (
(substream !== null && substream !== undefined) ||
(temporal !== null && temporal !== undefined)
) {
if (!remoteFeed.simulcastStarted) {
remoteFeed.simulcastStarted = true;
// Add some new buttons
addSimulcastSvcButtons(remoteFeed.rfindex, true);
}
// We just received notice that there's been a switch, update the buttons
updateSimulcastSvcButtons(remoteFeed.rfindex, substream, temporal);
}
// Or maybe SVC?
let spatial = msg["spatial_layer"];
temporal = msg["temporal_layer"];
if (
(spatial !== null && spatial !== undefined) ||
(temporal !== null && temporal !== undefined)
) {
if (!remoteFeed.svcStarted) {
remoteFeed.svcStarted = true;
// Add some new buttons
addSimulcastSvcButtons(remoteFeed.rfindex, true);
}
// We just received notice that there's been a switch, update the buttons
updateSimulcastSvcButtons(remoteFeed.rfindex, spatial, temporal);
}
} else {
// What has just happened?
}
}
if (jsep) {
Janus.debug("Handling SDP as well...", jsep);
let stereo = jsep.sdp.indexOf("stereo=1") !== -1;
// 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" }],
customizeSdp: function (jsep) {
if (stereo && jsep.sdp.indexOf("stereo=1") == -1) {
// Make sure that our offer contains stereo too
jsep.sdp = jsep.sdp.replace(
"useinbandfec=1",
"useinbandfec=1;stereo=1"
);
}
},
success: function (jsep) {
Janus.debug("Got SDP!", 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 feed #" +
remoteFeed.rfindex +
", remote track (mid=" +
mid +
") " +
(on ? "added" : "removed") +
(metadata ? " (" + metadata.reason + ") " : "") +
":",
track
);
if (!on) {
// Track removed, get rid of the stream and the rendering
$("#remotevideo" + remoteFeed.rfindex + "-" + mid).remove();
if (track.kind === "video") {
remoteFeed.remoteVideos--;
if (remoteFeed.remoteVideos === 0) {
// No video, at least for now: show a placeholder
if (
$("#videoremote" + remoteFeed.rfindex + " .no-video-container")
.length === 0
) {
$("#videoremote" + remoteFeed.rfindex).append(
'
' +
'' +
'No remote video available' +
"
"
);
}
}
}
delete remoteFeed.remoteTracks[mid];
return;
}
// If we're here, a new track was added
if ($("#remotevideo" + remoteFeed.rfindex + "-" + mid).length > 0) return;
if (track.kind === "audio") {
// New audio track: create a stream out of it, and use a hidden