[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