[Pkg-voip-commits] [janus] 01/163: First integration of VP9 SVC support in the VideoRoom plugin
Jonas Smedegaard
dr at jones.dk
Sat Oct 28 01:22:02 UTC 2017
This is an automated email from the git hooks/post-receive script.
js pushed a commit to annotated tag debian/0.2.5-1
in repository janus.
commit 242e4f3177af3e4514b6410354b53b7e7cb5a959
Author: Lorenzo Miniero <lminiero at gmail.com>
Date: Wed Jun 14 17:42:55 2017 +0200
First integration of VP9 SVC support in the VideoRoom plugin
---
conf/janus.plugin.videoroom.cfg.sample | 16 +
html/demos.html | 20 +-
html/navbar.html | 1 +
html/vp9svctest.html | 234 +++++++++++++
html/vp9svctest.js | 579 +++++++++++++++++++++++++++++++++
plugins/janus_videoroom.c | 335 ++++++++++++++++++-
6 files changed, 1174 insertions(+), 11 deletions(-)
diff --git a/conf/janus.plugin.videoroom.cfg.sample b/conf/janus.plugin.videoroom.cfg.sample
index 1c5f911..e563135 100644
--- a/conf/janus.plugin.videoroom.cfg.sample
+++ b/conf/janus.plugin.videoroom.cfg.sample
@@ -11,6 +11,7 @@
; fir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)
; audiocodec = opus|isac32|isac16|pcmu|pcma (audio codec to force on publishers, default=opus)
; videocodec = vp8|vp9|h264 (video codec to force on publishers, default=vp8)
+; video_svc = yes|no (whether SVC support must be enabled; works only for VP9, default=no)
; audiolevel_ext = yes|no (whether the ssrc-audio-level RTP extension must
; be negotiated/used or not for new publishers, default=yes)
; audiolevel_event = yes|no (whether to emit event to other users or not, default=no)
@@ -39,3 +40,18 @@ fir_freq = 10
;videocodec = vp8
record = false
;rec_dir = /path/to/recordings-folder
+
+
+; This other demo room here is only there in case you want to play with
+; the VP9 SVC support. Notice that you'll need a Chrome launched with
+; the flag that enables that support, or otherwise you'll be using just
+; plain VP9 (which is good if you want to test how this indeed affect
+; what receivers will get, whether they're encoding SVC or not).
+[5678]
+description = VP9-SVC Demo Room
+secret = adminpwd
+publishers = 6
+bitrate = 1024000
+fir_freq = 10
+videocodec = vp9
+video_svc = true
diff --git a/html/demos.html b/html/demos.html
index 1b9214e..0a73be8 100644
--- a/html/demos.html
+++ b/html/demos.html
@@ -32,11 +32,11 @@
<div class="page-header">
<h1>Janus WebRTC Gateway: Demo Tests</h1>
</div>
- <table class="table">
+ <table class="table table-striped">
<tr>
<td colspan=2><h3>Plugin demos</h3></td>
</tr>
- <tr class="active">
+ <tr>
<td><a href="echotest.html">Echo Test</a></td>
<td>A simple Echo Test demo, with knobs to control the bitrate.</td>
</tr>
@@ -44,7 +44,7 @@
<td><a href="streamingtest.html">Streaming</a></td>
<td>A media Streaming demo, with sample live and on-demand streams.</td>
</tr>
- <tr class="active">
+ <tr>
<td><a href="videocalltest.html">Video Call</a></td>
<td>A Video Call demo, a bit like AppRTC but with media passing through the gateway.</td>
</tr>
@@ -52,7 +52,7 @@
<td><a href="siptest.html">SIP Gateway</a></td>
<td>A SIP Gateway demo, allowing you to register at a SIP server and start/receive calls.</td>
</tr>
- <tr class="active">
+ <tr>
<td><a href="videoroomtest.html">Video Room</a></td>
<td>A videoconferencing demo, allowing you to join a video room with up to six users.</td>
</tr>
@@ -60,7 +60,7 @@
<td><a href="audiobridgetest.html">Audio Room</a></td>
<td>An audio mixing/bridge demo, allowing you join an Audio Room room.</td>
</tr>
- <tr class="active">
+ <tr>
<td><a href="textroomtest.html">Text Room</a></td>
<td>A text room demo, using DataChannels only.</td>
</tr>
@@ -68,7 +68,7 @@
<td><a href="voicemailtest.html">Voice Mail</a></td>
<td>A simple audio recorder demo, returning an .opus file after 10 seconds.</td>
</tr>
- <tr class="active">
+ <tr>
<td><a href="recordplaytest.html">Recorder/Playout</a></td>
<td>A demo to record audio/video messages, and subsequently replay them through WebRTC.</td>
</tr>
@@ -77,15 +77,19 @@
<td>A webinar-like screen sharing session, based on the Video Room plugin.</td>
</tr>
</table>
- <table class="table">
+ <table class="table table-striped">
<tr>
<td colspan=2><h3>Other demos</h3></td>
</tr>
- <tr class="active">
+ <tr>
<td><a href="devicetest.html">Device Selection</a></td>
<td>A variant of the Echo Test demo, that allows you to choose a specific capture device.</td>
</tr>
<tr>
+ <td><a href="vp9svctest.html">VP9-SVC Video Room</a></td>
+ <td>A variant of the Video Room demo, that allows you to test the VP9 SVC layer selection, if available.</td>
+ </tr>
+ <tr>
<td><a href="admin.html">Admin/Monitor</a></td>
<td>A simple page showcasing how you can use the Janus Admin/Monitor API.</td>
</tr>
diff --git a/html/navbar.html b/html/navbar.html
index ee8a005..521f311 100644
--- a/html/navbar.html
+++ b/html/navbar.html
@@ -26,6 +26,7 @@
<li><a href="screensharingtest.html">Screen Sharing</a></li>
<li class="divider"></li>
<li><a href="devicetest.html">Device Selection</a></li>
+ <li><a href="vp9svctest.html">VP9-SVC Video Room</a></li>
<li class="divider"></li>
<li><a href="admin.html">Admin/Monitor</a></li>
</ul>
diff --git a/html/vp9svctest.html b/html/vp9svctest.html
new file mode 100644
index 0000000..24c7503
--- /dev/null
+++ b/html/vp9svctest.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Janus WebRTC Gateway: VP9-SVC Video Room Demo</title>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/3.4.3/adapter.min.js" ></script>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.2/jquery.min.js" ></script>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js" ></script>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/js/bootstrap.min.js"></script>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.1.0/bootbox.min.js"></script>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js"></script>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js"></script>
+<script type="text/javascript" src="janus.js" ></script>
+<script type="text/javascript" src="vp9svctest.js"></script>
+<script>
+ $(function() {
+ $(".navbar-static-top").load("navbar.html", function() {
+ $(".navbar-static-top li.dropdown").addClass("active");
+ $(".navbar-static-top a[href='vp9svctest.html']").parent().addClass("active");
+ });
+ $(".footer").load("footer.html");
+ });
+</script>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/cerulean/bootstrap.min.css" type="text/css"/>
+<link rel="stylesheet" href="css/demo.css" type="text/css"/>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.2/css/font-awesome.min.css"/>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.css"/>
+</head>
+<body>
+
+<a href="https://github.com/meetecho/janus-gateway"><img style="position: absolute; top: 0; left: 0; border: 0; z-index: 1001;" src="https://s3.amazonaws.com/github/ribbons/forkme_left_darkblue_121621.png" alt="Fork me on GitHub"></a>
+
+<nav class="navbar navbar-default navbar-static-top">
+</nav>
+
+<div class="container">
+ <div class="row">
+ <div class="col-md-12">
+ <div class="page-header">
+ <h1>Plugin Demo: VP9-SVC Video Room
+ <button class="btn btn-default" autocomplete="off" id="start">Start</button>
+ </h1>
+ </div>
+ <div class="container" id="details">
+ <div class="row">
+ <div class="col-md-12">
+ <h3>Demo details</h3>
+ <p>This is basically a clone of the plain <a href="videoroomtest.hmtl">Video Room</a>
+ demo, but with a key difference: it forces VP9 on all publishers, and supports
+ the VP9 SVC layer selection (if you don't know what this means, check this
+ <a target="_blank" href="https://webrtchacks.com/chrome-vp9-svc/">excellent blog post</a>).
+ As such, it will allow viewers to select which layer, spatial or temporal, to receive
+ from the publishers, in order to receive more or less data according to what they
+ can/want to get.</p>
+ <p>Notice that this only works if the publishers joining the room will use a recent
+ Chrome version that has been started with the following flag:</p>
+ <p><div class="alert alert-info"><code>--force-fieldtrials=WebRTC-SupportVP9SVC/EnabledByFlag_2SL3TL/</code></div></p>
+ <p>If you join with a Chrome that doesn't have that option set, or with
+ another browser, you'll be able to select a layer, but all request to set
+ a custom layer on your video will fail, meaning you'll act as a plain VP9 client.
+ If you ask for a layer from a publisher that doesn't support VP9 SVC either,
+ then your request will be ignored as well.</p>
+ <p>In case some publishers do support the VP9 SVC features, selecting a different
+ layer from them should result in variations of the video quality, and a notable
+ difference in the bandwidth used to receive their video as well.</p>
+ <p>Press the <code>Start</code> button above to launch the demo.</p>
+ </div>
+ </div>
+ </div>
+ <div class="container hide" id="videojoin">
+ <div class="row">
+ <span class="label label-info" id="you"></span>
+ <div class="col-md-12" id="controls">
+ <div class="input-group margin-bottom-md hide" id="registernow">
+ <span class="input-group-addon">@</span>
+ <input autocomplete="off" class="form-control" autocomplete="off" type="text" placeholder="Choose a display name" id="username" onkeypress="return checkEnter(this, event);"></input>
+ <span class="input-group-btn">
+ <button class="btn btn-success" autocomplete="off" id="register">Join the room</button>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="container hide" id="videos">
+ <div class="row">
+ <div class="col-md-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Local Video <span class="label label-primary hide" id="publisher"></span></h3>
+ </div>
+ <div class="panel-body" id="videolocal"></div>
+ </div>
+ </div>
+ <div class="col-md-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Remote Video #1
+ <span class="label label-info hide" id="remote1"></span>
+ <div id="layers1" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="sl1-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
+ <button id="sl1-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
+ </div>
+ </div>
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="tl1-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
+ <button id="tl1-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
+ <button id="tl1-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
+ </div>
+ </div>
+ </div>
+ </h3>
+ </div>
+ <div class="panel-body relative" id="videoremote1"></div>
+ </div>
+ </div>
+ <div class="col-md-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Remote Video #2
+ <span class="label label-info hide" id="remote2"></span>
+ <div id="layers2" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="sl2-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
+ <button id="sl2-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
+ </div>
+ </div>
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="tl2-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
+ <button id="tl2-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
+ <button id="tl2-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
+ </div>
+ </div>
+ </div>
+ </h3>
+ </div>
+ <div class="panel-body relative" id="videoremote2"></div>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Remote Video #3
+ <span class="label label-info hide" id="remote3"></span>
+ <div id="layers3" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="sl3-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
+ <button id="sl3-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
+ </div>
+ </div>
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="tl3-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
+ <button id="tl3-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
+ <button id="tl3-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
+ </div>
+ </div>
+ </div>
+ </h3>
+ </div>
+ <div class="panel-body relative" id="videoremote3"></div>
+ </div>
+ </div>
+ <div class="col-md-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Remote Video #4
+ <span class="label label-info hide" id="remote4"></span>
+ <div id="layers4" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="sl4-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
+ <button id="sl4-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
+ </div>
+ </div>
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="tl4-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
+ <button id="tl4-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
+ <button id="tl4-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
+ </div>
+ </div>
+ </div>
+ </h3>
+ </div>
+ <div class="panel-body relative" id="videoremote4"></div>
+ </div>
+ </div>
+ <div class="col-md-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">Remote Video #5
+ <span class="label label-info hide" id="remote5"></span>
+ <div id="layers5" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="sl5-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
+ <button id="sl5-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
+ </div>
+ </div>
+ <div class"row">
+ <div class="btn-group btn-group-xs" style="width: 100%">
+ <button id="tl5-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
+ <button id="tl5-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
+ <button id="tl5-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
+ </div>
+ </div>
+ </div>
+ </h3>
+ </div>
+ <div class="panel-body relative" id="videoremote5"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <hr>
+ <div class="footer">
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/html/vp9svctest.js b/html/vp9svctest.js
new file mode 100644
index 0000000..a9e8b59
--- /dev/null
+++ b/html/vp9svctest.js
@@ -0,0 +1,579 @@
+// We make use of this 'server' variable to provide the address of the
+// REST Janus API. By default, in this example we assume that Janus is
+// co-located with the web server hosting the HTML pages but listening
+// on a different port (8088, the default for HTTP in Janus), which is
+// why we make use of the 'window.location.hostname' base address. Since
+// Janus can also do HTTPS, and considering we don't really want to make
+// use of HTTP for Janus if your demos are served on HTTPS, we also rely
+// on the 'window.location.protocol' prefix to build the variable, in
+// particular to also change the port used to contact Janus (8088 for
+// HTTP and 8089 for HTTPS, if enabled).
+// In case you place Janus behind an Apache frontend (as we did on the
+// online demos at http://janus.conf.meetecho.com) you can just use a
+// relative path for the variable, e.g.:
+//
+// var server = "/janus";
+//
+// which will take care of this on its own.
+//
+//
+// If you want to use the WebSockets frontend to Janus, instead, you'll
+// have to pass a different kind of address, e.g.:
+//
+// var server = "ws://" + window.location.hostname + ":8188";
+//
+// Of course this assumes that support for WebSockets has been built in
+// when compiling the gateway. WebSockets support has not been tested
+// as much as the REST API, so handle with care!
+//
+//
+// If you have multiple options available, and want to let the library
+// autodetect the best way to contact your gateway (or pool of gateways),
+// you can also pass an array of servers, e.g., to provide alternative
+// means of access (e.g., try WebSockets first and, if that fails, fall
+// back to plain HTTP) or just have failover servers:
+//
+// var server = [
+// "ws://" + window.location.hostname + ":8188",
+// "/janus"
+// ];
+//
+// This will tell the library to try connecting to each of the servers
+// in the presented order. The first working server will be used for
+// the whole session.
+//
+var server = null;
+if(window.location.protocol === 'http:')
+ server = "http://" + window.location.hostname + ":8088/janus";
+else
+ server = "https://" + window.location.hostname + ":8089/janus";
+
+var janus = null;
+var sfutest = null;
+var opaqueId = "videoroomtest-"+Janus.randomString(12);
+
+var started = false;
+
+var myroom = 5678;
+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 feeds = [];
+var bitrateTimer = [];
+
+
+$(document).ready(function() {
+ // Initialize the library (all console debuggers enabled)
+ Janus.init({debug: "all", callback: function() {
+ // Use a button to start the demo
+ $('#start').click(function() {
+ if(started)
+ return;
+ started = true;
+ $(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,
+ 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').show();
+ $('#registernow').removeClass('hide').show();
+ $('#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: '<div><img src="up_arrow.png"/></div>',
+ css: {
+ border: 'none',
+ padding: '15px',
+ backgroundColor: 'transparent',
+ color: '#aaa',
+ top: '10px',
+ left: (navigator.mozGetUserMedia ? '-100px' : '300px')
+ } });
+ } else {
+ // Restore screen
+ $.unblockUI();
+ }
+ },
+ mediaState: function(medium, on) {
+ Janus.log("Janus " + (on ? "started" : "stopped") + " receiving our " + medium);
+ },
+ webrtcState: function(on) {
+ Janus.log("Janus says our WebRTC PeerConnection is " + (on ? "up" : "down") + " now");
+ $("#videolocal").parent().parent().unblock();
+ },
+ onmessage: function(msg, jsep) {
+ Janus.debug(" ::: Got a message (publisher) :::");
+ Janus.debug(JSON.stringify(msg));
+ var 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);
+ publishOwnFeed(true);
+ // Any new feed to attach to?
+ if(msg["publishers"] !== undefined && msg["publishers"] !== null) {
+ var list = msg["publishers"];
+ Janus.debug("Got a list of available publishers/feeds:");
+ Janus.debug(list);
+ for(var f in list) {
+ var id = list[f]["id"];
+ var display = list[f]["display"];
+ Janus.debug(" >> [" + id + "] " + display);
+ newRemoteFeed(id, display)
+ }
+ }
+ } 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 new feed to attach to?
+ if(msg["publishers"] !== undefined && msg["publishers"] !== null) {
+ var list = msg["publishers"];
+ Janus.debug("Got a list of available publishers/feeds:");
+ Janus.debug(list);
+ for(var f in list) {
+ var id = list[f]["id"];
+ var display = list[f]["display"];
+ Janus.debug(" >> [" + id + "] " + display);
+ newRemoteFeed(id, display)
+ }
+ } else if(msg["leaving"] !== undefined && msg["leaving"] !== null) {
+ // One of the publishers has gone away?
+ var leaving = msg["leaving"];
+ Janus.log("Publisher left: " + leaving);
+ var remoteFeed = null;
+ for(var i=1; i<6; i++) {
+ if(feeds[i] != null && feeds[i] != undefined && feeds[i].rfid == leaving) {
+ remoteFeed = feeds[i];
+ break;
+ }
+ }
+ if(remoteFeed != null) {
+ Janus.debug("Feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") has left the room, detaching");
+ $('#remote'+remoteFeed.rfindex).empty().hide();
+ $('#videoremote'+remoteFeed.rfindex).empty();
+ feeds[remoteFeed.rfindex] = null;
+ remoteFeed.detach();
+ }
+ } else if(msg["unpublished"] !== undefined && msg["unpublished"] !== null) {
+ // One of the publishers has unpublished?
+ var unpublished = msg["unpublished"];
+ Janus.log("Publisher left: " + unpublished);
+ if(unpublished === 'ok') {
+ // That's us
+ sfutest.hangup();
+ return;
+ }
+ var remoteFeed = null;
+ for(var i=1; i<6; i++) {
+ if(feeds[i] != null && feeds[i] != undefined && feeds[i].rfid == unpublished) {
+ remoteFeed = feeds[i];
+ break;
+ }
+ }
+ if(remoteFeed != null) {
+ Janus.debug("Feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") has left the room, detaching");
+ $('#remote'+remoteFeed.rfindex).empty().hide();
+ $('#videoremote'+remoteFeed.rfindex).empty();
+ feeds[remoteFeed.rfindex] = null;
+ remoteFeed.detach();
+ }
+ } else if(msg["error"] !== undefined && msg["error"] !== null) {
+ 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 demo uses for testing VP9 SVC) " +
+ "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 !== undefined && jsep !== null) {
+ Janus.debug("Handling SDP as well...");
+ Janus.debug(jsep);
+ sfutest.handleRemoteJsep({jsep: jsep});
+ }
+ },
+ onlocalstream: function(stream) {
+ Janus.debug(" ::: Got a local stream :::");
+ mystream = stream;
+ Janus.debug(JSON.stringify(stream));
+ $('#videolocal').empty();
+ $('#videojoin').hide();
+ $('#videos').removeClass('hide').show();
+ if($('#myvideo').length === 0) {
+ $('#videolocal').append('<video class="rounded centered" id="myvideo" width="100%" height="100%" autoplay muted="muted"/>');
+ // Add a 'mute' button
+ $('#videolocal').append('<button class="btn btn-warning btn-xs" id="mute" style="position: absolute; bottom: 0px; left: 0px; margin: 15px;">Mute</button>');
+ $('#mute').click(toggleMute);
+ // Add an 'unpublish' button
+ $('#videolocal').append('<button class="btn btn-warning btn-xs" id="unpublish" style="position: absolute; bottom: 0px; right: 0px; margin: 15px;">Unpublish</button>');
+ $('#unpublish').click(unpublishOwnFeed);
+ }
+ $('#publisher').removeClass('hide').html(myusername).show();
+ Janus.attachMediaStream($('#myvideo').get(0), stream);
+ $("#myvideo").get(0).muted = "muted";
+ $("#videolocal").parent().parent().block({
+ message: '<b>Publishing...</b>',
+ css: {
+ border: 'none',
+ backgroundColor: 'transparent',
+ color: 'white'
+ }
+ });
+ var videoTracks = stream.getVideoTracks();
+ if(videoTracks === null || videoTracks === undefined || videoTracks.length === 0) {
+ // No webcam
+ $('#myvideo').hide();
+ $('#videolocal').append(
+ '<div class="no-video-container">' +
+ '<i class="fa fa-video-camera fa-5 no-video-icon" style="height: 100%;"></i>' +
+ '<span class="no-video-text" style="font-size: 16px;">No webcam available</span>' +
+ '</div>');
+ }
+ },
+ onremotestream: function(stream) {
+ // 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;
+ $('#videolocal').html('<button id="publish" class="btn btn-primary">Publish</button>');
+ $('#publish').click(function() { publishOwnFeed(true); });
+ $("#videolocal").parent().parent().unblock();
+ }
+ });
+ },
+ error: function(error) {
+ Janus.error(error);
+ bootbox.alert(error, function() {
+ window.location.reload();
+ });
+ },
+ destroyed: function() {
+ window.location.reload();
+ }
+ });
+ });
+ }});
+});
+
+function checkEnter(field, event) {
+ var 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');
+ var username = $('#username').val();
+ if(username === "") {
+ $('#you')
+ .removeClass().addClass('label label-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('label label-warning')
+ .html('Input is not alphanumeric');
+ $('#username').removeAttr('disabled').val("");
+ $('#register').removeAttr('disabled').click(registerUsername);
+ return;
+ }
+ var register = { "request": "join", "room": myroom, "ptype": "publisher", "display": username };
+ myusername = username;
+ sfutest.send({"message": register});
+ }
+}
+
+function publishOwnFeed(useAudio) {
+ // Publish our stream
+ $('#publish').attr('disabled', true).unbind('click');
+ sfutest.createOffer(
+ {
+ // Add data:true here if you want to publish datachannels as well
+ media: { audioRecv: false, videoRecv: false, audioSend: useAudio, videoSend: true }, // Publishers are sendonly
+ success: function(jsep) {
+ Janus.debug("Got publisher SDP!");
+ Janus.debug(jsep);
+ var publish = { "request": "configure", "audio": useAudio, "video": true };
+ sfutest.send({"message": publish, "jsep": jsep});
+ },
+ error: function(error) {
+ Janus.error("WebRTC error:", error);
+ if (useAudio) {
+ publishOwnFeed(false);
+ } else {
+ bootbox.alert("WebRTC error... " + JSON.stringify(error));
+ $('#publish').removeAttr('disabled').click(function() { publishOwnFeed(true); });
+ }
+ }
+ });
+}
+
+function toggleMute() {
+ var 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');
+ var unpublish = { "request": "unpublish" };
+ sfutest.send({"message": unpublish});
+}
+
+function newRemoteFeed(id, display) {
+ // A new feed has been published, create a new plugin handle and attach to it as a listener
+ var remoteFeed = null;
+ janus.attach(
+ {
+ plugin: "janus.plugin.videoroom",
+ opaqueId: opaqueId,
+ success: function(pluginHandle) {
+ remoteFeed = pluginHandle;
+ Janus.log("Plugin attached! (" + remoteFeed.getPlugin() + ", id=" + remoteFeed.getId() + ")");
+ Janus.log(" -- This is a subscriber");
+ // We wait for the plugin to send us an offer
+ var listen = { "request": "join", "room": myroom, "ptype": "listener", "feed": id, "private_id": mypvtid };
+ remoteFeed.send({"message": listen});
+ },
+ error: function(error) {
+ Janus.error(" -- Error attaching plugin...", error);
+ bootbox.alert("Error attaching plugin... " + error);
+ },
+ onmessage: function(msg, jsep) {
+ Janus.debug(" ::: Got a message (listener) :::");
+ Janus.debug(JSON.stringify(msg));
+ var event = msg["videoroom"];
+ Janus.debug("Event: " + event);
+ if(event != undefined && event != null) {
+ if(event === "attached") {
+ // Subscriber created and attached
+ for(var i=1;i<6;i++) {
+ if(feeds[i] === undefined || feeds[i] === null) {
+ feeds[i] = remoteFeed;
+ remoteFeed.rfindex = i;
+ break;
+ }
+ }
+ remoteFeed.rfid = msg["id"];
+ remoteFeed.rfdisplay = msg["display"];
+ if(remoteFeed.spinner === undefined || remoteFeed.spinner === null) {
+ var target = document.getElementById('videoremote'+remoteFeed.rfindex);
+ remoteFeed.spinner = new Spinner({top:100}).spin(target);
+ } else {
+ remoteFeed.spinner.spin();
+ }
+ Janus.log("Successfully attached to feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") in room " + msg["room"]);
+ $('#remote'+remoteFeed.rfindex).removeClass('hide').html(remoteFeed.rfdisplay).show();
+ } else if(msg["error"] !== undefined && msg["error"] !== null) {
+ bootbox.alert(msg["error"]);
+ } else {
+ // What has just happened?
+ }
+ }
+ if(jsep !== undefined && jsep !== null) {
+ Janus.debug("Handling SDP as well...");
+ Janus.debug(jsep);
+ // Answer and attach
+ remoteFeed.createAnswer(
+ {
+ jsep: jsep,
+ // Add data:true here if you want to subscribe to datachannels as well
+ // (obviously only works if the publisher offered them in the first place)
+ media: { audioSend: false, videoSend: false }, // We want recvonly audio/video
+ success: function(jsep) {
+ Janus.debug("Got SDP!");
+ Janus.debug(jsep);
+ var body = { "request": "start", "room": myroom };
+ remoteFeed.send({"message": body, "jsep": jsep});
+ },
+ error: function(error) {
+ Janus.error("WebRTC error:", error);
+ bootbox.alert("WebRTC error... " + JSON.stringify(error));
+ }
+ });
+ }
+ },
+ webrtcState: function(on) {
+ Janus.log("Janus says this WebRTC PeerConnection (feed #" + remoteFeed.rfindex + ") is " + (on ? "up" : "down") + " now");
+ },
+ onlocalstream: function(stream) {
+ // The subscriber stream is recvonly, we don't expect anything here
+ },
+ onremotestream: function(stream) {
+ Janus.debug("Remote feed #" + remoteFeed.rfindex);
+ if($('#remotevideo'+remoteFeed.rfindex).length === 0) {
+ // No remote video yet
+ $('#videoremote'+remoteFeed.rfindex).append('<video class="rounded centered" id="waitingvideo' + remoteFeed.rfindex + '" width=320 height=240 />');
+ $('#videoremote'+remoteFeed.rfindex).append('<video class="rounded centered relative hide" id="remotevideo' + remoteFeed.rfindex + '" width="100%" height="100%" autoplay/>');
+ }
+ $('#videoremote'+remoteFeed.rfindex).append(
+ '<span class="label label-primary hide" id="curres'+remoteFeed.rfindex+'" style="position: absolute; bottom: 0px; left: 0px; margin: 15px;"></span>' +
+ '<span class="label label-info hide" id="curbitrate'+remoteFeed.rfindex+'" style="position: absolute; bottom: 0px; right: 0px; margin: 15px;"></span>');
+ // Show the video, hide the spinner and show the resolution when we get a playing event
+ $("#remotevideo"+remoteFeed.rfindex).bind("playing", function () {
+ if(remoteFeed.spinner !== undefined && remoteFeed.spinner !== null)
+ remoteFeed.spinner.stop();
+ remoteFeed.spinner = null;
+ $('#waitingvideo'+remoteFeed.rfindex).remove();
+ $('#remotevideo'+remoteFeed.rfindex).removeClass('hide');
+ var width = this.videoWidth;
+ var height = this.videoHeight;
+ $('#curres'+remoteFeed.rfindex).removeClass('hide').text(width+'x'+height).show();
+ // Enable the layer selection buttons
+ $('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-success')
+ .unbind('click').click(function() {
+ toastr.success("Selected spatial layer 1 of " + remoteFeed.rfdisplay + "'s video (normal resolution)", null, {timeOut: 2000});
+ $('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-success');
+ $('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary');
+ var body = { request: "configure", spatial_layer: 1};
+ remoteFeed.send({message: body});
+ });
+ $('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary')
+ .unbind('click').click(function() {
+ toastr.success("Selected spatial layer 0 of " + remoteFeed.rfdisplay + "'s video (smaller resolution)", null, {timeOut: 2000});
+ $('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary');
+ $('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-success');
+ var body = { request: "configure", spatial_layer: 0};
+ remoteFeed.send({message: body});
+ });
+ $('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-success')
+ .unbind('click').click(function() {
+ toastr.success("Selected temporal layer 2 of " + remoteFeed.rfdisplay + "'s video (normal FPS)", null, {timeOut: 2000});
+ $('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-success');
+ $('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary');
+ $('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary');
+ var body = { request: "configure", temporal_layer: 2};
+ remoteFeed.send({message: body});
+ });
+ $('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary')
+ .unbind('click').click(function() {
+ toastr.success("Selected temporal layer 1 of " + remoteFeed.rfdisplay + "'s video (low FPS)", null, {timeOut: 2000});
+ $('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-primary');
+ $('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-success');
+ $('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary');
+ var body = { request: "configure", temporal_layer: 1};
+ remoteFeed.send({message: body});
+ });
+ $('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary')
+ .unbind('click').click(function() {
+ toastr.success("Selected temporal layer 0 of " + remoteFeed.rfdisplay + "'s video (lowest FPS)", {timeOut: 2000});
+ $('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-primary');
+ $('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary');
+ $('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-success');
+ var body = { request: "configure", temporal_layer: 0};
+ remoteFeed.send({message: body});
+ });
+ $('#layers'+remoteFeed.rfindex).removeClass('hide');
+ });
+ Janus.attachMediaStream($('#remotevideo'+remoteFeed.rfindex).get(0), stream);
+ var videoTracks = stream.getVideoTracks();
+ if(videoTracks === null || videoTracks === undefined || videoTracks.length === 0 || videoTracks[0].muted) {
+ // No remote video
+ $('#remotevideo'+remoteFeed.rfindex).hide();
+ $('#videoremote'+remoteFeed.rfindex).append(
+ '<div class="no-video-container">' +
+ '<i class="fa fa-video-camera fa-5 no-video-icon" style="height: 100%;"></i>' +
+ '<span class="no-video-text" style="font-size: 16px;">No remote video available</span>' +
+ '</div>');
+ }
+ if(adapter.browserDetails.browser === "chrome" || adapter.browserDetails.browser === "firefox") {
+ $('#curbitrate'+remoteFeed.rfindex).removeClass('hide').show();
+ bitrateTimer[remoteFeed.rfindex] = setInterval(function() {
+ // Display updated bitrate, if supported
+ var bitrate = remoteFeed.getBitrate();
+ $('#curbitrate'+remoteFeed.rfindex).text(bitrate);
+ var width = $("#remotevideo"+remoteFeed.rfindex).get(0).videoWidth;
+ var height = $("#remotevideo"+remoteFeed.rfindex).get(0).videoHeight;
+ if(width > 0 && height > 0)
+ $('#curres'+remoteFeed.rfindex).removeClass('hide').text(width+'x'+height).show();
+ }, 1000);
+ }
+ },
+ oncleanup: function() {
+ Janus.log(" ::: Got a cleanup notification (remote feed " + id + ") :::");
+ if(remoteFeed.spinner !== undefined && remoteFeed.spinner !== null)
+ remoteFeed.spinner.stop();
+ remoteFeed.spinner = null;
+ $('#waitingvideo'+remoteFeed.rfindex).remove();
+ $('#curbitrate'+remoteFeed.rfindex).remove();
+ $('#curres'+remoteFeed.rfindex).remove();
+ if(bitrateTimer[remoteFeed.rfindex] !== null && bitrateTimer[remoteFeed.rfindex] !== null)
+ clearInterval(bitrateTimer[remoteFeed.rfindex]);
+ bitrateTimer[remoteFeed.rfindex] = null;
+ $('#sl'+remoteFeed.rfindex+'-1').unbind('click');
+ $('#sl'+remoteFeed.rfindex+'-0').unbind('click');
+ $('#tl'+remoteFeed.rfindex+'-2').unbind('click');
+ $('#tl'+remoteFeed.rfindex+'-1').unbind('click');
+ $('#tl'+remoteFeed.rfindex+'-0').unbind('click');
+ $('#layers'+remoteFeed.rfindex).addClass('hide');
+ }
+ });
+}
diff --git a/plugins/janus_videoroom.c b/plugins/janus_videoroom.c
index 68d7066..1334850 100644
--- a/plugins/janus_videoroom.c
+++ b/plugins/janus_videoroom.c
@@ -66,6 +66,7 @@ bitrate = <max video bitrate for senders> (e.g., 128000)
fir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)
audiocodec = opus|isac32|isac16|pcmu|pcma|g722 (audio codec to force on publishers, default=opus)
videocodec = vp8|vp9|h264 (video codec to force on publishers, default=vp8)
+video_svc = yes|no (whether SVC support must be enabled; works only for VP9, default=no)
audiolevel_ext = yes|no (whether the ssrc-audio-level RTP extension must be
negotiated/used or not for new publishers, default=yes)
audiolevel_event = yes|no (whether to emit event to other users or not)
@@ -148,8 +149,8 @@ rec_dir = <folder where recordings should be stored, when enabled>
/* Plugin information */
-#define JANUS_VIDEOROOM_VERSION 8
-#define JANUS_VIDEOROOM_VERSION_STRING "0.0.8"
+#define JANUS_VIDEOROOM_VERSION 9
+#define JANUS_VIDEOROOM_VERSION_STRING "0.0.9"
#define JANUS_VIDEOROOM_DESCRIPTION "This is a plugin implementing a videoconferencing SFU (Selective Forwarding Unit) for Janus, that is an audio/video router."
#define JANUS_VIDEOROOM_NAME "JANUS VideoRoom plugin"
#define JANUS_VIDEOROOM_AUTHOR "Meetecho s.r.l."
@@ -229,6 +230,7 @@ static struct janus_json_parameter create_parameters[] = {
{"publishers", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"audiocodec", JSON_STRING, 0},
{"videocodec", JSON_STRING, 0},
+ {"video_svc", JANUS_JSON_BOOL, 0},
{"audiolevel_ext", JANUS_JSON_BOOL, 0},
{"audiolevel_event", JANUS_JSON_BOOL, 0},
{"audio_active_packets", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
@@ -300,7 +302,9 @@ static struct janus_json_parameter publisher_parameters[] = {
static struct janus_json_parameter configure_parameters[] = {
{"audio", JANUS_JSON_BOOL, 0},
{"video", JANUS_JSON_BOOL, 0},
- {"data", JANUS_JSON_BOOL, 0}
+ {"data", JANUS_JSON_BOOL, 0},
+ {"spatial_layer", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
+ {"temporal_layer", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}
};
static struct janus_json_parameter listener_parameters[] = {
{"feed", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
@@ -447,6 +451,13 @@ static int janus_videoroom_videocodec_pt(janus_videoroom_videocodec vcodec) {
return VP8_PT;
}
}
+/* Helper method to parse an RTP video frame and get some SVC-related info
+ * (note: this only works with VP9, right now, on an experimental basis) */
+static int janus_videoroom_videocodec_parse_svc(janus_videoroom_videocodec vcodec,
+ char *buf, int total, int *found,
+ int *spatial_layer, int *temporal_layer,
+ uint8_t *p, uint8_t *d, uint8_t *u, uint8_t *b, uint8_t *e);
+
typedef struct janus_videoroom {
guint64 room_id; /* Unique room ID */
@@ -460,6 +471,7 @@ typedef struct janus_videoroom {
uint16_t fir_freq; /* Regular FIR frequency (0=disabled) */
janus_videoroom_audiocodec acodec; /* Audio codec to force on publishers*/
janus_videoroom_videocodec vcodec; /* Video codec to force on publishers*/
+ gboolean do_svc; /* Whether SVC must be done for video (note: only available for VP9 right now) */
gboolean audiolevel_ext; /* Whether the ssrc-audio-level extension must be negotiated or not for new publishers */
gboolean audiolevel_event; /* Whether to emit event to other users about audiolevel */
int audio_active_packets; /* amount of packets with audio level for checkup */
@@ -559,6 +571,9 @@ typedef struct janus_videoroom_listener {
gboolean audio, video, data; /* Whether audio, video and/or data must be sent to this publisher */
gboolean paused;
gboolean kicked; /* Whether this subscription belongs to a participant that has been kicked */
+ /* The following are only relevant if we're doing VP9 SVC*/
+ int spatial_layer, target_spatial_layer;
+ int temporal_layer, target_temporal_layer;
} janus_videoroom_listener;
static void janus_videoroom_listener_free(janus_videoroom_listener *l);
@@ -568,6 +583,11 @@ typedef struct janus_videoroom_rtp_relay_packet {
gboolean is_video;
uint32_t timestamp;
uint16_t seq_number;
+ /* The following are only relevant if we're doing VP9 SVC*/
+ gboolean svc;
+ int spatial_layer;
+ int temporal_layer;
+ uint8_t pbit, dbit, ubit, bbit, ebit;
} janus_videoroom_rtp_relay_packet;
@@ -785,6 +805,7 @@ int janus_videoroom_init(janus_callbacks *callback, const char *config_path) {
janus_config_item *firfreq = janus_config_get_item(cat, "fir_freq");
janus_config_item *audiocodec = janus_config_get_item(cat, "audiocodec");
janus_config_item *videocodec = janus_config_get_item(cat, "videocodec");
+ janus_config_item *svc = janus_config_get_item(cat, "video_svc");
janus_config_item *audiolevel_ext = janus_config_get_item(cat, "audiolevel_ext");
janus_config_item *audiolevel_event = janus_config_get_item(cat, "audiolevel_event");
janus_config_item *audio_active_packets = janus_config_get_item(cat, "audio_active_packets");
@@ -855,6 +876,14 @@ int janus_videoroom_init(janus_callbacks *callback, const char *config_path) {
videoroom->vcodec = JANUS_VIDEOROOM_VP8;
}
}
+ if(svc && svc->value && janus_is_true(svc->value)) {
+ if(videoroom->vcodec == JANUS_VIDEOROOM_VP9) {
+ videoroom->do_svc = TRUE;
+ } else {
+ JANUS_LOG(LOG_WARN, "SVC is only supported, in an experimental way, for VP9, not %s: disabling it...\n",
+ janus_videoroom_videocodec_name(videoroom->vcodec));
+ }
+ }
videoroom->audiolevel_ext = TRUE;
if(audiolevel_ext != NULL && audiolevel_ext->value != NULL)
videoroom->audiolevel_ext = janus_is_true(audiolevel_ext->value);
@@ -1185,6 +1214,14 @@ json_t *janus_videoroom_query_session(janus_plugin_session *handle) {
json_object_set_new(media, "video", json_integer(participant->video));
json_object_set_new(media, "data", json_integer(participant->data));
json_object_set_new(info, "media", media);
+ if(participant->room && participant->room->do_svc) {
+ json_t *svc = json_object();
+ json_object_set_new(svc, "spatial-layer", json_integer(participant->spatial_layer));
+ json_object_set_new(svc, "target-spatial-layer", json_integer(participant->target_spatial_layer));
+ json_object_set_new(svc, "temporal-layer", json_integer(participant->temporal_layer));
+ json_object_set_new(svc, "target-temporal-layer", json_integer(participant->target_temporal_layer));
+ json_object_set_new(info, "svc", svc);
+ }
}
}
}
@@ -1329,6 +1366,7 @@ struct janus_plugin_result *janus_videoroom_handle_message(janus_plugin_session
goto plugin_response;
}
}
+ json_t *svc = json_object_get(root, "video_svc");
json_t *audiolevel_ext = json_object_get(root, "audiolevel_ext");
json_t *audiolevel_event = json_object_get(root, "audiolevel_event");
json_t *audio_active_packets = json_object_get(root, "audio_active_packets");
@@ -1460,6 +1498,14 @@ struct janus_plugin_result *janus_videoroom_handle_message(janus_plugin_session
videoroom->vcodec = JANUS_VIDEOROOM_VP8;
}
}
+ if(svc && json_is_true(svc)) {
+ if(videoroom->vcodec == JANUS_VIDEOROOM_VP9) {
+ videoroom->do_svc = TRUE;
+ } else {
+ JANUS_LOG(LOG_WARN, "SVC is only supported, in an experimental way, for VP9, not %s: disabling it...\n",
+ janus_videoroom_videocodec_name(videoroom->vcodec));
+ }
+ }
videoroom->audiolevel_ext = audiolevel_ext ? json_is_true(audiolevel_ext) : TRUE;
videoroom->audiolevel_event = audiolevel_event ? json_is_true(audiolevel_event) : FALSE;
if(videoroom->audiolevel_event) {
@@ -1537,6 +1583,8 @@ struct janus_plugin_result *janus_videoroom_handle_message(janus_plugin_session
}
janus_config_add_item(config, cat, "audiocodec", janus_videoroom_audiocodec_name(videoroom->acodec));
janus_config_add_item(config, cat, "videocodec", janus_videoroom_videocodec_name(videoroom->vcodec));
+ if(videoroom->do_svc)
+ janus_config_add_item(config, cat, "video_svc", "yes");
if(videoroom->room_secret)
janus_config_add_item(config, cat, "secret", videoroom->room_secret);
if(videoroom->room_pin)
@@ -1674,6 +1722,8 @@ struct janus_plugin_result *janus_videoroom_handle_message(janus_plugin_session
json_object_set_new(rl, "fir_freq", json_integer(room->fir_freq));
json_object_set_new(rl, "audiocodec", json_string(janus_videoroom_audiocodec_name(room->acodec)));
json_object_set_new(rl, "videocodec", json_string(janus_videoroom_videocodec_name(room->vcodec)));
+ if(room->do_svc)
+ json_object_set_new(rl, "video_svc", json_true());
json_object_set_new(rl, "record", room->record ? json_true() : json_false());
json_object_set_new(rl, "rec_dir", json_string(room->rec_dir));
/* TODO: Should we list participants as well? or should there be a separate API call on a specific room for this? */
@@ -2397,6 +2447,27 @@ void janus_videoroom_incoming_rtp(janus_plugin_session *handle, int video, char
packet.data = rtp;
packet.length = len;
packet.is_video = video;
+ packet.svc = FALSE;
+ if(video && videoroom->do_svc) {
+ /* We're doing SVC: let's parse this packet to see which layers are there */
+ uint8_t pbit = 0, dbit = 0, ubit = 0, bbit = 0, ebit = 0;
+ int found = 0, spatial_layer = 0, temporal_layer = 0;
+ if(janus_videoroom_videocodec_parse_svc(videoroom->vcodec, buf, len, &found, &spatial_layer, &temporal_layer, &pbit, &dbit, &ubit, &bbit, &ebit) == 0) {
+ if(found) {
+ packet.svc = TRUE;
+ packet.spatial_layer = spatial_layer;
+ packet.temporal_layer = temporal_layer;
+ packet.pbit = pbit;
+ packet.dbit = dbit;
+ packet.ubit = ubit;
+ packet.bbit = bbit;
+ packet.ebit = ebit;
+ JANUS_LOG(LOG_WARN, "sl=%d, tl=%d, p=%u, d=%u, u=%u, b=%u, e=%u\n",
+ packet.spatial_layer, packet.temporal_layer,
+ packet.pbit, packet.dbit, packet.ubit, packet.bbit, packet.ebit);
+ }
+ }
+ }
/* Backup the actual timestamp and sequence number set by the publisher, in case switching is involved */
packet.timestamp = ntohl(packet.data->timestamp);
packet.seq_number = ntohs(packet.data->seq_number);
@@ -3088,6 +3159,14 @@ static void *janus_videoroom_handler(void *data) {
listener->data = FALSE; /* ... unless the publisher isn't sending any data */
listener->paused = TRUE; /* We need an explicit start from the listener */
session->participant = listener;
+ if(videoroom->do_svc) {
+ /* This listener belongs to a room where VP9 SVC has been enabled,
+ * let's assume we're interested in all layers for the time being */
+ listener->spatial_layer = -1;
+ listener->target_spatial_layer = 1; /* FIXME Chrome sends 0 and 1 */
+ listener->temporal_layer = -1;
+ listener->target_temporal_layer = 2; /* FIXME Chrome sends 0, 1 and 2 */
+ }
janus_mutex_lock(&publisher->listeners_mutex);
publisher->listeners = g_slist_append(publisher->listeners, listener);
janus_mutex_unlock(&publisher->listeners_mutex);
@@ -3388,6 +3467,8 @@ static void *janus_videoroom_handler(void *data) {
json_t *audio = json_object_get(root, "audio");
json_t *video = json_object_get(root, "video");
json_t *data = json_object_get(root, "data");
+ json_t *spatial = json_object_get(root, "spatial_layer");
+ json_t *temporal = json_object_get(root, "temporal_layer");
/* Update the audio/video/data flags, if set */
janus_videoroom_participant *publisher = listener->feed;
if(publisher) {
@@ -3398,6 +3479,34 @@ static void *janus_videoroom_handler(void *data) {
if(data && publisher->data)
listener->data = json_is_true(data);
}
+ if(listener->room->do_svc) {
+ /* Also check if the viewer is trying to configure a layer change */
+ if(spatial) {
+ int spatial_layer = json_integer_value(spatial);
+ if(spatial_layer > 1) {
+ JANUS_LOG(LOG_WARN, "Spatial layer higher than 1, will probably be ignored\n");
+ }
+ if(spatial_layer != listener->target_spatial_layer) {
+ /* Send a FIR to the new RTP forward publisher */
+ char buf[20];
+ janus_rtcp_fir((char *)&buf, 20, &publisher->fir_seq);
+ JANUS_LOG(LOG_VERB, "Need to downscale spatially, sending FIR to %"SCNu64" (%s)\n", publisher->user_id, publisher->display ? publisher->display : "??");
+ gateway->relay_rtcp(publisher->session->handle, 1, buf, 20);
+ /* Send a PLI too, just in case... */
+ janus_rtcp_pli((char *)&buf, 12);
+ JANUS_LOG(LOG_VERB, "Need to downscale spatially, sending PLI to %"SCNu64" (%s)\n", publisher->user_id, publisher->display ? publisher->display : "??");
+ gateway->relay_rtcp(publisher->session->handle, 1, buf, 12);
+ }
+ listener->target_spatial_layer = spatial_layer;
+ }
+ if(temporal) {
+ int temporal_layer = json_integer_value(temporal);
+ if(temporal_layer > 2) {
+ JANUS_LOG(LOG_WARN, "Temporal layer higher than 2, will probably be ignored\n");
+ }
+ listener->target_temporal_layer = temporal_layer;
+ }
+ }
event = json_object();
json_object_set_new(event, "videoroom", json_string("event"));
json_object_set_new(event, "room", json_integer(listener->room->room_id));
@@ -3462,6 +3571,14 @@ static void *janus_videoroom_handler(void *data) {
listener->data = data ? json_is_true(data) : TRUE; /* True by default */
if(!publisher->data)
listener->data = FALSE; /* ... unless the publisher isn't sending any data */
+ if(listener->room && listener->room->do_svc) {
+ /* This listener belongs to a room where VP9 SVC has been enabled,
+ * let's assume we're interested in all layers for the time being */
+ listener->spatial_layer = -1;
+ listener->target_spatial_layer = 1; /* FIXME Chrome sends 0 and 1 */
+ listener->temporal_layer = -1;
+ listener->target_temporal_layer = 2; /* FIXME Chrome sends 0, 1 and 2 */
+ }
janus_mutex_lock(&publisher->listeners_mutex);
publisher->listeners = g_slist_append(publisher->listeners, listener);
janus_mutex_unlock(&publisher->listeners_mutex);
@@ -3828,6 +3945,148 @@ error:
return NULL;
}
+/* Helper method to parse an RTP video frame and get some SVC-related info */
+static int janus_videoroom_videocodec_parse_svc(janus_videoroom_videocodec vcodec,
+ char *buf, int total, int *found,
+ int *spatial_layer, int *temporal_layer,
+ uint8_t *p, uint8_t *d, uint8_t *u, uint8_t *b, uint8_t *e) {
+ if(found)
+ *found = 0;
+ if(!buf || total < 12)
+ return -1;
+ /* This only works with VP9, right now, on an experimental basis) */
+ if(vcodec != JANUS_VIDEOROOM_VP9)
+ return -2;
+ /* Skip RTP header and extensions */
+ int len = 0;
+ char *buffer = janus_rtp_payload(buf, total, &len);
+ /* VP9 depay: */
+ /* https://tools.ietf.org/html/draft-ietf-payload-vp9-03 */
+ /* Read the first octet (VP9 Payload Descriptor) */
+ uint8_t vp9pd = *buffer;
+ uint8_t ibit = (vp9pd & 0x80) >> 7;
+ uint8_t pbit = (vp9pd & 0x40) >> 6;
+ uint8_t lbit = (vp9pd & 0x20) >> 5;
+ uint8_t fbit = (vp9pd & 0x10) >> 4;
+ uint8_t bbit = (vp9pd & 0x08) >> 3;
+ uint8_t ebit = (vp9pd & 0x04) >> 2;
+ uint8_t vbit = (vp9pd & 0x02) >> 1;
+ if(!lbit) {
+ /* No Layer indices present, no need to go on */
+ if(found)
+ *found = 0;
+ return 0;
+ }
+ /* Move to the next octet and see what's there */
+ buffer++;
+ len--;
+ if(ibit) {
+ /* Read the PictureID octet */
+ vp9pd = *buffer;
+ uint16_t picid = vp9pd, wholepicid = picid;
+ uint8_t mbit = (vp9pd & 0x80);
+ if(!mbit) {
+ buffer++;
+ len--;
+ } else {
+ memcpy(&picid, buffer, sizeof(uint16_t));
+ wholepicid = ntohs(picid);
+ picid = (wholepicid & 0x7FFF);
+ buffer += 2;
+ len -= 2;
+ }
+ }
+ if(lbit) {
+ /* Read the octet and parse the layer indices now */
+ vp9pd = *buffer;
+ int tlid = (vp9pd & 0xE0) >> 5;
+ uint8_t ubit = (vp9pd & 0x10) >> 4;
+ int slid = (vp9pd & 0x0E) >> 1;
+ uint8_t dbit = (vp9pd & 0x01);
+ JANUS_LOG(LOG_HUGE, "Parsed Layer indices: Temporal: %d (%u), Spatial: %d (%u)\n",
+ tlid, ubit, slid, dbit);
+ if(temporal_layer)
+ *temporal_layer = tlid;
+ if(spatial_layer)
+ *spatial_layer = slid;
+ if(p)
+ *p = pbit;
+ if(d)
+ *d = dbit;
+ if(u)
+ *u = ubit;
+ if(b)
+ *b = bbit;
+ if(e)
+ *e = ebit;
+ if(found)
+ *found = 1;
+ /* Go on, just to get to the SS, if available (which we currently ignore anyway) */
+ buffer++;
+ len--;
+ if(!fbit) {
+ /* Non-flexible mode, skip TL0PICIDX */
+ buffer++;
+ len--;
+ }
+ }
+ if(fbit && pbit) {
+ /* Skip reference indices */
+ uint8_t nbit = 1;
+ while(nbit) {
+ vp9pd = *buffer;
+ nbit = (vp9pd & 0x01);
+ buffer++;
+ len--;
+ }
+ }
+ if(vbit) {
+ /* Parse and skip SS */
+ vp9pd = *buffer;
+ int n_s = (vp9pd & 0xE0) >> 5;
+ n_s++;
+ JANUS_LOG(LOG_HUGE, "There are %d spatial layers\n", n_s);
+ uint8_t ybit = (vp9pd & 0x10);
+ uint8_t gbit = (vp9pd & 0x08);
+ if(ybit) {
+ /* Iterate on all spatial layers and get resolution */
+ buffer++;
+ len--;
+ int i=0;
+ for(i=0; i<n_s; i++) {
+ /* Been there, done that: skip skip skip */
+ buffer += 4;
+ len -= 4;
+ }
+ }
+ if(gbit) {
+ if(!ybit) {
+ buffer++;
+ len--;
+ }
+ uint8_t n_g = *buffer;
+ JANUS_LOG(LOG_HUGE, "There are %u frames in a GOF\n", n_g);
+ buffer++;
+ len--;
+ if(n_g > 0) {
+ int i=0;
+ for(i=0; i<n_g; i++) {
+ /* Read the R bits */
+ vp9pd = *buffer;
+ int r = (vp9pd & 0x0C) >> 2;
+ if(r > 0) {
+ /* Skip reference indices */
+ buffer += r;
+ len -= r;
+ }
+ buffer++;
+ len--;
+ }
+ }
+ }
+ }
+ return 0;
+}
/* Helper to quickly relay RTP packets from publishers to subscribers */
static void janus_videoroom_relay_rtp_packet(gpointer data, gpointer user_data) {
@@ -3862,10 +4121,80 @@ static void janus_videoroom_relay_rtp_packet(gpointer data, gpointer user_data)
/* Nope, don't relay */
return;
}
+ /* Check if there's any SVC info to take into account */
+ gboolean override_mark_bit = FALSE, has_marker_bit = packet->data->markerbit;
+ if(packet->svc) {
+ /* There is: check if this is a layer that can be dropped for this viewer
+ * Note: Following core inspired by the excellent job done by Sergio Garcia Murillo here:
+ * https://github.com/medooze/media-server/blob/master/src/vp9/VP9LayerSelector.cpp */
+ int temporal_layer = listener->temporal_layer;
+ if(listener->target_temporal_layer > listener->temporal_layer) {
+ /* We need to upscale */
+ JANUS_LOG(LOG_WARN, "We need to upscale temporally:\n");
+ if(packet->ubit && packet->bbit && packet->temporal_layer <= listener->target_temporal_layer) {
+ JANUS_LOG(LOG_WARN, " -- Upscaling temporal layer: %u --> %u\n",
+ packet->temporal_layer, listener->target_temporal_layer);
+ listener->temporal_layer = packet->temporal_layer;
+ temporal_layer = listener->temporal_layer;
+ }
+ } else if(listener->target_temporal_layer < listener->temporal_layer) {
+ /* We need to downscale */
+ JANUS_LOG(LOG_WARN, "We need to downscale temporally:\n");
+ if(packet->ebit) {
+ JANUS_LOG(LOG_WARN, " -- Downscaling temporal layer: %u --> %u\n",
+ listener->temporal_layer, listener->target_temporal_layer);
+ listener->temporal_layer = listener->target_temporal_layer;
+ }
+ }
+ if(temporal_layer < packet->temporal_layer) {
+ /* Drop the packet: update the context to make sure sequence number is increased normally later */
+ JANUS_LOG(LOG_WARN, "Dropping packet (temporal layer %d < %d)\n", temporal_layer, packet->temporal_layer);
+ listener->context.v_base_seq++;
+ return;
+ }
+ int spatial_layer = listener->spatial_layer;
+ if(listener->target_spatial_layer > listener->spatial_layer) {
+ JANUS_LOG(LOG_WARN, "We need to upscale spatially:\n");
+ /* We need to upscale */
+ if(packet->pbit == 0 && packet->bbit && packet->spatial_layer == listener->spatial_layer+1) {
+ JANUS_LOG(LOG_WARN, " -- Upscaling spatial layer: %u --> %u\n",
+ packet->spatial_layer, listener->target_spatial_layer);
+ listener->spatial_layer = packet->spatial_layer;
+ spatial_layer = listener->spatial_layer;
+ }
+ } else if(listener->target_spatial_layer < listener->spatial_layer) {
+ /* We need to downscale */
+ JANUS_LOG(LOG_WARN, "We need to downscale spatially:\n");
+ if(packet->ebit) {
+ JANUS_LOG(LOG_WARN, " -- Downscaling spatial layer: %u --> %u\n",
+ listener->spatial_layer, listener->target_spatial_layer);
+ listener->spatial_layer = listener->target_spatial_layer;
+ }
+ }
+ if(spatial_layer < packet->spatial_layer) {
+ /* Drop the packet: update the context to make sure sequence number is increased normally later */
+ JANUS_LOG(LOG_HUGE, "Dropping packet (spatial layer %d < %d)\n", spatial_layer, packet->spatial_layer);
+ listener->context.v_base_seq++;
+ return;
+ } else if(packet->ebit && spatial_layer == packet->spatial_layer) {
+ /* If we stop at layer 0, we need a marker bit now, as the one from layer 1 will not be received */
+ override_mark_bit = TRUE;
+ }
+ /* If we got here, we can send the frame: this doesn't necessarily mean it's
+ * one of the layers the user wants, as there may be dependencies involved */
+ JANUS_LOG(LOG_HUGE, "Sending packet (spatial=%d, temporal=%d)\n",
+ packet->spatial_layer, packet->temporal_layer);
+ }
/* Fix sequence number and timestamp (publisher switching may be involved) */
janus_rtp_header_update(packet->data, &listener->context, TRUE, 4500);
+ if(override_mark_bit && !has_marker_bit) {
+ packet->data->markerbit = 1;
+ }
if(gateway != NULL)
gateway->relay_rtp(session->handle, packet->is_video, (char *)packet->data, packet->length);
+ if(override_mark_bit && !has_marker_bit) {
+ packet->data->markerbit = 0;
+ }
/* Restore the timestamp and sequence number to what the publisher set them to */
packet->data->timestamp = htonl(packet->timestamp);
packet->data->seq_number = htons(packet->seq_number);
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-voip/janus.git
More information about the Pkg-voip-commits
mailing list