[Pkg-voip-commits] [janus] 07/163: First experiments with VP8 simulcasting (Chrome and Firefox, WIP)

Jonas Smedegaard dr at jones.dk
Sat Oct 28 01:22:03 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 3e01d6ef6936510821bb699566be3b0cf8d3041a
Author: Lorenzo Miniero <lminiero at gmail.com>
Date:   Wed Jun 28 10:53:38 2017 +0200

    First experiments with VP8 simulcasting (Chrome and Firefox, WIP)
---
 html/devicetest.html      |   8 ++
 html/devicetest.js        |  45 ++++++-
 html/echotest.html        |  16 ++-
 html/echotest.js          |  37 ++++++
 html/janus.js             | 123 +++++++++++++++++-
 html/videoroomtest.html   |  66 +++++++++-
 html/videoroomtest.js     |  58 +++++++++
 ice.c                     |  30 ++++-
 ice.h                     |   6 +
 janus.c                   |  42 ++++++-
 plugins/janus_echotest.c  | 277 +++++++++++++++++++++++++++++++++++++----
 plugins/janus_streaming.c | 265 +++++++--------------------------------
 plugins/janus_videoroom.c | 143 +++++++++++++++++----
 postprocessing/pp-webm.c  |   2 +-
 rtp.c                     |  33 ++++-
 rtp.h                     |  13 ++
 sdp.c                     | 130 +++++++++++++++++--
 sdp.h                     |   8 ++
 utils.c                   | 308 ++++++++++++++++++++++++++++++++++++++++++++++
 utils.h                   |  35 ++++++
 20 files changed, 1339 insertions(+), 306 deletions(-)

diff --git a/html/devicetest.html b/html/devicetest.html
index 8872892..798a54c 100644
--- a/html/devicetest.html
+++ b/html/devicetest.html
@@ -11,6 +11,7 @@
 <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="devicetest.js"></script>
 <script>
@@ -25,6 +26,7 @@
 <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" type="text/css"/>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.css"/>
 </head>
 <body>
 
@@ -118,6 +120,12 @@
 								<h3 class="panel-title">Remote Stream
 									<span class="label label-primary hide" id="curres"></span>
 									<span class="label label-info hide" id="curbitrate"></span>
+									<div id="simulcast" class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<button id="sl-1" type="button" class="btn btn-primary" data-toggle="tooltip" title="Switch to higher quality" style="width: 50%">SL 1</button>
+											<button id="sl-0" type="button" class="btn btn-primary" data-toggle="tooltip" title="Switch to lower quality" style="width: 50%">SL 0</button>
+										</div>
+									</div>
 									<div class="btn-group btn-group-xs pull-right hide" id="output-devices">
 										<div class="btn-group btn-group-xs">
 											<button id="outputdeviceset" autocomplete="off" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
diff --git a/html/devicetest.js b/html/devicetest.js
index 4b63a97..07f5f8c 100644
--- a/html/devicetest.js
+++ b/html/devicetest.js
@@ -58,6 +58,7 @@ var spinner = null;
 
 var audioenabled = false;
 var videoenabled = false;
+var simulcastStarted = false;
 
 // Helper method to prepare a UI selection of the available devices
 function initDevices(devices) {
@@ -112,12 +113,12 @@ function initDevices(devices) {
 		// A different device has been selected: hangup the session, and set it up again
 		$('#audio-device, #video-device').attr('disabled', true);
 		$('#change-devices').attr('disabled', true);
-		echotest.hangup(true);
 		if(firstTime) {
 			firstTime = false;
 			restartCapture();
 			return;
 		}
+		echotest.hangup(true);
 		// Let's wait a couple of seconds before restarting
 		setTimeout(restartCapture, 2000);
 	});
@@ -133,7 +134,7 @@ function restartCapture() {
 	Janus.debug("Trying a createOffer too (audio/video sendrecv)");
 	echotest.createOffer(
 		{
-			// No media provided: by default, it's sendrecv for audio and video
+			// We provide a specific device ID for both audio and video
 			media: {
 				audio: {
 					deviceId: {
@@ -147,6 +148,9 @@ function restartCapture() {
 				},
 				data: true	// Let's negotiate data channels as well
 			},
+			// If you want to test simulcasting (Chrome and Firefox only), then
+			// uncomment the "simulcast:true," line: new buttons will appear
+			simulcast: true,
 			success: function(jsep) {
 				Janus.debug("Got SDP!");
 				Janus.debug(jsep);
@@ -263,6 +267,39 @@ $(document).ready(function() {
 											$('#outputdeviceset').html('Output device<span class="caret"></span>');
 										}
 									}
+									// Is simulcast in place?
+									var simulcast = msg["simulcast"];
+									if(simulcast !== null && simulcast !== undefined) {
+										if(!simulcastStarted) {
+											simulcastStarted = true;
+											$('#simulcast').removeClass('hide');
+											// Enable the VP8 simulcast selection buttons
+											$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')
+												.unbind('click').click(function() {
+													toastr.info("Switching simulcast video, wait for it... (lower quality)", null, {timeOut: 2000});
+													$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+													$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
+													echotest.send({message: { simulcast: 0}});
+												});
+											$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-success')
+												.unbind('click').click(function() {
+													toastr.info("Switching simulcast video, wait for it... (higher quality)", null, {timeOut: 2000});
+													$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
+													$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+													echotest.send({message: { simulcast: 1}});
+												});
+										}
+										// We just received notice that there's been a switch, update the buttons
+										if(simulcast === 0) {
+											toastr.success("Switched simulcast video! (lower quality)", null, {timeOut: 2000});
+											$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+											$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
+										} else if(simulcast === 1) {
+											toastr.success("Switched simulcast video! (higher quality)", null, {timeOut: 2000});
+											$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
+											$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+										}
+									}
 								},
 								onlocalstream: function(stream) {
 									Janus.debug(" ::: Got a local stream :::");
@@ -412,6 +449,10 @@ $(document).ready(function() {
 									$('#datasend').val('').attr('disabled', true);
 									$('#datarecv').val('');
 									$('#outputdeviceset').html('Output device<span class="caret"></span>');
+									simulcastStarted = false;
+									$('#simulcast').addClass('hide');
+									$('#sl-0').unbind('click');
+									$('#sl-1').unbind('click');
 								}
 							});
 					},
diff --git a/html/echotest.html b/html/echotest.html
index 8308399..f9e576a 100644
--- a/html/echotest.html
+++ b/html/echotest.html
@@ -11,6 +11,7 @@
 <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="echotest.js"></script>
 <script>
@@ -25,6 +26,7 @@
 <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" type="text/css"/>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.css"/>
 </head>
 <body>
 
@@ -54,7 +56,10 @@
 						which will tell the gateway to drop the frames and not echo them
 						back to you. You can also try and cap the bitrate: such control
 						will tell the gateway to manipulate the RTCP REMB packets passing
-						through, in order to simulate a bandwidth limitation.</p>
+						through, in order to simulate a bandwidth limitation. In case
+						Simulcasting has been enabled, buttons will appear to allow you
+						to switch between lower and higher quality: notice that you may
+						have to increase the bandwidth to have the quality video appear.</p>
 						<p>Finally, this demo also includes Data Channels: whatever you
 						write in the text box under your local video, will be sent via
 						Data Channels to the plugins, modified by adding a fixed prefix,
@@ -104,7 +109,14 @@
 					<div class="col-md-6">
 						<div class="panel panel-default">
 							<div class="panel-heading">
-								<h3 class="panel-title">Remote Stream <span class="label label-primary hide" id="curres"></span> <span class="label label-info hide" id="curbitrate"></span></h3>
+								<h3 class="panel-title">Remote Stream <span class="label label-primary hide" id="curres"></span> <span class="label label-info hide" id="curbitrate"></span>
+									<div id="simulcast" class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<button id="sl-1" type="button" class="btn btn-primary" data-toggle="tooltip" title="Switch to higher quality" style="width: 50%">SL 1</button>
+											<button id="sl-0" type="button" class="btn btn-primary" data-toggle="tooltip" title="Switch to lower quality" style="width: 50%">SL 0</button>
+										</div>
+									</div>
+								</h3>
 							</div>
 							<div class="panel-body" id="videoright"></div>
 						</div>
diff --git a/html/echotest.js b/html/echotest.js
index f720d0a..c98b866 100644
--- a/html/echotest.js
+++ b/html/echotest.js
@@ -58,6 +58,7 @@ var spinner = null;
 
 var audioenabled = false;
 var videoenabled = false;
+var simulcastStarted = false;
 
 $(document).ready(function() {
 	// Initialize the library (all console debuggers enabled)
@@ -105,6 +106,9 @@ $(document).ready(function() {
 										{
 											// No media provided: by default, it's sendrecv for audio and video
 											media: { data: true },	// Let's negotiate data channels as well
+											// If you want to test simulcasting (Chrome and Firefox only), then
+											// uncomment the "simulcast:true," line: new buttons will appear
+											simulcast: true,
 											success: function(jsep) {
 												Janus.debug("Got SDP!");
 												Janus.debug(jsep);
@@ -185,6 +189,39 @@ $(document).ready(function() {
 											$('#curres').hide();
 										}
 									}
+									// Is simulcast in place?
+									var simulcast = msg["simulcast"];
+									if(simulcast !== null && simulcast !== undefined) {
+										if(!simulcastStarted) {
+											simulcastStarted = true;
+											$('#simulcast').removeClass('hide');
+											// Enable the VP8 simulcast selection buttons
+											$('#sl-0').removeClass('btn-primary btn-success').addClass('btn-primary')
+												.unbind('click').click(function() {
+													toastr.info("Switching simulcast video, wait for it... (lower quality)", null, {timeOut: 2000});
+													$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+													$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
+													echotest.send({message: { simulcast: 0}});
+												});
+											$('#sl-1').removeClass('btn-primary btn-success').addClass('btn-success')
+												.unbind('click').click(function() {
+													toastr.info("Switching simulcast video, wait for it... (higher quality)", null, {timeOut: 2000});
+													$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
+													$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+													echotest.send({message: { simulcast: 1}});
+												});
+										}
+										// We just received notice that there's been a switch, update the buttons
+										if(simulcast === 0) {
+											toastr.success("Switched simulcast video! (lower quality)", null, {timeOut: 2000});
+											$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+											$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
+										} else if(simulcast === 1) {
+											toastr.success("Switched simulcast video! (higher quality)", null, {timeOut: 2000});
+											$('#sl-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
+											$('#sl-0').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+										}
+									}
 								},
 								onlocalstream: function(stream) {
 									Janus.debug(" ::: Got a local stream :::");
diff --git a/html/janus.js b/html/janus.js
index 7647dfb..46d1e54 100644
--- a/html/janus.js
+++ b/html/janus.js
@@ -1731,7 +1731,8 @@ function Janus(gatewayCallbacks) {
 			return;
 		}
 		var config = pluginHandle.webrtcStuff;
-		Janus.log("Creating offer (iceDone=" + config.iceDone + ")");
+		var simulcast = callbacks.simulcast === true ? true : false;
+		Janus.log("Creating offer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")");
 		// https://code.google.com/p/webrtc/issues/detail?id=3508
 		var mediaConstraints = null;
 		if(adapter.browserDetails.browser == "firefox" || adapter.browserDetails.browser == "edge") {
@@ -1748,11 +1749,129 @@ function Janus(gatewayCallbacks) {
 			};
 		}
 		Janus.debug(mediaConstraints);
+		// Check if this is Firefox and we've been asked to do simulcasting
+		if(simulcast && adapter.browserDetails.browser === "firefox") {
+			// FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
+			Janus.log("Enabling Simulcasting for Firefox (RID)");
+			var sender = config.pc.getSenders()[1];
+			Janus.log(sender);
+			var parameters = sender.getParameters();
+			Janus.log(parameters);
+			sender.setParameters({encodings: [
+				{ rid: "high", active: true, priority: "high", maxBitrate: 2000000 },
+				{ rid: "medium", active: true, priority: "medium", maxBitrate: 200000 }
+			]});
+		}
 		config.pc.createOffer(
 			function(offer) {
 				Janus.debug(offer);
 				if(config.mySdp === null || config.mySdp === undefined) {
 					Janus.log("Setting local description");
+					if(simulcast) {
+						// This SDP munging only works with Chrome
+						if(adapter.browserDetails.browser === "chrome") {
+							Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
+							// Let's munge the SDP to add the attributes for enabling simulcasting
+							// (based on https://gist.github.com/ggarber/a19b4c33510028b9c657)
+							var lines = offer.sdp.split("\r\n");
+							var video = false;
+							var ssrc = [ -1 ], ssrc_fid = -1;
+							var cname = null, msid = null, mslabel = null, label = null;
+							var insertAt = -1;
+							for(var i=0; i<lines.length; i++) {
+								var mline = lines[i].match(/m=(\w+) */);
+								if(mline) {
+									var medium = mline[1];
+									if(medium === "video") {
+										// New video m-line: make sure it's the first one
+										if(ssrc[0] < 0) {
+											video = true;
+										} else {
+											// We're done, let's add the new attributes here
+											insertAt = i;
+											break;
+										}
+									} else {
+										// New non-video m-line: do we have what we were looking for?
+										if(ssrc[0] > -1) {
+											// We're done, let's add the new attributes here
+											insertAt = i;
+											break;
+										}
+									}
+									continue;
+								}
+								var fid = lines[i].match(/a=ssrc-group:FID (\d+) (\d+)/);
+								if(fid) {
+									ssrc[0] = fid[1];
+									ssrc_fid = fid[2];
+									lines.splice(i, 1); i--;
+									continue;
+								}
+								if(ssrc[0]) {
+									var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
+									if(match) {
+										cname = match[1];
+									}
+									match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
+									if(match) {
+										msid = match[1];
+									}
+									match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
+									if(match) {
+										mslabel = match[1];
+									}
+									match = lines[i].match('a=ssrc:' + ssrc + ' label:(.+)')
+									if(match) {
+										label = match[1];
+									}
+									if(lines[i].indexOf('a=ssrc:' + ssrc_fid) === 0) {
+										lines.splice(i, 1); i--;
+										continue;
+									}
+									if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
+										lines.splice(i, 1); i--;
+										continue;
+									}
+								}
+								if(lines[i].length == 0) {
+									lines.splice(i, 1); i--;
+									continue;
+								}
+							}
+							if(insertAt < 0) {
+								// Append at the end
+								insertAt = lines.length-1;
+							}
+							// Generate a couple of SSRCs
+							ssrc[1] = Math.floor(Math.random()*0xFFFFFFFF);
+							ssrc[2] = Math.floor(Math.random()*0xFFFFFFFF);
+							// Add attributes to the SDP
+							lines.splice(insertAt, 0, 'a=ssrc-group:SIM ' + ssrc[0] + ' ' + ssrc[1] + ' ' + ssrc[2]);
+							insertAt++;
+							for(var i=0; i<ssrc.length; i++) {
+								if(cname) {
+									lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' cname:' + cname);
+									insertAt++;
+								}
+								if(msid) {
+									lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' msid:' + msid);
+									insertAt++;
+								}
+								if(mslabel) {
+									lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' mslabel:' + msid);
+									insertAt++;
+								}
+								if(label) {
+									lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' label:' + msid);
+									insertAt++;
+								}
+							}
+							offer.sdp = lines.join("\r\n");
+						} else if(adapter.browserDetails.browser !== "firefox") {
+							Janus.warn("simulcast=true, but this is not Chrome nor Firefox, ignoring");
+						}
+					}
 					config.mySdp = offer.sdp;
 					config.pc.setLocalDescription(offer);
 				}
@@ -2027,7 +2146,7 @@ function Janus(gatewayCallbacks) {
 			return config.bitrate.value;
 		} else if(config.pc.getStats && adapter.browserDetails.browser == "firefox") {
 			// Do it the Firefox way
-			if(config.remoteStream === null || config.remoteStream === undefined
+			if(config.remoteStream === null || config.remoteStream === undefined || config.remoteStream.remoteStreams === undefined
 					|| config.remoteStream.streams[0] === null || config.remoteStream.streams[0] === undefined) {
 				Janus.warn("Remote stream unavailable");
 				return "Remote stream unavailable";
diff --git a/html/videoroomtest.html b/html/videoroomtest.html
index f8a0f3e..4ec8abf 100644
--- a/html/videoroomtest.html
+++ b/html/videoroomtest.html
@@ -11,6 +11,7 @@
 <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="videoroomtest.js"></script>
 <script>
@@ -25,6 +26,7 @@
 <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>
 
@@ -81,7 +83,24 @@
 					<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>
+								<h3 class="panel-title">Local Video <span class="label label-primary hide" id="publisher"></span>
+									<div class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<button id="bitrateset" autocomplete="off" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+												Bandwidth<span class="caret"></span>
+											</button>
+											<ul id="bitrate" class="dropdown-menu" role="menu">
+												<li><a href="#" id="0">No limit</a></li>
+												<li><a href="#" id="128">Cap to 128kbit</a></li>
+												<li><a href="#" id="256">Cap to 256kbit</a></li>
+												<li><a href="#" id="512">Cap to 512kbit</a></li>
+												<li><a href="#" id="1024">Cap to 1mbit</a></li>
+												<li><a href="#" id="1500">Cap to 1.5mbit</a></li>
+												<li><a href="#" id="2000">Cap to 2mbit</a></li>
+											</ul>
+										</div>
+									</div>
+								</h3>
 							</div>
 							<div class="panel-body" id="videolocal"></div>
 						</div>
@@ -89,7 +108,14 @@
 					<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></h3>
+								<h3 class="panel-title">Remote Video #1 <span class="label label-info hide" id="remote1"></span>
+									<div id="simulcast1" class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<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>
+								</h3>
 							</div>
 							<div class="panel-body relative" id="videoremote1"></div>
 						</div>
@@ -97,7 +123,14 @@
 					<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></h3>
+								<h3 class="panel-title">Remote Video #2 <span class="label label-info hide" id="remote2"></span>
+									<div id="simulcast2" class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<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>
+								</h3>
 							</div>
 							<div class="panel-body relative" id="videoremote2"></div>
 						</div>
@@ -107,7 +140,14 @@
 					<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></h3>
+								<h3 class="panel-title">Remote Video #3 <span class="label label-info hide" id="remote3"></span>
+									<div id="simulcast3" class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<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>
+								</h3>
 							</div>
 							<div class="panel-body relative" id="videoremote3"></div>
 						</div>
@@ -115,7 +155,14 @@
 					<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></h3>
+								<h3 class="panel-title">Remote Video #4 <span class="label label-info hide" id="remote4"></span>
+									<div id="simulcast4" class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<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>
+								</h3>
 							</div>
 							<div class="panel-body relative" id="videoremote4"></div>
 						</div>
@@ -123,7 +170,14 @@
 					<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></h3>
+								<h3 class="panel-title">Remote Video #5 <span class="label label-info hide" id="remote5"></span>
+									<div id="simulcast5" class="btn-group btn-group-xs pull-right hide">
+										<div class="btn-group btn-group-xs">
+											<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>
+								</h3>
 							</div>
 							<div class="panel-body relative" id="videoremote5"></div>
 						</div>
diff --git a/html/videoroomtest.js b/html/videoroomtest.js
index d139bf9..6903193 100644
--- a/html/videoroomtest.js
+++ b/html/videoroomtest.js
@@ -134,6 +134,20 @@ $(document).ready(function() {
 								webrtcState: function(on) {
 									Janus.log("Janus says our WebRTC PeerConnection is " + (on ? "up" : "down") + " now");
 									$("#videolocal").parent().parent().unblock();
+									// This controls allows us to override the global room bitrate cap
+									$('#bitrate').parent().parent().removeClass('hide').show();
+									$('#bitrate a').click(function() {
+										var id = $(this).attr("id");
+										var bitrate = parseInt(id)*1000;
+										if(bitrate === 0) {
+											Janus.log("Not limiting bandwidth via REMB");
+										} else {
+											Janus.log("Capping bandwidth to " + bitrate + " via REMB");
+										}
+										$('#bitrateset').html($(this).html() + '<span class="caret"></span>').parent().removeClass('open');
+										sfutest.send({"message": { "request": "configure", "bitrate": bitrate }});
+										return false;
+									});
 								},
 								onmessage: function(msg, jsep) {
 									Janus.debug(" ::: Got a message (publisher) :::");
@@ -286,6 +300,8 @@ $(document).ready(function() {
 									$('#videolocal').html('<button id="publish" class="btn btn-primary">Publish</button>');
 									$('#publish').click(function() { publishOwnFeed(true); });
 									$("#videolocal").parent().parent().unblock();
+									$('#bitrate').parent().parent().addClass('hide');
+									$('#bitrate a').unbind('click');
 								}
 							});
 					},
@@ -352,6 +368,10 @@ function publishOwnFeed(useAudio) {
 		{
 			// Add data:true here if you want to publish datachannels as well
 			media: { audioRecv: false, videoRecv: false, audioSend: useAudio, videoSend: true },	// Publishers are sendonly
+			// If you want to test simulcasting (Chrome and Firefox only), then
+			// uncomment the "simulcast:true," line: new buttons will appear
+			// for the viewers subscribing to this stream remotely
+			simulcast: true,
 			success: function(jsep) {
 				Janus.debug("Got publisher SDP!");
 				Janus.debug(jsep);
@@ -397,6 +417,7 @@ function newRemoteFeed(id, display) {
 			opaqueId: opaqueId,
 			success: function(pluginHandle) {
 				remoteFeed = pluginHandle;
+				remoteFeed.simulcastStarted = false;
 				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
@@ -432,6 +453,40 @@ function newRemoteFeed(id, display) {
 						}
 						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(event === "simulcast") {
+						// We got an event on a simulcast switch from this publisher
+						var simulcast = msg["simulcast"];
+						if(simulcast !== null && simulcast !== undefined) {
+							if(!remoteFeed.simulcastStarted) {
+								remoteFeed.simulcastStarted = true;
+								$('#simulcast'+remoteFeed.rfindex).removeClass('hide');
+								// Enable the VP8 simulcast selection buttons
+								$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary')
+									.unbind('click').click(function() {
+										toastr.info("Switching " + remoteFeed.rfdisplay + "'s simulcast video, wait for it... (lower quality)", null, {timeOut: 2000});
+										$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+										$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
+										remoteFeed.send({message: { request: "configure", simulcast: 0}});
+									});
+								$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-success')
+									.unbind('click').click(function() {
+										toastr.info("Switching " + remoteFeed.rfdisplay + "'s simulcast video, wait for it... (higher quality)", null, {timeOut: 2000});
+										$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-info btn-success').addClass('btn-info');
+										$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+										remoteFeed.send({message: { request: "configure", simulcast: 1}});
+									});
+							}
+							// We just received notice that there's been a switch, update the buttons
+							if(simulcast === 0) {
+								toastr.success("Switched " + remoteFeed.rfdisplay + "'s simulcast video! (lower quality)", null, {timeOut: 2000});
+								$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+								$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
+							} else if(simulcast === 1) {
+								toastr.success("Switched " + remoteFeed.rfdisplay + "'s simulcast video! (higher quality)", null, {timeOut: 2000});
+								$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-info btn-success').addClass('btn-success');
+								$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-info btn-success').addClass('btn-primary');
+							}
+						}
 					} else if(msg["error"] !== undefined && msg["error"] !== null) {
 						bootbox.alert(msg["error"]);
 					} else {
@@ -532,6 +587,9 @@ function newRemoteFeed(id, display) {
 				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');
+				$('#simulcast'+remoteFeed.rfindex).addClass('hide');
 			}
 		});
 }
diff --git a/ice.c b/ice.c
index 56e7ada..49afd90 100644
--- a/ice.c
+++ b/ice.c
@@ -1329,6 +1329,12 @@ void janus_ice_stream_free(GHashTable *streams, janus_ice_stream *stream) {
 		g_free(stream->rpass);
 		stream->rpass = NULL;
 	}
+	g_free(stream->rid[0]);
+	stream->rid[0] = NULL;
+	g_free(stream->rid[1]);
+	stream->rid[1] = NULL;
+	g_free(stream->rid[2]);
+	stream->rid[2] = NULL;
 	g_list_free(stream->audio_payload_types);
 	stream->audio_payload_types = NULL;
 	g_list_free(stream->video_payload_types);
@@ -1896,7 +1902,10 @@ void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_i
 			} else {
 				/* Bundled streams, check SSRC */
 				guint32 packet_ssrc = ntohl(header->ssrc);
-				video = ((stream->video_ssrc_peer == packet_ssrc || stream->video_ssrc_peer_rtx == packet_ssrc) ? 1 : 0);
+				video = ((stream->video_ssrc_peer == packet_ssrc
+					|| stream->video_ssrc_peer_rtx == packet_ssrc
+					|| stream->video_ssrc_peer_sim_1 == packet_ssrc
+					|| stream->video_ssrc_peer_sim_2 == packet_ssrc) ? 1 : 0);
 				if(!video && stream->audio_ssrc_peer != packet_ssrc) {
 					/* FIXME In case it happens, we should check what it is */
 					if(stream->audio_ssrc_peer == 0 || stream->video_ssrc_peer == 0) {
@@ -1941,6 +1950,12 @@ void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_i
 					/* FIXME This is a video retransmission: set the regular peer SSRC so
 					 * that we avoid outgoing SRTP errors in case we got the packet already */
 					header->ssrc = htonl(stream->video_ssrc_peer);
+				} else if(stream->video_ssrc_peer_sim_1 == packet_ssrc) {
+					/* FIXME Simulcast (1) */
+					JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Simulcast #1 (SSRC %"SCNu32")...\n", handle->handle_id, packet_ssrc);
+				} else if(stream->video_ssrc_peer_sim_2 == packet_ssrc) {
+					/* FIXME Simulcast (2) */
+					JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Simulcast #2 (SSRC %"SCNu32")...\n", handle->handle_id, packet_ssrc);
 				}
 				//~ JANUS_LOG(LOG_VERB, "[RTP] Bundling: this is %s (video=%"SCNu64", audio=%"SCNu64", got %ld)\n",
 					//~ video ? "video" : "audio", stream->video_ssrc_peer, stream->audio_ssrc_peer, ntohl(header->ssrc));
@@ -2015,6 +2030,10 @@ void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_i
 					janus_mutex_unlock(&component->mutex);
 				}
 
+				/* FIXME Don't handle RTCP or stats for the simulcasted SSRCs, for now */
+				if(video && ntohl(header->ssrc) != stream->video_ssrc_peer)
+					return;
+
 				/* Update the RTCP context as well */
 				rtcp_context *rtcp_ctx = video ? stream->video_rtcp_ctx : stream->audio_rtcp_ctx;
 				janus_rtcp_process_incoming_rtp(rtcp_ctx, buf, buflen);
@@ -2180,6 +2199,7 @@ void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_i
 						}
 					}
 				}
+
 				/* Let's process this RTCP (compound?) packet, and update the RTCP context for this stream in case */
 				rtcp_context *rtcp_ctx = video ? stream->video_rtcp_ctx : stream->audio_rtcp_ctx;
 				janus_rtcp_parse(rtcp_ctx, buf, buflen);
@@ -2826,7 +2846,9 @@ int janus_ice_setup_local(janus_ice_handle *handle, int offer, int audio, int vi
 			audio_stream->video_ssrc = janus_random_uint32();	/* FIXME Should we look for conflicts? */
 		}
 		audio_stream->video_ssrc_peer = 0;	/* FIXME Right now we don't know what this will be */
-		audio_stream->video_ssrc_peer_rtx = 0;	/* FIXME Right now we don't know what this will be */
+		audio_stream->video_ssrc_peer_rtx = 0;		/* FIXME Right now we don't know if and what this will be */
+		audio_stream->video_ssrc_peer_sim_1 = 0;	/* FIXME Right now we don't know if and what this will be */
+		audio_stream->video_ssrc_peer_sim_2 = 0;	/* FIXME Right now we don't know if and what this will be */
 		audio_stream->audio_rtcp_ctx = g_malloc0(sizeof(rtcp_context));
 		audio_stream->audio_rtcp_ctx->tb = 48000;	/* May change later */
 		audio_stream->video_rtcp_ctx = g_malloc0(sizeof(rtcp_context));
@@ -2979,7 +3001,9 @@ int janus_ice_setup_local(janus_ice_handle *handle, int offer, int audio, int vi
 		video_stream->dtls_role = offer ? JANUS_DTLS_ROLE_CLIENT : JANUS_DTLS_ROLE_ACTPASS;
 		video_stream->video_ssrc = janus_random_uint32();	/* FIXME Should we look for conflicts? */
 		video_stream->video_ssrc_peer = 0;	/* FIXME Right now we don't know what this will be */
-		video_stream->video_ssrc_peer_rtx = 0;	/* FIXME Right now we don't know what this will be */
+		video_stream->video_ssrc_peer_rtx = 0;		/* FIXME Right now we don't know if and what this will be */
+		video_stream->video_ssrc_peer_sim_1 = 0;	/* FIXME Right now we don't know if and what this will be */
+		video_stream->video_ssrc_peer_sim_2 = 0;	/* FIXME Right now we don't know if and what this will be */
 		video_stream->audio_ssrc = 0;
 		video_stream->audio_ssrc_peer = 0;
 		video_stream->video_rtcp_ctx = g_malloc0(sizeof(rtcp_context));
diff --git a/ice.h b/ice.h
index f1fb4f8..351b862 100644
--- a/ice.h
+++ b/ice.h
@@ -359,6 +359,12 @@ struct janus_ice_stream {
 	guint32 video_ssrc_peer;
 	/*! \brief Video retransmissions SSRC of the peer for this stream (may be bundled) */
 	guint32 video_ssrc_peer_rtx;
+	/*! \brief Video SSRC (simulcasted 1) of the peer for this stream (may be bundled) */
+	guint32 video_ssrc_peer_sim_1;
+	/*! \brief Video SSRC (simulcasted 2) of the peer for this stream (may be bundled) */
+	guint32 video_ssrc_peer_sim_2;
+	/*! \brief Array of RTP Stream IDs (for Firefox simulcasting, if enabled) */
+	char *rid[3];
 	/*! \brief List of payload types we can expect for audio */
 	GList *audio_payload_types;
 	/*! \brief List of payload types we can expect for video */
diff --git a/janus.c b/janus.c
index 8b24050..06b0447 100644
--- a/janus.c
+++ b/janus.c
@@ -1119,6 +1119,8 @@ int janus_process_incoming_request(janus_request *request) {
 								handle->audio_stream->video_ssrc = handle->video_stream->video_ssrc;
 								handle->audio_stream->video_ssrc_peer = handle->video_stream->video_ssrc_peer;
 								handle->audio_stream->video_ssrc_peer_rtx = handle->video_stream->video_ssrc_peer_rtx;
+								handle->audio_stream->video_ssrc_peer_sim_1 = handle->video_stream->video_ssrc_peer_sim_1;
+								handle->audio_stream->video_ssrc_peer_sim_2 = handle->video_stream->video_ssrc_peer_sim_2;
 								nice_agent_attach_recv(handle->agent, handle->video_stream->stream_id, 1, g_main_loop_get_context (handle->iceloop), NULL, NULL);
 								if(!handle->force_rtcp_mux && !janus_ice_is_rtcpmux_forced())
 									nice_agent_attach_recv(handle->agent, handle->video_stream->stream_id, 2, g_main_loop_get_context (handle->iceloop), NULL, NULL);
@@ -1345,9 +1347,30 @@ int janus_process_incoming_request(janus_request *request) {
 
 		/* Send the message to the plugin (which must eventually free transaction_text and unref the two objects, body and jsep) */
 		json_incref(body);
+		json_t *body_jsep = NULL;
+		if(jsep_sdp_stripped) {
+			body_jsep = json_pack("{ssss}", "type", jsep_type, "sdp", jsep_sdp_stripped);
+			/* Check if VP8 simulcasting is enabled */
+			if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_HAS_VIDEO)) {
+				if(handle->video_stream && handle->video_stream->video_ssrc_peer_sim_1) {
+					json_t *simulcast = json_object();
+					json_object_set(simulcast, "ssrc-0", json_integer(handle->video_stream->video_ssrc_peer));
+					json_object_set(simulcast, "ssrc-1", json_integer(handle->video_stream->video_ssrc_peer_sim_1));
+					if(handle->video_stream->video_ssrc_peer_sim_2)
+						json_object_set(simulcast, "ssrc-2", json_integer(handle->video_stream->video_ssrc_peer_sim_2));
+					json_object_set(body_jsep, "simulcast", simulcast);
+				} else if(handle->audio_stream && handle->audio_stream->video_ssrc_peer_sim_1) {
+					json_t *simulcast = json_object();
+					json_object_set(simulcast, "ssrc-0", json_integer(handle->audio_stream->video_ssrc_peer));
+					json_object_set(simulcast, "ssrc-1", json_integer(handle->audio_stream->video_ssrc_peer_sim_1));
+					if(handle->audio_stream->video_ssrc_peer_sim_2)
+						json_object_set(simulcast, "ssrc-2", json_integer(handle->audio_stream->video_ssrc_peer_sim_2));
+					json_object_set(body_jsep, "simulcast", simulcast);
+				}
+			}
+		};
 		janus_plugin_result *result = plugin_t->handle_message(handle->app_handle,
-			g_strdup((char *)transaction_text), body,
-			jsep_sdp_stripped ? json_pack("{ssss}", "type", jsep_type, "sdp", jsep_sdp_stripped) : NULL);
+			g_strdup((char *)transaction_text), body, body_jsep);
 		g_free(jsep_type);
 		g_free(jsep_sdp_stripped);
 		if(result == NULL) {
@@ -2346,6 +2369,19 @@ json_t *janus_admin_stream_summary(janus_ice_stream *stream) {
 		json_object_set_new(ss, "video-peer", json_integer(stream->video_ssrc_peer));
 	if(stream->video_ssrc_peer_rtx)
 		json_object_set_new(ss, "video-peer-rtx", json_integer(stream->video_ssrc_peer_rtx));
+	if(stream->video_ssrc_peer_sim_1)
+		json_object_set_new(ss, "video-peer-sim-1", json_integer(stream->video_ssrc_peer_sim_1));
+	if(stream->video_ssrc_peer_sim_2)
+		json_object_set_new(ss, "video-peer-sim-2", json_integer(stream->video_ssrc_peer_sim_2));
+	if(stream->rid[0]) {
+		json_t *rid = json_array();
+		json_array_append_new(rid, json_string(stream->rid[0]));
+		if(stream->rid[1])
+			json_array_append_new(rid, json_string(stream->rid[1]));
+		if(stream->rid[1])
+			json_array_append_new(rid, json_string(stream->rid[2]));
+		json_object_set_new(ss, "rid", rid);
+	}
 	json_object_set_new(s, "ssrc", ss);
 	json_t *components = json_array();
 	if(stream->rtp_component) {
@@ -2875,6 +2911,8 @@ json_t *janus_plugin_handle_sdp(janus_plugin_session *plugin_session, janus_plug
 						ice_handle->audio_stream->video_ssrc = ice_handle->video_stream->video_ssrc;
 						ice_handle->audio_stream->video_ssrc_peer = ice_handle->video_stream->video_ssrc_peer;
 						ice_handle->audio_stream->video_ssrc_peer_rtx = ice_handle->video_stream->video_ssrc_peer_rtx;
+						ice_handle->audio_stream->video_ssrc_peer_sim_1 = ice_handle->video_stream->video_ssrc_peer_sim_1;
+						ice_handle->audio_stream->video_ssrc_peer_sim_2 = ice_handle->video_stream->video_ssrc_peer_sim_2;
 						nice_agent_attach_recv(ice_handle->agent, ice_handle->video_stream->stream_id, 1, g_main_loop_get_context (ice_handle->iceloop), NULL, NULL);
 						if(!ice_handle->force_rtcp_mux && !janus_ice_is_rtcpmux_forced())
 							nice_agent_attach_recv(ice_handle->agent, ice_handle->video_stream->stream_id, 2, g_main_loop_get_context (ice_handle->iceloop), NULL, NULL);
diff --git a/plugins/janus_echotest.c b/plugins/janus_echotest.c
index 7fbb3d1..34ea725 100644
--- a/plugins/janus_echotest.c
+++ b/plugins/janus_echotest.c
@@ -92,6 +92,7 @@
 #include "../config.h"
 #include "../mutex.h"
 #include "../record.h"
+#include "../rtp.h"
 #include "../rtcp.h"
 #include "../sdp-utils.h"
 #include "../utils.h"
@@ -185,8 +186,15 @@ typedef struct janus_echotest_session {
 	gboolean audio_active;
 	gboolean video_active;
 	uint64_t bitrate;
+	janus_rtp_switching_context context;
+	uint32_t ssrc[3];		/* Only needed in case VP8 simulcasting is involved */
+	int rtpmapid_extmap_id;	/* Only needed in case Firefox's RID-based simulcasting is involved */
+	char *rid[3];			/* Only needed in case Firefox's RID-based simulcasting is involved */
+	int simulcast;			/* Which simulcast "layer" we should forward back */
+	int simulcast_target;	/* As above, but to handle transitions (e.g., wait for keyframe) */
+	janus_vp8_simulcast_context simulcast_context;
 	janus_recorder *arc;	/* The Janus recorder instance for this user's audio, if enabled */
-	janus_recorder *vrc;	/* The Janus recorder instance for this user's video, if enabled */
+	janus_recorder *vrc[3];	/* The Janus recorder instance for this user's video, if enabled (there may be more if we're simulcasting) */
 	janus_recorder *drc;	/* The Janus recorder instance for this user's data, if enabled */
 	janus_mutex rec_mutex;	/* Mutex to protect the recorders from race conditions */
 	guint16 slowlink_count;
@@ -387,6 +395,13 @@ void janus_echotest_create_session(janus_plugin_session *handle, int *error) {
 	session->video_active = TRUE;
 	janus_mutex_init(&session->rec_mutex);
 	session->bitrate = 0;	/* No limit */
+	janus_rtp_switching_context_reset(&session->context);
+	session->ssrc[0] = 0;
+	session->ssrc[1] = 0;
+	session->ssrc[2] = 0;
+	session->simulcast = -1;
+	session->simulcast_target = 0;
+	janus_vp8_simulcast_context_reset(&session->simulcast_context);
 	session->destroyed = 0;
 	g_atomic_int_set(&session->hangingup, 0);
 	handle->plugin_handle = session;
@@ -434,12 +449,18 @@ json_t *janus_echotest_query_session(janus_plugin_session *handle) {
 	json_object_set_new(info, "audio_active", session->audio_active ? json_true() : json_false());
 	json_object_set_new(info, "video_active", session->video_active ? json_true() : json_false());
 	json_object_set_new(info, "bitrate", json_integer(session->bitrate));
-	if(session->arc || session->vrc || session->drc) {
+	if(session->ssrc[0] != 0)
+		json_object_set_new(info, "simulcast", json_true());
+	if(session->arc || session->vrc[0] || session->drc) {
 		json_t *recording = json_object();
 		if(session->arc && session->arc->filename)
 			json_object_set_new(recording, "audio", json_string(session->arc->filename));
-		if(session->vrc && session->vrc->filename)
-			json_object_set_new(recording, "video", json_string(session->vrc->filename));
+		if(session->vrc[0] && session->vrc[0]->filename)
+			json_object_set_new(recording, "video", json_string(session->vrc[0]->filename));
+		if(session->vrc[1] && session->vrc[1]->filename)
+			json_object_set_new(recording, "video-sim1", json_string(session->vrc[1]->filename));
+		if(session->vrc[2] && session->vrc[2]->filename)
+			json_object_set_new(recording, "video-sim2", json_string(session->vrc[2]->filename));
 		if(session->drc && session->drc->filename)
 			json_object_set_new(recording, "data", json_string(session->drc->filename));
 		json_object_set_new(info, "recording", recording);
@@ -494,11 +515,74 @@ void janus_echotest_incoming_rtp(janus_plugin_session *handle, int video, char *
 		}
 		if(session->destroyed)
 			return;
-		if((!video && session->audio_active) || (video && session->video_active)) {
+		if(video && session->video_active && session->rtpmapid_extmap_id != -1) {
+			/* FIXME Experiments with Firefox simulcasting */
+			rtp_header *header = (rtp_header *)buf;
+			uint32_t seq_number = ntohs(header->seq_number);
+			uint32_t timestamp = ntohl(header->timestamp);
+			uint32_t ssrc = ntohl(header->ssrc);
+			char sdes_item[16];
+			if(janus_rtp_header_extension_parse_rtp_stream_id(buf, len, session->rtpmapid_extmap_id, sdes_item, sizeof(sdes_item)) == 0) {
+				JANUS_LOG(LOG_DBG, "%"SCNu32"/%"SCNu16"/%"SCNu32": RTP stream ID extension: %s\n", ssrc, seq_number, timestamp, sdes_item);
+			}
+		}
+		if(video && session->video_active && session->ssrc[0] != 0) {
+			/* Handle simulcast: don't relay if it's not the SSRC we wanted to handle */
+			rtp_header *header = (rtp_header *)buf;
+			uint32_t seq_number = ntohs(header->seq_number);
+			uint32_t timestamp = ntohl(header->timestamp);
+			uint32_t ssrc = ntohl(header->ssrc);
 			/* Save the frame if we're recording */
-			janus_recorder_save_frame(video ? session->vrc : session->arc, buf, len);
+			if(ssrc == session->ssrc[0])
+				janus_recorder_save_frame(session->vrc[0], buf, len);
+			else if(ssrc == session->ssrc[1])
+				janus_recorder_save_frame(session->vrc[1], buf, len);
+			else if(ssrc == session->ssrc[2])
+				janus_recorder_save_frame(session->vrc[2], buf, len);
+			/* Access the packet payload */
+			int plen = 0;
+			char *payload = janus_rtp_payload(buf, len, &plen);
+			if(payload == NULL)
+				return;
+			gboolean switched = FALSE;
+			if(session->simulcast != session->simulcast_target) {
+				/* There has been a change: let's wait for a keyframe on the target */
+				if(ssrc == session->ssrc[session->simulcast_target]) {
+					if(janus_vp8_is_keyframe(payload, plen)) {
+						JANUS_LOG(LOG_WARN, "Received keyframe on SSRC %"SCNu32", switching (was %"SCNu32")\n",
+							ssrc, session->ssrc[session->simulcast]);
+						session->simulcast = session->simulcast_target;
+						switched = TRUE;
+						/* Notify the user */
+						json_t *event = json_object();
+						json_object_set_new(event, "echotest", json_string("event"));
+						json_object_set_new(event, "simulcast", json_integer(session->simulcast));
+						gateway->push_event(session->handle, &janus_echotest_plugin, NULL, event, NULL);
+						json_decref(event);
+					} else {
+						JANUS_LOG(LOG_WARN, "Not a keyframe on SSRC %"SCNu32" yet, waiting before switching\n", ssrc);
+					}
+				}
+			}
+			if(ssrc != session->ssrc[session->simulcast]) {
+				JANUS_LOG(LOG_HUGE, "Dropping packet (it's from SSRC %"SCNu32", but we're only relaying SSRC %"SCNu32" now\n",
+					ssrc, session->ssrc[session->simulcast]);
+				return;
+			}
+			janus_rtp_header_update(header, &session->context, TRUE, 4500);
+			janus_vp8_simulcast_descriptor_update(payload, plen, &session->simulcast_context, switched);
 			/* Send the frame back */
 			gateway->relay_rtp(handle, video, buf, len);
+			/* Restore header or core statistics will be messed up */
+			header->timestamp = htonl(timestamp);
+			header->seq_number = htons(seq_number);
+		} else {
+			if((!video && session->audio_active) || (video && session->video_active)) {
+				/* Save the frame if we're recording */
+				janus_recorder_save_frame(video ? session->vrc[0] : session->arc, buf, len);
+				/* Send the frame back */
+				gateway->relay_rtp(handle, video, buf, len);
+			}
 		}
 	}
 }
@@ -625,12 +709,24 @@ void janus_echotest_hangup_media(janus_plugin_session *handle) {
 		janus_recorder_free(session->arc);
 	}
 	session->arc = NULL;
-	if(session->vrc) {
-		janus_recorder_close(session->vrc);
-		JANUS_LOG(LOG_INFO, "Closed video recording %s\n", session->vrc->filename ? session->vrc->filename : "??");
-		janus_recorder_free(session->vrc);
+	if(session->vrc[0]) {
+		janus_recorder_close(session->vrc[0]);
+		JANUS_LOG(LOG_INFO, "Closed video recording %s\n", session->vrc[0]->filename ? session->vrc[0]->filename : "??");
+		janus_recorder_free(session->vrc[0]);
+	}
+	session->vrc[0] = NULL;
+	if(session->vrc[1]) {
+		janus_recorder_close(session->vrc[1]);
+		JANUS_LOG(LOG_INFO, "Closed video recording %s (simulcasting #1)\n", session->vrc[1]->filename ? session->vrc[1]->filename : "??");
+		janus_recorder_free(session->vrc[1]);
+	}
+	session->vrc[1] = NULL;
+	if(session->vrc[2]) {
+		janus_recorder_close(session->vrc[2]);
+		JANUS_LOG(LOG_INFO, "Closed video recording %s (simulcasting #2)\n", session->vrc[2]->filename ? session->vrc[2]->filename : "??");
+		janus_recorder_free(session->vrc[2]);
 	}
-	session->vrc = NULL;
+	session->vrc[2] = NULL;
 	if(session->drc) {
 		janus_recorder_close(session->drc);
 		JANUS_LOG(LOG_INFO, "Closed data recording %s\n", session->drc->filename ? session->drc->filename : "??");
@@ -645,6 +741,11 @@ void janus_echotest_hangup_media(janus_plugin_session *handle) {
 	session->audio_active = TRUE;
 	session->video_active = TRUE;
 	session->bitrate = 0;
+	session->ssrc[0] = 0;
+	session->ssrc[1] = 0;
+	session->ssrc[2] = 0;
+	session->simulcast = -1;
+	session->simulcast_target = 0;
 }
 
 /* Thread to handle incoming messages */
@@ -697,6 +798,14 @@ static void *janus_echotest_handler(void *data) {
 		/* Parse request */
 		const char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, "type"));
 		const char *msg_sdp = json_string_value(json_object_get(msg->jsep, "sdp"));
+		json_t *msg_simulcast = json_object_get(msg->jsep, "simulcast");
+		if(msg_simulcast) {
+			JANUS_LOG(LOG_WARN, "EchoTest client is going to do simulcasting\n");
+			session->ssrc[0] = json_integer_value(json_object_get(msg_simulcast, "ssrc-0"));
+			session->ssrc[1] = json_integer_value(json_object_get(msg_simulcast, "ssrc-1"));
+			session->ssrc[2] = json_integer_value(json_object_get(msg_simulcast, "ssrc-2"));
+			session->simulcast_target = 0;	/* Let's start with low quality */
+		}
 		json_t *audio = json_object_get(root, "audio");
 		if(audio && !json_is_boolean(audio)) {
 			JANUS_LOG(LOG_ERR, "Invalid element (audio should be a boolean)\n");
@@ -718,6 +827,13 @@ static void *janus_echotest_handler(void *data) {
 			g_snprintf(error_cause, 512, "Invalid value (bitrate should be a positive integer)");
 			goto error;
 		}
+		json_t *simulcast = json_object_get(root, "simulcast");
+		if(simulcast && (!json_is_integer(simulcast) || (json_integer_value(simulcast) < 0 && json_integer_value(simulcast) > 2))) {
+			JANUS_LOG(LOG_ERR, "Invalid element (simulcast should be 0, 1 or 2)\n");
+			error_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT;
+			g_snprintf(error_cause, 512, "Invalid value (simulcast should be 0, 1 or 2)");
+			goto error;
+		}
 		json_t *record = json_object_get(root, "record");
 		if(record && !json_is_boolean(record)) {
 			JANUS_LOG(LOG_ERR, "Invalid element (record should be a boolean)\n");
@@ -762,6 +878,17 @@ static void *janus_echotest_handler(void *data) {
 				/* FIXME How should we handle a subsequent "no limit" bitrate? */
 			}
 		}
+		if(simulcast) {
+			session->simulcast_target = json_integer_value(simulcast);
+			JANUS_LOG(LOG_VERB, "Setting video SSRC to let through (simulcast): %"SCNu32" (index %d, was %d)\n",
+				session->ssrc[session->simulcast], session->simulcast_target, session->simulcast);
+			/* Send a PLI */
+			JANUS_LOG(LOG_VERB, "Simulcasting change, sending a PLI to kickstart it\n");
+			char buf[12];
+			memset(buf, 0, 12);
+			janus_rtcp_pli((char *)&buf, 12);
+			gateway->relay_rtcp(session->handle, 1, buf, 12);
+		}
 		if(record) {
 			if(msg_sdp) {
 				session->has_audio = (strstr(msg_sdp, "m=audio") != NULL);
@@ -780,12 +907,24 @@ static void *janus_echotest_handler(void *data) {
 					janus_recorder_free(session->arc);
 				}
 				session->arc = NULL;
-				if(session->vrc) {
-					janus_recorder_close(session->vrc);
-					JANUS_LOG(LOG_INFO, "Closed video recording %s\n", session->vrc->filename ? session->vrc->filename : "??");
-					janus_recorder_free(session->vrc);
+				if(session->vrc[0]) {
+					janus_recorder_close(session->vrc[0]);
+					JANUS_LOG(LOG_INFO, "Closed video recording %s\n", session->vrc[0]->filename ? session->vrc[0]->filename : "??");
+					janus_recorder_free(session->vrc[0]);
+				}
+				session->vrc[0] = NULL;
+				if(session->vrc[1]) {
+					janus_recorder_close(session->vrc[1]);
+					JANUS_LOG(LOG_INFO, "Closed video recording %s (simulcasting #1)\n", session->vrc[1]->filename ? session->vrc[1]->filename : "??");
+					janus_recorder_free(session->vrc[1]);
+				}
+				session->vrc[1] = NULL;
+				if(session->vrc[2]) {
+					janus_recorder_close(session->vrc[2]);
+					JANUS_LOG(LOG_INFO, "Closed video recording %s (simulcasting #2)\n", session->vrc[2]->filename ? session->vrc[2]->filename : "??");
+					janus_recorder_free(session->vrc[2]);
 				}
-				session->vrc = NULL;
+				session->vrc[2] = NULL;
 				if(session->drc) {
 					janus_recorder_close(session->drc);
 					JANUS_LOG(LOG_INFO, "Closed data recording %s\n", session->drc->filename ? session->drc->filename : "??");
@@ -823,19 +962,49 @@ static void *janus_echotest_handler(void *data) {
 					if(recording_base) {
 						/* Use the filename and path we have been provided */
 						g_snprintf(filename, 255, "%s-video", recording_base);
-						session->vrc = janus_recorder_create(NULL, "vp8", filename);
-						if(session->vrc == NULL) {
+						session->vrc[0] = janus_recorder_create(NULL, "vp8", filename);
+						if(session->vrc[0] == NULL) {
 							/* FIXME We should notify the fact the recorder could not be created */
 							JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this EchoTest user!\n");
 						}
+						if(session->ssrc[0] != 0) {
+							/* Create recordings for the other layers as well */
+							g_snprintf(filename, 255, "%s-video-sim1", recording_base);
+							session->vrc[1] = janus_recorder_create(NULL, "vp8", filename);
+							if(session->vrc[1] == NULL) {
+								/* FIXME We should notify the fact the recorder could not be created */
+								JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #1) for this EchoTest user!\n");
+							}
+							g_snprintf(filename, 255, "%s-video-sim2", recording_base);
+							session->vrc[2] = janus_recorder_create(NULL, "vp8", filename);
+							if(session->vrc[2] == NULL) {
+								/* FIXME We should notify the fact the recorder could not be created */
+								JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #2) for this EchoTest user!\n");
+							}
+						}
 					} else {
 						/* Build a filename */
 						g_snprintf(filename, 255, "echotest-%p-%"SCNi64"-video", session, now);
-						session->vrc = janus_recorder_create(NULL, "vp8", filename);
-						if(session->vrc == NULL) {
+						session->vrc[0] = janus_recorder_create(NULL, "vp8", filename);
+						if(session->vrc[0] == NULL) {
 							/* FIXME We should notify the fact the recorder could not be created */
 							JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this EchoTest user!\n");
 						}
+						if(session->ssrc[0] != 0) {
+							/* Create recordings for the other layers as well */
+							g_snprintf(filename, 255, "echotest-%p-%"SCNi64"-video-sim1", session, now);
+							session->vrc[1] = janus_recorder_create(NULL, "vp8", filename);
+							if(session->vrc[1] == NULL) {
+								/* FIXME We should notify the fact the recorder could not be created */
+								JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #1) for this EchoTest user!\n");
+							}
+							g_snprintf(filename, 255, "echotest-%p-%"SCNi64"-video-sim2", session, now);
+							session->vrc[2] = janus_recorder_create(NULL, "vp8", filename);
+							if(session->vrc[2] == NULL) {
+								/* FIXME We should notify the fact the recorder could not be created */
+								JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #2) for this EchoTest user!\n");
+							}
+						}
 					}
 					/* Send a PLI */
 					JANUS_LOG(LOG_VERB, "Recording video, sending a PLI to kickstart it\n");
@@ -875,10 +1044,10 @@ static void *janus_echotest_handler(void *data) {
 			session->has_data = (strstr(msg_sdp, "DTLS/SCTP") != NULL);
 		}
 
-		if(!audio && !video && !bitrate && !record && !msg_sdp) {
-			JANUS_LOG(LOG_ERR, "No supported attributes (audio, video, bitrate, record, jsep) found\n");
+		if(!audio && !video && !bitrate && !simulcast && !record && !msg_sdp) {
+			JANUS_LOG(LOG_ERR, "No supported attributes (audio, video, bitrate, simulcast, record, jsep) found\n");
 			error_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT;
-			g_snprintf(error_cause, 512, "Message error: no supported attributes (audio, video, bitrate, record, jsep) found");
+			g_snprintf(error_cause, 512, "Message error: no supported attributes (audio, video, bitrate, simulcast, record, jsep) found");
 			goto error;
 		}
 
@@ -902,7 +1071,51 @@ static void *janus_echotest_handler(void *data) {
 				g_snprintf(error_cause, 512, "Error parsing offer: %s", error_str);
 				goto error;
 			}
+			/* Check if we need to negotiate the rtp-stream-id extension */
+			session->rtpmapid_extmap_id = -1;
+			janus_sdp_mdirection extmap_mdir = JANUS_SDP_SENDRECV;
+			GList *temp = offer->m_lines;
+			while(temp) {
+				/* Which media are available? */
+				janus_sdp_mline *m = (janus_sdp_mline *)temp->data;
+				if(m->type == JANUS_SDP_VIDEO && m->port > 0) {
+					/* Are the extmaps we care about there? */
+					GList *ma = m->attributes;
+					while(ma) {
+						janus_sdp_attribute *a = (janus_sdp_attribute *)ma->data;
+						if(a->value) {
+							if(strstr(a->value, JANUS_RTP_EXTMAP_RTP_STREAM_ID)) {
+								session->rtpmapid_extmap_id = atoi(a->value);
+								extmap_mdir = a->direction;
+								break;
+							}
+						}
+						ma = ma->next;
+					}
+				}
+				temp = temp->next;
+			}
 			janus_sdp *answer = janus_sdp_generate_answer(offer, JANUS_SDP_OA_DONE);
+			/* Add the extmap attribute, if needed */
+			if(session->rtpmapid_extmap_id > -1) {
+				/* First of all, let's check if the extmap attribute had a direction */
+				const char *direction = NULL;
+				switch(extmap_mdir) {
+					case JANUS_SDP_SENDONLY:
+						direction = "/recvonly";
+						break;
+					case JANUS_SDP_RECVONLY:
+					case JANUS_SDP_INACTIVE:
+						direction = "/inactive";
+						break;
+					default:
+						direction = "";
+						break;
+				}
+				janus_sdp_attribute *a = janus_sdp_attribute_create("extmap",
+					"%d%s %s\r\n", session->rtpmapid_extmap_id, direction, JANUS_RTP_EXTMAP_RTP_STREAM_ID);
+				janus_sdp_attribute_add_to_mline(janus_sdp_mline_find(answer, JANUS_SDP_VIDEO), a);
+			}
 			char *sdp = janus_sdp_write(answer);
 			janus_sdp_free(offer);
 			janus_sdp_free(answer);
@@ -926,12 +1139,24 @@ static void *janus_echotest_handler(void *data) {
 			json_object_set_new(info, "audio_active", session->audio_active ? json_true() : json_false());
 			json_object_set_new(info, "video_active", session->video_active ? json_true() : json_false());
 			json_object_set_new(info, "bitrate", json_integer(session->bitrate));
-			if(session->arc || session->vrc) {
+			if(session->ssrc[0] && session->ssrc[1]) {
+				json_t *simulcast = json_object();
+				json_object_set_new(simulcast, "ssrc-0", json_integer(session->ssrc[0]));
+				json_object_set_new(simulcast, "ssrc-1", json_integer(session->ssrc[1]));
+				json_object_set_new(simulcast, "ssrc-2", json_integer(session->ssrc[2]));
+				json_object_set_new(simulcast, "simulcast", json_integer(session->simulcast));
+				json_object_set_new(info, "simulcast", simulcast);
+			}
+			if(session->arc || session->vrc[0]) {
 				json_t *recording = json_object();
 				if(session->arc && session->arc->filename)
 					json_object_set_new(recording, "audio", json_string(session->arc->filename));
-				if(session->vrc && session->vrc->filename)
-					json_object_set_new(recording, "video", json_string(session->vrc->filename));
+				if(session->vrc[0] && session->vrc[0]->filename)
+					json_object_set_new(recording, "video", json_string(session->vrc[0]->filename));
+				if(session->vrc[1] && session->vrc[1]->filename)
+					json_object_set_new(recording, "video-sim1", json_string(session->vrc[1]->filename));
+				if(session->vrc[2] && session->vrc[2]->filename)
+					json_object_set_new(recording, "video-sim2", json_string(session->vrc[2]->filename));
 				json_object_set_new(info, "recording", recording);
 			}
 			gateway->notify_event(&janus_echotest_plugin, session->handle, info);
diff --git a/plugins/janus_streaming.c b/plugins/janus_streaming.c
index 2ed2213..fafb578 100644
--- a/plugins/janus_streaming.c
+++ b/plugins/janus_streaming.c
@@ -327,7 +327,6 @@ static void *janus_streaming_ondemand_thread(void *data);
 static void *janus_streaming_filesource_thread(void *data);
 static void janus_streaming_relay_rtp_packet(gpointer data, gpointer user_data);
 static void *janus_streaming_relay_thread(void *data);
-static gboolean janus_streaming_is_keyframe(gint codec, char* buffer, int len);
 
 typedef enum janus_streaming_type {
 	janus_streaming_type_none = 0,
@@ -4067,24 +4066,50 @@ static void *janus_streaming_relay_thread(void *data) {
 							pkt->seq_number = ntohs(rtp->seq_number);
 							source->keyframe.temp_keyframe = g_list_append(source->keyframe.temp_keyframe, pkt);
 							janus_mutex_unlock(&source->keyframe.mutex);
-						} else if(janus_streaming_is_keyframe(mountpoint->codecs.video_codec, buffer, bytes)) {
-							/* New keyframe, start saving it */
-							source->keyframe.temp_ts = ntohl(rtp->timestamp);
-							JANUS_LOG(LOG_HUGE, "[%s] New keyframe received! ts=%"SCNu32"\n", name, source->keyframe.temp_ts);
-							janus_mutex_lock(&source->keyframe.mutex);
-							janus_streaming_rtp_relay_packet *pkt = g_malloc0(sizeof(janus_streaming_rtp_relay_packet));
-							pkt->data = g_malloc0(bytes);
-							memcpy(pkt->data, buffer, bytes);
-							pkt->data->ssrc = htons(1);
-							pkt->data->type = mountpoint->codecs.video_pt;
-							packet.is_rtp = TRUE;
-							packet.is_video = TRUE;
-							packet.is_keyframe = TRUE;
-							pkt->length = bytes;
-							pkt->timestamp = source->keyframe.temp_ts;
-							pkt->seq_number = ntohs(rtp->seq_number);
-							source->keyframe.temp_keyframe = g_list_append(source->keyframe.temp_keyframe, pkt);
-							janus_mutex_unlock(&source->keyframe.mutex);
+						} else {
+							gboolean kf = FALSE;
+							/* Parse RTP header first */
+							rtp_header *header = (rtp_header *)buffer;
+							guint32 timestamp = ntohl(header->timestamp);
+							guint16 seq = ntohs(header->seq_number);
+							JANUS_LOG(LOG_HUGE, "Checking if packet (size=%d, seq=%"SCNu16", ts=%"SCNu32") is a key frame...\n",
+								bytes, seq, timestamp);
+							int plen = 0;
+							char *payload = janus_rtp_payload(buffer, bytes, &plen);
+							if(payload) {
+								switch(mountpoint->codecs.video_codec) {
+									case JANUS_STREAMING_VP8:
+										kf = janus_vp8_is_keyframe(payload, plen);
+										break;
+									case JANUS_STREAMING_VP9:
+										kf = janus_vp9_is_keyframe(payload, plen);
+										break;
+									case JANUS_STREAMING_H264:
+										kf = janus_h264_is_keyframe(payload, plen);
+										break;
+									default:
+										break;
+								}
+								if(kf) {
+									/* New keyframe, start saving it */
+									source->keyframe.temp_ts = ntohl(rtp->timestamp);
+									JANUS_LOG(LOG_HUGE, "[%s] New keyframe received! ts=%"SCNu32"\n", name, source->keyframe.temp_ts);
+									janus_mutex_lock(&source->keyframe.mutex);
+									janus_streaming_rtp_relay_packet *pkt = g_malloc0(sizeof(janus_streaming_rtp_relay_packet));
+									pkt->data = g_malloc0(bytes);
+									memcpy(pkt->data, buffer, bytes);
+									pkt->data->ssrc = htons(1);
+									pkt->data->type = mountpoint->codecs.video_pt;
+									packet.is_rtp = TRUE;
+									packet.is_video = TRUE;
+									packet.is_keyframe = TRUE;
+									pkt->length = bytes;
+									pkt->timestamp = source->keyframe.temp_ts;
+									pkt->seq_number = ntohs(rtp->seq_number);
+									source->keyframe.temp_keyframe = g_list_append(source->keyframe.temp_keyframe, pkt);
+									janus_mutex_unlock(&source->keyframe.mutex);
+								}
+							}
 						}
 					}
 					/* If paused, ignore this packet */
@@ -4242,205 +4267,3 @@ static void janus_streaming_relay_rtp_packet(gpointer data, gpointer user_data)
 
 	return;
 }
-
-/* Helpers to check if frame is a key frame (see post processor code) */
-#if defined(__ppc__) || defined(__ppc64__)
-	# define swap2(d)  \
-	((d&0x000000ff)<<8) |  \
-	((d&0x0000ff00)>>8)
-#else
-	# define swap2(d) d
-#endif
-
-static gboolean janus_streaming_is_keyframe(gint codec, char* buffer, int len) {
-	if(codec == JANUS_STREAMING_VP8) {
-		/* VP8 packet */
-		if(!buffer || len < 28)
-			return FALSE;
-		/* Parse RTP header first */
-		rtp_header *header = (rtp_header *)buffer;
-		guint32 timestamp = ntohl(header->timestamp);
-		guint16 seq = ntohs(header->seq_number);
-		JANUS_LOG(LOG_HUGE, "Checking if VP8 packet (size=%d, seq=%"SCNu16", ts=%"SCNu32") is a key frame...\n",
-			len, seq, timestamp);
-		int plen = 0;
-		buffer = janus_rtp_payload(buffer, len, &plen);
-		if(!buffer) {
-			JANUS_LOG(LOG_WARN, "Couldn't access RTP payload\n");
-			return FALSE;
-		}
-		/* Parse VP8 header now */
-		uint8_t vp8pd = *buffer;
-		uint8_t xbit = (vp8pd & 0x80);
-		uint8_t sbit = (vp8pd & 0x10);
-		if(xbit) {
-			JANUS_LOG(LOG_HUGE, "  -- X bit is set!\n");
-			/* Read the Extended control bits octet */
-			buffer++;
-			vp8pd = *buffer;
-			uint8_t ibit = (vp8pd & 0x80);
-			uint8_t lbit = (vp8pd & 0x40);
-			uint8_t tbit = (vp8pd & 0x20);
-			uint8_t kbit = (vp8pd & 0x10);
-			if(ibit) {
-				JANUS_LOG(LOG_HUGE, "  -- I bit is set!\n");
-				/* Read the PictureID octet */
-				buffer++;
-				vp8pd = *buffer;
-				uint16_t picid = vp8pd, wholepicid = picid;
-				uint8_t mbit = (vp8pd & 0x80);
-				if(mbit) {
-					JANUS_LOG(LOG_HUGE, "  -- M bit is set!\n");
-					memcpy(&picid, buffer, sizeof(uint16_t));
-					wholepicid = ntohs(picid);
-					picid = (wholepicid & 0x7FFF);
-					buffer++;
-				}
-				JANUS_LOG(LOG_HUGE, "  -- -- PictureID: %"SCNu16"\n", picid);
-			}
-			if(lbit) {
-				JANUS_LOG(LOG_HUGE, "  -- L bit is set!\n");
-				/* Read the TL0PICIDX octet */
-				buffer++;
-				vp8pd = *buffer;
-			}
-			if(tbit || kbit) {
-				JANUS_LOG(LOG_HUGE, "  -- T/K bit is set!\n");
-				/* Read the TID/KEYIDX octet */
-				buffer++;
-				vp8pd = *buffer;
-			}
-			buffer++;	/* Now we're in the payload */
-			if(sbit) {
-				JANUS_LOG(LOG_HUGE, "  -- S bit is set!\n");
-				unsigned long int vp8ph = 0;
-				memcpy(&vp8ph, buffer, 4);
-				vp8ph = ntohl(vp8ph);
-				uint8_t pbit = ((vp8ph & 0x01000000) >> 24);
-				if(!pbit) {
-					JANUS_LOG(LOG_HUGE, "  -- P bit is NOT set!\n");
-					/* It is a key frame! Get resolution for debugging */
-					unsigned char *c = (unsigned char *)buffer+3;
-					/* vet via sync code */
-					if(c[0]!=0x9d||c[1]!=0x01||c[2]!=0x2a) {
-						JANUS_LOG(LOG_WARN, "First 3-bytes after header not what they're supposed to be?\n");
-					} else {
-						int vp8w = swap2(*(unsigned short*)(c+3))&0x3fff;
-						int vp8ws = swap2(*(unsigned short*)(c+3))>>14;
-						int vp8h = swap2(*(unsigned short*)(c+5))&0x3fff;
-						int vp8hs = swap2(*(unsigned short*)(c+5))>>14;
-						JANUS_LOG(LOG_HUGE, "Got a VP8 key frame: %dx%d (scale=%dx%d)\n", vp8w, vp8h, vp8ws, vp8hs);
-						return TRUE;
-					}
-				}
-			}
-		}
-		/* If we got here it's not a key frame */
-		return FALSE;
-	} else if(codec == JANUS_STREAMING_VP9) {
-		/* Parse RTP header first */
-		rtp_header *header = (rtp_header *)buffer;
-		guint32 timestamp = ntohl(header->timestamp);
-		guint16 seq = ntohs(header->seq_number);
-		JANUS_LOG(LOG_HUGE, "Checking if VP9 packet (size=%d, seq=%"SCNu16", ts=%"SCNu32") is a key frame...\n",
-			len, seq, timestamp);
-		int plen = 0;
-		buffer = janus_rtp_payload(buffer, len, &plen);
-		if(!buffer) {
-			JANUS_LOG(LOG_WARN, "Couldn't access RTP payload\n");
-			return FALSE;
-		}
-		/* Parse VP9 header now */
-		uint8_t vp9pd = *buffer;
-		uint8_t ibit = (vp9pd & 0x80);
-		uint8_t pbit = (vp9pd & 0x40);
-		uint8_t lbit = (vp9pd & 0x20);
-		uint8_t fbit = (vp9pd & 0x10);
-		uint8_t vbit = (vp9pd & 0x02);
-		buffer++;
-		if(ibit) {
-			/* Read the PictureID octet */
-			vp9pd = *buffer;
-			uint16_t picid = vp9pd, wholepicid = picid;
-			uint8_t mbit = (vp9pd & 0x80);
-			if(!mbit) {
-				buffer++;
-			} else {
-				memcpy(&picid, buffer, sizeof(uint16_t));
-				wholepicid = ntohs(picid);
-				picid = (wholepicid & 0x7FFF);
-				buffer += 2;
-			}
-		}
-		if(lbit) {
-			buffer++;
-			if(!fbit) {
-				/* Non-flexible mode, skip TL0PICIDX */
-				buffer++;
-			}
-		}
-		if(fbit && pbit) {
-			/* Skip reference indices */
-			uint8_t nbit = 1;
-			while(nbit) {
-				vp9pd = *buffer;
-				nbit = (vp9pd & 0x01);
-				buffer++;
-			}
-		}
-		if(vbit) {
-			/* Parse and skip SS */
-			vp9pd = *buffer;
-			uint n_s = (vp9pd & 0xE0) >> 5;
-			n_s++;
-			uint8_t ybit = (vp9pd & 0x10);
-			if(ybit) {
-				/* Iterate on all spatial layers and get resolution */
-				buffer++;
-				uint i=0;
-				for(i=0; i<n_s; i++) {
-					/* Width */
-					uint16_t *w = (uint16_t *)buffer;
-					int vp9w = ntohs(*w);
-					buffer += 2;
-					/* Height */
-					uint16_t *h = (uint16_t *)buffer;
-					int vp9h = ntohs(*h);
-					buffer += 2;
-					JANUS_LOG(LOG_WARN, "Got a VP9 key frame: %dx%d\n", vp9w, vp9h);
-					return TRUE;
-				}
-			}
-		}
-		/* If we got here it's not a key frame */
-		return FALSE;
-	} else if(codec == JANUS_STREAMING_H264) {
-		/* Parse RTP header first */
-		rtp_header *header = (rtp_header *)buffer;
-		guint32 timestamp = ntohl(header->timestamp);
-		guint16 seq = ntohs(header->seq_number);
-		JANUS_LOG(LOG_HUGE, "Checking if H264 packet (size=%d, seq=%"SCNu16", ts=%"SCNu32") is a key frame...\n",
-			len, seq, timestamp);
-		int plen = 0;
-		buffer = janus_rtp_payload(buffer, len, &plen);
-		if(!buffer) {
-			JANUS_LOG(LOG_WARN, "Couldn't access RTP payload\n");
-			return FALSE;
-		}
-		/* Parse H264 header now */
-		uint8_t fragment = *buffer & 0x1F;
-		uint8_t nal = *(buffer+1) & 0x1F;
-		uint8_t start_bit = *(buffer+1) & 0x80;
-		JANUS_LOG(LOG_HUGE, "Fragment=%d, NAL=%d, Start=%d\n", fragment, nal, start_bit);
-		if(fragment == 5 ||
-				((fragment == 28 || fragment == 29) && nal == 5 && start_bit == 128)) {
-			JANUS_LOG(LOG_HUGE, "Got an H264 key frame\n");
-			return TRUE;
-		}
-		/* If we got here it's not a key frame */
-		return FALSE;
-	} else {
-		/* FIXME Not a clue */
-		return FALSE;
-	}
-}
diff --git a/plugins/janus_videoroom.c b/plugins/janus_videoroom.c
index 68d7066..7317208 100644
--- a/plugins/janus_videoroom.c
+++ b/plugins/janus_videoroom.c
@@ -462,8 +462,8 @@ typedef struct janus_videoroom {
 	janus_videoroom_videocodec vcodec;	/* Video codec to force on publishers*/
 	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 */
-	int audio_level_average;	/* average audio level */
+	int audio_active_packets;	/* Amount of packets with audio level for checkup */
+	int audio_level_average;	/* Average audio level */
 	gboolean videoorient_ext;	/* Whether the video-orientation extension must be negotiated or not for new publishers */
 	gboolean playoutdelay_ext;	/* Whether the playout-delay extension must be negotiated or not for new publishers */
 	gboolean record;			/* Whether the feeds from publishers in this room should be recorded */
@@ -515,6 +515,7 @@ typedef struct janus_videoroom_participant {
 	guint32 video_pt;		/* Video payload type (depends on room configuration) */
 	guint32 audio_ssrc;		/* Audio SSRC of this publisher */
 	guint32 video_ssrc;		/* Video SSRC of this publisher */
+	uint32_t ssrc[3];		/* Only needed in case VP8 simulcasting is involved */
 	guint8 audio_level_extmap_id;	/* Audio level extmap ID */
 	guint8 video_orient_extmap_id;	/* Video orientation extmap ID */
 	guint8 playout_delay_extmap_id;	/* Playout delay extmap ID */
@@ -534,7 +535,7 @@ typedef struct janus_videoroom_participant {
 	gboolean recording_active;	/* Whether this publisher has to be recorded or not */
 	gchar *recording_base;	/* Base name for the recording (e.g., /path/to/filename, will generate /path/to/filename-audio.mjr and/or /path/to/filename-video.mjr */
 	janus_recorder *arc;	/* The Janus recorder instance for this publisher's audio, if enabled */
-	janus_recorder *vrc;	/* The Janus recorder instance for this publisher's video, if enabled */
+	janus_recorder *vrc[3];	/* The Janus recorder instance for this user's video, if enabled (there may be more if we're simulcasting) */
 	janus_recorder *drc;	/* The Janus recorder instance for this publisher's data, if enabled */
 	janus_mutex rec_mutex;	/* Mutex to protect the recorders from race conditions */
 	GSList *listeners;		/* Subscriptions to this publisher (who's watching this publisher)  */
@@ -556,6 +557,9 @@ typedef struct janus_videoroom_listener {
 	janus_videoroom_participant *feed;	/* Participant this listener is subscribed to */
 	guint32 pvt_id;		/* Private ID of the participant that is subscribing (if available/provided) */
 	janus_rtp_switching_context context;	/* Needed in case there are publisher switches on this listener */
+	int simulcast;			/* Which simulcast "layer" we should forward back, in case the publisher is simulcasting */
+	int simulcast_target;	/* As above, but to handle transitions (e.g., wait for keyframe) */
+	janus_vp8_simulcast_context simulcast_context;
 	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 */
@@ -1152,12 +1156,16 @@ json_t *janus_videoroom_query_session(janus_plugin_session *handle) {
 				json_object_set_new(media, "data", json_integer(participant->data));
 				json_object_set_new(info, "media", media);
 				json_object_set_new(info, "bitrate", json_integer(participant->bitrate));
-				if(participant->arc || participant->vrc || participant->drc) {
+				if(participant->arc || participant->vrc[0] || participant->drc) {
 					json_t *recording = json_object();
 					if(participant->arc && participant->arc->filename)
 						json_object_set_new(recording, "audio", json_string(participant->arc->filename));
-					if(participant->vrc && participant->vrc->filename)
-						json_object_set_new(recording, "video", json_string(participant->vrc->filename));
+					if(participant->vrc[0] && participant->vrc[0]->filename)
+						json_object_set_new(recording, "video", json_string(participant->vrc[0]->filename));
+					if(participant->vrc[1] && participant->vrc[1]->filename)
+						json_object_set_new(recording, "video-sim1", json_string(participant->vrc[1]->filename));
+					if(participant->vrc[2] && participant->vrc[0]->filename)
+						json_object_set_new(recording, "video-sim2", json_string(participant->vrc[2]->filename));
 					if(participant->drc && participant->drc->filename)
 						json_object_set_new(recording, "data", json_string(participant->drc->filename));
 					json_object_set_new(info, "recording", recording);
@@ -2361,9 +2369,20 @@ void janus_videoroom_incoming_rtp(janus_plugin_session *handle, int video, char
 	}
 
 	if((!video && participant->audio_active) || (video && participant->video_active)) {
+		rtp_header *rtp = (rtp_header *)buf;
+		uint32_t ssrc = ntohl(rtp->ssrc);
+		int sc = -1;
+		/* Check if we're simulcasting, and if so, keep track of the "layer" */
+		if(video && participant->ssrc[0] != 0) {
+			if(ssrc == participant->ssrc[0])
+				sc = 0;
+			else if(ssrc == participant->ssrc[1])
+				sc = 1;
+			else if(ssrc == participant->ssrc[2])
+				sc = 2;
+		}
 		/* Update payload type and SSRC */
 		janus_mutex_lock(&participant->rtp_forwarders_mutex);
-		rtp_header *rtp = (rtp_header *)buf;
 		rtp->type = video ? participant->video_pt : participant->audio_pt;
 		rtp->ssrc = htonl(video ? participant->video_ssrc : participant->audio_ssrc);
 		/* Forward RTP to the appropriate port for the rtp_forwarders associated with this publisher, if there are any */
@@ -2391,7 +2410,7 @@ void janus_videoroom_incoming_rtp(janus_plugin_session *handle, int video, char
 		}
 		janus_mutex_unlock(&participant->rtp_forwarders_mutex);
 		/* Save the frame if we're recording */
-		janus_recorder_save_frame(video ? participant->vrc : participant->arc, buf, len);
+		janus_recorder_save_frame(video ? (sc < 0 ? participant->vrc[0] : participant->vrc[sc]) : participant->arc, buf, len);
 		/* Done, relay it */
 		janus_videoroom_rtp_relay_packet packet;
 		packet.data = rtp;
@@ -2602,20 +2621,52 @@ static void janus_videoroom_recorder_create(janus_videoroom_participant *partici
 		if(participant->recording_base) {
 			/* Use the filename and path we have been provided */
 			g_snprintf(filename, 255, "%s-video", participant->recording_base);
-			participant->vrc = janus_recorder_create(participant->room->rec_dir,
+			participant->vrc[0] = janus_recorder_create(participant->room->rec_dir,
 				janus_videoroom_videocodec_name(participant->room->vcodec), filename);
-			if(participant->vrc == NULL) {
+			if(participant->vrc[0] == NULL) {
 				JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this publisher!\n");
 			}
+			if(participant->ssrc[0] != 0) {
+				/* Create recordings for the other layers as well */
+				g_snprintf(filename, 255, "%s-video-sim1", participant->recording_base);
+				participant->vrc[1] = janus_recorder_create(participant->room->rec_dir,
+					janus_videoroom_videocodec_name(participant->room->vcodec), filename);
+				if(participant->vrc[1] == NULL) {
+					JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #1) for this publisher!\n");
+				}
+				g_snprintf(filename, 255, "%s-video-sim2", participant->recording_base);
+				participant->vrc[2] = janus_recorder_create(participant->room->rec_dir,
+					janus_videoroom_videocodec_name(participant->room->vcodec), filename);
+				if(participant->vrc[2] == NULL) {
+					JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #2) for this publisher!\n");
+				}
+			}
 		} else {
 			/* Build a filename */
 			g_snprintf(filename, 255, "videoroom-%"SCNu64"-user-%"SCNu64"-%"SCNi64"-video",
 				participant->room->room_id, participant->user_id, now);
-			participant->vrc = janus_recorder_create(participant->room->rec_dir,
+			participant->vrc[0] = janus_recorder_create(participant->room->rec_dir,
 				janus_videoroom_videocodec_name(participant->room->vcodec), filename);
-			if(participant->vrc == NULL) {
+			if(participant->vrc[0] == NULL) {
 				JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this publisher!\n");
 			}
+			if(participant->ssrc[0] != 0) {
+				/* Create recordings for the other layers as well */
+				g_snprintf(filename, 255, "videoroom-%"SCNu64"-user-%"SCNu64"-%"SCNi64"-video-sim1",
+					participant->room->room_id, participant->user_id, now);
+				participant->vrc[1] = janus_recorder_create(participant->room->rec_dir,
+					janus_videoroom_videocodec_name(participant->room->vcodec), filename);
+				if(participant->vrc[1] == NULL) {
+					JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #1) for this publisher!\n");
+				}
+				g_snprintf(filename, 255, "videoroom-%"SCNu64"-user-%"SCNu64"-%"SCNi64"-video-sim2",
+					participant->room->room_id, participant->user_id, now);
+				participant->vrc[2] = janus_recorder_create(participant->room->rec_dir,
+					janus_videoroom_videocodec_name(participant->room->vcodec), filename);
+				if(participant->vrc[2] == NULL) {
+					JANUS_LOG(LOG_ERR, "Couldn't open an video recording file (simulcasting #2) for this publisher!\n");
+				}
+			}
 		}
 	}
 	if(data) {
@@ -2648,12 +2699,24 @@ static void janus_videoroom_recorder_close(janus_videoroom_participant *particip
 		janus_recorder_free(participant->arc);
 	}
 	participant->arc = NULL;
-	if(participant->vrc) {
-		janus_recorder_close(participant->vrc);
-		JANUS_LOG(LOG_INFO, "Closed video recording %s\n", participant->vrc->filename ? participant->vrc->filename : "??");
-		janus_recorder_free(participant->vrc);
-	}
-	participant->vrc = NULL;
+	if(participant->vrc[0]) {
+		janus_recorder_close(participant->vrc[0]);
+		JANUS_LOG(LOG_INFO, "Closed video recording %s\n", participant->vrc[0]->filename ? participant->vrc[0]->filename : "??");
+		janus_recorder_free(participant->vrc[0]);
+	}
+	participant->vrc[0] = NULL;
+	if(participant->vrc[1]) {
+		janus_recorder_close(participant->vrc[1]);
+		JANUS_LOG(LOG_INFO, "Closed video recording %s (simulcasting #1)\n", participant->vrc[1]->filename ? participant->vrc[1]->filename : "??");
+		janus_recorder_free(participant->vrc[1]);
+	}
+	participant->vrc[1] = NULL;
+	if(participant->vrc[2]) {
+		janus_recorder_close(participant->vrc[2]);
+		JANUS_LOG(LOG_INFO, "Closed video recording %s (simulcasting #2)\n", participant->vrc[2]->filename ? participant->vrc[2]->filename : "??");
+		janus_recorder_free(participant->vrc[2]);
+	}
+	participant->vrc[2] = NULL;
 	if(participant->drc) {
 		janus_recorder_close(participant->drc);
 		JANUS_LOG(LOG_INFO, "Closed data recording %s\n", participant->drc->filename ? participant->drc->filename : "??");
@@ -2897,7 +2960,9 @@ static void *janus_videoroom_handler(void *data) {
 				publisher->recording_active = FALSE;
 				publisher->recording_base = NULL;
 				publisher->arc = NULL;
-				publisher->vrc = NULL;
+				publisher->vrc[0] = NULL;
+				publisher->vrc[1] = NULL;
+				publisher->vrc[2] = NULL;
 				publisher->drc = NULL;
 				janus_mutex_init(&publisher->rec_mutex);
 				publisher->firefox = FALSE;
@@ -3087,6 +3152,9 @@ static void *janus_videoroom_handler(void *data) {
 					if(!publisher->data)
 						listener->data = FALSE;	/* ... unless the publisher isn't sending any data */
 					listener->paused = TRUE;	/* We need an explicit start from the listener */
+					listener->simulcast = -1;
+					listener->simulcast_target = 0;
+					janus_vp8_simulcast_context_reset(&listener->simulcast_context);
 					session->participant = listener;
 					janus_mutex_lock(&publisher->listeners_mutex);
 					publisher->listeners = g_slist_append(publisher->listeners, listener);
@@ -3292,12 +3360,16 @@ static void *janus_videoroom_handler(void *data) {
 					json_object_set_new(info, "video_active", participant->video_active ? json_true() : json_false());
 					json_object_set_new(info, "data_active", participant->data_active ? json_true() : json_false());
 					json_object_set_new(info, "bitrate", json_integer(participant->bitrate));
-					if(participant->arc || participant->vrc || participant->drc) {
+					if(participant->arc || participant->vrc[0] || participant->drc) {
 						json_t *recording = json_object();
 						if(participant->arc && participant->arc->filename)
 							json_object_set_new(recording, "audio", json_string(participant->arc->filename));
-						if(participant->vrc && participant->vrc->filename)
-							json_object_set_new(recording, "video", json_string(participant->vrc->filename));
+						if(participant->vrc[0] && participant->vrc[0]->filename)
+							json_object_set_new(recording, "video", json_string(participant->vrc[0]->filename));
+						if(participant->vrc[1] && participant->vrc[1]->filename)
+							json_object_set_new(recording, "video-sim1", json_string(participant->vrc[1]->filename));
+						if(participant->vrc[2] && participant->vrc[2]->filename)
+							json_object_set_new(recording, "video-sim2", json_string(participant->vrc[2]->filename));
 						if(participant->drc && participant->drc->filename)
 							json_object_set_new(recording, "data", json_string(participant->drc->filename));
 						json_object_set_new(info, "recording", recording);
@@ -3527,6 +3599,7 @@ static void *janus_videoroom_handler(void *data) {
 		/* Any SDP to handle? */
 		const char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, "type"));
 		const char *msg_sdp = json_string_value(json_object_get(msg->jsep, "sdp"));
+		json_t *msg_simulcast = json_object_get(msg->jsep, "simulcast");
 		if(!msg_sdp) {
 			int ret = gateway->push_event(msg->handle, &janus_videoroom_plugin, msg->transaction, event, NULL);
 			JANUS_LOG(LOG_VERB, "  >> %d (%s)\n", ret, janus_get_api_error(ret));
@@ -3784,6 +3857,18 @@ static void *janus_videoroom_handler(void *data) {
 				if(videoroom->record || participant->recording_active) {
 					janus_videoroom_recorder_create(participant, participant->audio, participant->video, participant->data);
 				}
+				/* Is simulcasting involved */
+				if(msg_simulcast && videoroom->vcodec == JANUS_VIDEOROOM_VP8) {
+					JANUS_LOG(LOG_WARN, "Publisher is going to do simulcasting\n");
+					participant->ssrc[0] = json_integer_value(json_object_get(msg_simulcast, "ssrc-0"));
+					participant->ssrc[1] = json_integer_value(json_object_get(msg_simulcast, "ssrc-1"));
+					participant->ssrc[2] = json_integer_value(json_object_get(msg_simulcast, "ssrc-2"));
+				} else {
+					/* No simulcasting involved */
+					participant->ssrc[0] = 0;
+					participant->ssrc[1] = 0;
+					participant->ssrc[2] = 0;
+				}
 				janus_mutex_unlock(&participant->rec_mutex);
 				/* Send the answer back to the publisher */
 				JANUS_LOG(LOG_VERB, "Handling publisher: turned this into an '%s':\n%s\n", type, answer_sdp);
@@ -3939,9 +4024,17 @@ static void janus_videoroom_participant_free(janus_videoroom_participant *p) {
 		janus_recorder_free(p->arc);
 		p->arc = NULL;
 	}
-	if(p->vrc) {
-		janus_recorder_free(p->vrc);
-		p->vrc = NULL;
+	if(p->vrc[0]) {
+		janus_recorder_free(p->vrc[0]);
+		p->vrc[0] = NULL;
+	}
+	if(p->vrc[1]) {
+		janus_recorder_free(p->vrc[1]);
+		p->vrc[1] = NULL;
+	}
+	if(p->vrc[2]) {
+		janus_recorder_free(p->vrc[2]);
+		p->vrc[2] = NULL;
 	}
 	if(p->drc) {
 		janus_recorder_free(p->drc);
diff --git a/postprocessing/pp-webm.c b/postprocessing/pp-webm.c
index 5cbfdb7..84f0856 100644
--- a/postprocessing/pp-webm.c
+++ b/postprocessing/pp-webm.c
@@ -167,7 +167,7 @@ int janus_pp_webm_preprocess(FILE *file, janus_pp_frame_packet *list, int vp8) {
 			uint8_t xbit = (vp8pd & 0x80);
 			uint8_t sbit = (vp8pd & 0x10);
 			/* Read the Extended control bits octet */
-			if (xbit) {
+			if(xbit) {
 				buffer++;
 				vp8pd = *buffer;
 				uint8_t ibit = (vp8pd & 0x80);
diff --git a/rtp.c b/rtp.c
index 1e04481..ae5ddce 100644
--- a/rtp.c
+++ b/rtp.c
@@ -90,6 +90,8 @@ const char *janus_rtp_header_extension_get_from_id(const char *sdp, int id) {
 						return JANUS_RTP_EXTMAP_ABS_SEND_TIME;
 					if(strstr(extension, JANUS_RTP_EXTMAP_CC_EXTENSIONS))
 						return JANUS_RTP_EXTMAP_CC_EXTENSIONS;
+					if(strstr(extension, JANUS_RTP_EXTMAP_RTP_STREAM_ID))
+						return JANUS_RTP_EXTMAP_RTP_STREAM_ID;
 					JANUS_LOG(LOG_ERR, "Unsupported extension '%s'\n", extension);
 					return NULL;
 				}
@@ -102,7 +104,8 @@ const char *janus_rtp_header_extension_get_from_id(const char *sdp, int id) {
 }
 
 /* Static helper to quickly find the extension data */
-static int janus_rtp_header_extension_find(char *buf, int len, int id, uint8_t *byte, uint32_t *word) {
+static int janus_rtp_header_extension_find(char *buf, int len, int id,
+		uint8_t *byte, uint32_t *word, char **ref) {
 	if(!buf || len < 12)
 		return -1;
 	rtp_header *rtp = (rtp_header *)buf;
@@ -110,7 +113,7 @@ static int janus_rtp_header_extension_find(char *buf, int len, int id, uint8_t *
 	if(rtp->csrccount)	/* Skip CSRC if needed */
 		hlen += rtp->csrccount*4;
 	if(rtp->extension) {
-		janus_rtp_header_extension *ext = (janus_rtp_header_extension*)(buf+hlen);
+		janus_rtp_header_extension *ext = (janus_rtp_header_extension *)(buf+hlen);
 		int extlen = ntohs(ext->length)*4;
 		hlen += 4;
 		if(len > (hlen + extlen)) {
@@ -134,6 +137,8 @@ static int janus_rtp_header_extension_find(char *buf, int len, int id, uint8_t *
 							*byte = buf[hlen+i+1];
 						if(word)
 							*word = *(uint32_t *)(buf+hlen+i);
+						if(ref)
+							*ref = &buf[hlen];
 						return 0;
 					}
 					i += 1 + idlen;
@@ -147,7 +152,7 @@ static int janus_rtp_header_extension_find(char *buf, int len, int id, uint8_t *
 
 int janus_rtp_header_extension_parse_audio_level(char *buf, int len, int id, int *level) {
 	uint8_t byte = 0;
-	if(janus_rtp_header_extension_find(buf, len, id, &byte, NULL) < 0)
+	if(janus_rtp_header_extension_find(buf, len, id, &byte, NULL, NULL) < 0)
 		return -1;
 	/* a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level */
 	int v = (byte & 0x80) >> 7;
@@ -161,7 +166,7 @@ int janus_rtp_header_extension_parse_audio_level(char *buf, int len, int id, int
 int janus_rtp_header_extension_parse_video_orientation(char *buf, int len, int id,
 		gboolean *c, gboolean *f, gboolean *r1, gboolean *r0) {
 	uint8_t byte = 0;
-	if(janus_rtp_header_extension_find(buf, len, id, &byte, NULL) < 0)
+	if(janus_rtp_header_extension_find(buf, len, id, &byte, NULL, NULL) < 0)
 		return -1;
 	/* a=extmap:4 urn:3gpp:video-orientation */
 	gboolean cbit = (byte & 0x08) >> 3;
@@ -183,7 +188,7 @@ int janus_rtp_header_extension_parse_video_orientation(char *buf, int len, int i
 int janus_rtp_header_extension_parse_playout_delay(char *buf, int len, int id,
 		uint16_t *min_delay, uint16_t *max_delay) {
 	uint32_t bytes = 0;
-	if(janus_rtp_header_extension_find(buf, len, id, NULL, &bytes) < 0)
+	if(janus_rtp_header_extension_find(buf, len, id, NULL, &bytes, NULL) < 0)
 		return -1;
 	/* a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay */
 	uint16_t min = (bytes & 0x00FFF000) >> 12;
@@ -196,6 +201,24 @@ int janus_rtp_header_extension_parse_playout_delay(char *buf, int len, int id,
 	return 0;
 }
 
+int janus_rtp_header_extension_parse_rtp_stream_id(char *buf, int len, int id,
+		char *sdes_item, int sdes_len) {
+	char *ext = NULL;
+	if(janus_rtp_header_extension_find(buf, len, id, NULL, NULL, &ext) < 0)
+		return -1;
+	/* a=extmap:3/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id */
+	if(ext == NULL)
+		return -2;
+	int val_len = (*ext & 0x0F) + 1;
+	if(val_len > (sdes_len-1)) {
+		JANUS_LOG(LOG_WARN, "SDES buffer is too small (%d < %d), RTP stream ID will be cut\n", val_len, sdes_len);
+		val_len = sdes_len-1;
+	}
+	memcpy(sdes_item, ext+1, val_len);
+	*(sdes_item+val_len) = '\0';
+	return 0;
+}
+
 
 /* RTP context related methods */
 void janus_rtp_switching_context_reset(janus_rtp_switching_context *context) {
diff --git a/rtp.h b/rtp.h
index 9076982..3bcf4d1 100644
--- a/rtp.h
+++ b/rtp.h
@@ -102,6 +102,8 @@ typedef struct janus_rtp_header_extension {
 #define JANUS_RTP_EXTMAP_CC_EXTENSIONS		"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
 /*! \brief a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay */
 #define JANUS_RTP_EXTMAP_PLAYOUT_DELAY		"http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"
+/*! \brief a=extmap:3/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id */
+#define JANUS_RTP_EXTMAP_RTP_STREAM_ID		"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
 
 /*! \brief Helper to quickly access the RTP payload, skipping header and extensions
  * @param[in] buf The packet data
@@ -153,6 +155,17 @@ int janus_rtp_header_extension_parse_video_orientation(char *buf, int len, int i
 int janus_rtp_header_extension_parse_playout_delay(char *buf, int len, int id,
 	uint16_t *min_delay, uint16_t *max_delay);
 
+/*! \brief Helper to parse a rtp-stream-id RTP extension (https://tools.ietf.org/html/draft-ietf-avtext-rid-09)
+ * @param[in] buf The packet data
+ * @param[in] len The packet data length in bytes
+ * @param[in] id The extension ID to look for
+ * @param[out] sdes_item Buffer where the RTP stream ID will be written
+ * @param[in] sdes_len Size of the input/output buffer
+ * @returns 0 if found, -1 otherwise */
+int janus_rtp_header_extension_parse_rtp_stream_id(char *buf, int len, int id,
+	char *sdes_item, int sdes_len);
+
+
 /*! \brief RTP context, in order to make sure SSRC changes result in coherent seq/ts increases */
 typedef struct janus_rtp_switching_context {
 	uint32_t a_last_ssrc, a_last_ts, a_base_ts, a_base_ts_prev,
diff --git a/sdp.c b/sdp.c
index ce0697c..8fa0d3f 100644
--- a/sdp.c
+++ b/sdp.c
@@ -295,15 +295,37 @@ int janus_sdp_process(void *ice_handle, janus_sdp *remote_sdp) {
 							JANUS_LOG(LOG_ERR, "[%"SCNu64"] Failed to parse candidate... (%d)\n", handle->handle_id, res);
 						}
 					}
-				}
-				if(!strcasecmp(a->name, "ssrc")) {
+				} else if(!strcasecmp(a->name, "rid")) {
+					/* This attribute is used by Firefox for simulcasting */
+					char rid[16];
+					if(sscanf(a->value, "%15s send", rid) != 1) {
+						JANUS_LOG(LOG_ERR, "[%"SCNu64"] Failed to parse rid attribute...\n", handle->handle_id);
+					} else {
+						JANUS_LOG(LOG_VERB, "[%"SCNu64"] Parsed rid: %s\n", handle->handle_id, rid);
+						if(stream->rid[0] == NULL) {
+							stream->rid[0] = g_strdup(rid);
+						} else if(stream->rid[1] == NULL) {
+							stream->rid[1] = g_strdup(rid);
+						} else if(stream->rid[2] == NULL) {
+							stream->rid[2] = g_strdup(rid);
+						} else {
+							JANUS_LOG(LOG_WARN, "[%"SCNu64"] Too many RTP Stream IDs, ignoring '%s'...\n", handle->handle_id, rid);
+						}
+					}
+				} else if(!strcasecmp(a->name, "ssrc-group")) {
+					/* FIXME This can be either FID or SIM */
+					int res = janus_sdp_parse_ssrc_group(stream, (const char *)a->value, m->type == JANUS_SDP_VIDEO);
+					if(res != 0) {
+						JANUS_LOG(LOG_ERR, "[%"SCNu64"] Failed to parse SSRC group attribute... (%d)\n", handle->handle_id, res);
+					}
+				} else if(!strcasecmp(a->name, "ssrc")) {
 					int res = janus_sdp_parse_ssrc(stream, (const char *)a->value, m->type == JANUS_SDP_VIDEO);
 					if(res != 0) {
 						JANUS_LOG(LOG_ERR, "[%"SCNu64"] Failed to parse SSRC attribute... (%d)\n", handle->handle_id, res);
 					}
 				}
 #ifdef HAVE_SCTP
-				if(!strcasecmp(a->name, "sctpmap")) {
+				else if(!strcasecmp(a->name, "sctpmap")) {
 					/* TODO Parse sctpmap line to get the UDP-port value and the number of channels */
 					JANUS_LOG(LOG_VERB, "Got a sctpmap attribute: %s\n", a->value);
 				}
@@ -534,6 +556,64 @@ int janus_sdp_parse_candidate(void *ice_stream, const char *candidate, int trick
 	return 0;
 }
 
+int janus_sdp_parse_ssrc_group(void *ice_stream, const char *group_attr, int video) {
+	if(ice_stream == NULL || group_attr == NULL)
+		return -1;
+	janus_ice_stream *stream = (janus_ice_stream *)ice_stream;
+	janus_ice_handle *handle = stream->handle;
+	if(handle == NULL)
+		return -2;
+	if(!video)
+		return -3;
+	gboolean fid = strstr(group_attr, "FID") != NULL;
+	gboolean sim = strstr(group_attr, "SIM") != NULL;
+	guint64 ssrc = 0;
+	gchar **list = g_strsplit(group_attr, " ", -1);
+	gchar *index = list[0];
+	if(index != NULL) {
+		int i=0;
+		while(index != NULL) {
+			if(i > 0 && strlen(index) > 0) {
+				ssrc = g_ascii_strtoull(index, NULL, 0);
+				switch(i) {
+					case 1:
+						stream->video_ssrc_peer = ssrc;
+						JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC: %"SCNu32"\n", handle->handle_id, stream->video_ssrc_peer);
+						break;
+					case 2:
+						if(fid) {
+							stream->video_ssrc_peer_rtx = ssrc;
+							JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC (rtx): %"SCNu32"\n", handle->handle_id, stream->video_ssrc_peer_rtx);
+						} else if(sim) {
+							stream->video_ssrc_peer_sim_1 = ssrc;
+							JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC (sim-1): %"SCNu32"\n", handle->handle_id, stream->video_ssrc_peer_sim_1);
+						} else {
+							JANUS_LOG(LOG_WARN, "[%"SCNu64"] Don't know what to do with SSRC: %"SCNu64"\n", handle->handle_id, ssrc);
+						}
+						break;
+					case 3:
+						if(fid) {
+							JANUS_LOG(LOG_WARN, "[%"SCNu64"] Found one too many retransmission SSRC (rtx): %"SCNu64"\n", handle->handle_id, ssrc);
+						} else if(sim) {
+							stream->video_ssrc_peer_sim_2 = ssrc;
+							JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC (sim-2): %"SCNu32"\n", handle->handle_id, stream->video_ssrc_peer_sim_2);
+						} else {
+							JANUS_LOG(LOG_WARN, "[%"SCNu64"] Don't know what to do with SSRC: %"SCNu64"\n", handle->handle_id, ssrc);
+						}
+						break;
+					default:
+						JANUS_LOG(LOG_WARN, "[%"SCNu64"] Don't know what to do with video SSRC: %"SCNu64"\n", handle->handle_id, ssrc);
+						break;
+				}
+			}
+			i++;
+			index = list[i];
+		}
+	}
+	g_clear_pointer(&list, g_strfreev);
+	return 0;
+}
+
 int janus_sdp_parse_ssrc(void *ice_stream, const char *ssrc_attr, int video) {
 	if(ice_stream == NULL || ssrc_attr == NULL)
 		return -1;
@@ -547,18 +627,25 @@ int janus_sdp_parse_ssrc(void *ice_stream, const char *ssrc_attr, int video) {
 	if(video) {
 		if(stream->video_ssrc_peer == 0) {
 			stream->video_ssrc_peer = ssrc;
-			JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC: %u\n", handle->handle_id, stream->video_ssrc_peer);
-		} else if(stream->video_ssrc_peer != ssrc) {
-			/* FIXME We assume the second SSRC we get is the one Chrome associates with retransmissions, e.g.
-			 * 	a=ssrc-group:FID 586466331 2053167359 (SSRC SSRC-rtx)
-			 * SSRC group FID: https://tools.ietf.org/html/rfc3388#section-7 */
-			stream->video_ssrc_peer_rtx = ssrc;
-			JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC (rtx): %u\n", handle->handle_id, stream->video_ssrc_peer_rtx);
+			JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC: %"SCNu32"\n", handle->handle_id, stream->video_ssrc_peer);
+		} else {
+			/* We already have a video SSRC: check if RID is involved, and we'll keep track of this for simulcasting */
+			if(stream->rid[0]) {
+				if(stream->video_ssrc_peer_sim_1 == 0) {
+					stream->video_ssrc_peer_sim_1 = ssrc;
+					JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC (sim-1): %"SCNu32"\n", handle->handle_id, stream->video_ssrc_peer_sim_1);
+				} else if(stream->video_ssrc_peer_sim_2 == 0) {
+					stream->video_ssrc_peer_sim_2 = ssrc;
+					JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer video SSRC (sim-2): %"SCNu32"\n", handle->handle_id, stream->video_ssrc_peer_sim_2);
+				} else {
+					JANUS_LOG(LOG_WARN, "[%"SCNu64"] Don't know what to do with video SSRC: %"SCNu64"\n", handle->handle_id, ssrc);
+				}
+			}
 		}
 	} else {
 		if(stream->audio_ssrc_peer == 0) {
 			stream->audio_ssrc_peer = ssrc;
-			JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer audio SSRC: %u\n", handle->handle_id, stream->audio_ssrc_peer);
+			JANUS_LOG(LOG_VERB, "[%"SCNu64"] Peer audio SSRC: %"SCNu32"\n", handle->handle_id, stream->audio_ssrc_peer);
 		}
 	}
 	return 0;
@@ -636,6 +723,7 @@ int janus_sdp_anonymize(janus_sdp *anon) {
 					|| !strcasecmp(a->name, "mid")
 					|| !strcasecmp(a->name, "msid")
 					|| !strcasecmp(a->name, "msid-semantic")
+					|| !strcasecmp(a->name, "rid")
 					|| !strcasecmp(a->name, "rtcp")
 					|| !strcasecmp(a->name, "rtcp-mux")
 					|| !strcasecmp(a->name, "rtcp-rsize")
@@ -916,6 +1004,26 @@ char *janus_sdp_merge(void *ice_handle, janus_sdp *anon) {
 			a = janus_sdp_attribute_create("ssrc", "%"SCNu32" label:janusv0", stream->video_ssrc);
 			m->attributes = g_list_append(m->attributes, a);
 		}
+		/* FIXME If the peer is Firefox and is negotiating simulcasting, add the rid attributes */
+		if(m->type == JANUS_SDP_VIDEO && stream->rid[0] != NULL) {
+			char rids[50];
+			rids[0] = '\0';
+			int i=0;
+			for(i=0; i<3; i++) {
+				if(stream->rid[i] == NULL)
+					continue;
+				a = janus_sdp_attribute_create("rid", "%s recv", stream->rid[i]);
+				m->attributes = g_list_append(m->attributes, a);
+				if(strlen(rids) == 0) {
+					g_strlcat(rids, stream->rid[i], sizeof(rids));
+				} else {
+					g_strlcat(rids, ";", sizeof(rids));
+					g_strlcat(rids, stream->rid[i], sizeof(rids));
+				}
+			}
+			a = janus_sdp_attribute_create("simulcast", " recv rid=%s", rids);
+			m->attributes = g_list_append(m->attributes, a);
+		}
 		/* And now the candidates */
 		janus_ice_candidates_to_sdp(handle, m, stream->stream_id, 1);
 		if(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RTCPMUX) &&
diff --git a/sdp.h b/sdp.h
index 9229bd6..96b1513 100644
--- a/sdp.h
+++ b/sdp.h
@@ -62,6 +62,14 @@ int janus_sdp_process(void *handle, janus_sdp *sdp);
  * @returns 0 in case of success, a non-zero integer in case of an error */
 int janus_sdp_parse_candidate(void *stream, const char *candidate, int trickle);
 
+/*! \brief Method to parse a SSRC group attribute
+ * \details This method will parse a SSRC group attribute, and set the parsed values for the peer
+ * @param[in] stream Opaque pointer to the ICE stream this candidate refers to
+ * @param[in] group_attr The SSRC group attribute value to parse
+ * @param[in] video Whether this is video-related or not
+ * @returns 0 in case of success, a non-zero integer in case of an error */
+int janus_sdp_parse_ssrc_group(void *stream, const char *group_attr, int video);
+
 /*! \brief Method to parse a SSRC attribute
  * \details This method will parse a SSRC attribute, and set it for the peer
  * @param[in] stream Opaque pointer to the ICE stream this candidate refers to
diff --git a/utils.c b/utils.c
index edaac94..668cec1 100644
--- a/utils.c
+++ b/utils.c
@@ -17,6 +17,8 @@
 #include <sys/file.h>
 #include <sys/types.h>
 #include <unistd.h>
+#include <arpa/inet.h>
+#include <inttypes.h>
 
 #include "utils.h"
 #include "debug.h"
@@ -494,3 +496,309 @@ gboolean janus_json_is_valid(json_t *val, json_type jtype, unsigned int flags) {
 	}
 	return is_valid;
 }
+
+/* The following code is more related to codec specific helpers */
+#if defined(__ppc__) || defined(__ppc64__)
+	# define swap2(d)  \
+	((d&0x000000ff)<<8) |  \
+	((d&0x0000ff00)>>8)
+#else
+	# define swap2(d) d
+#endif
+
+gboolean janus_vp8_is_keyframe(char* buffer, int len) {
+	if(!buffer || len < 0)
+		return FALSE;
+	/* Parse VP8 header now */
+	uint8_t vp8pd = *buffer;
+	uint8_t xbit = (vp8pd & 0x80);
+	uint8_t sbit = (vp8pd & 0x10);
+	if(xbit) {
+		JANUS_LOG(LOG_HUGE, "  -- X bit is set!\n");
+		/* Read the Extended control bits octet */
+		buffer++;
+		vp8pd = *buffer;
+		uint8_t ibit = (vp8pd & 0x80);
+		uint8_t lbit = (vp8pd & 0x40);
+		uint8_t tbit = (vp8pd & 0x20);
+		uint8_t kbit = (vp8pd & 0x10);
+		if(ibit) {
+			JANUS_LOG(LOG_HUGE, "  -- I bit is set!\n");
+			/* Read the PictureID octet */
+			buffer++;
+			vp8pd = *buffer;
+			uint16_t picid = vp8pd, wholepicid = picid;
+			uint8_t mbit = (vp8pd & 0x80);
+			if(mbit) {
+				JANUS_LOG(LOG_HUGE, "  -- M bit is set!\n");
+				memcpy(&picid, buffer, sizeof(uint16_t));
+				wholepicid = ntohs(picid);
+				picid = (wholepicid & 0x7FFF);
+				buffer++;
+			}
+			JANUS_LOG(LOG_HUGE, "  -- -- PictureID: %"SCNu16"\n", picid);
+		}
+		if(lbit) {
+			JANUS_LOG(LOG_HUGE, "  -- L bit is set!\n");
+			/* Read the TL0PICIDX octet */
+			buffer++;
+			vp8pd = *buffer;
+		}
+		if(tbit || kbit) {
+			JANUS_LOG(LOG_HUGE, "  -- T/K bit is set!\n");
+			/* Read the TID/KEYIDX octet */
+			buffer++;
+			vp8pd = *buffer;
+		}
+		buffer++;	/* Now we're in the payload */
+		if(sbit) {
+			JANUS_LOG(LOG_HUGE, "  -- S bit is set!\n");
+			unsigned long int vp8ph = 0;
+			memcpy(&vp8ph, buffer, 4);
+			vp8ph = ntohl(vp8ph);
+			uint8_t pbit = ((vp8ph & 0x01000000) >> 24);
+			if(!pbit) {
+				JANUS_LOG(LOG_HUGE, "  -- P bit is NOT set!\n");
+				/* It is a key frame! Get resolution for debugging */
+				unsigned char *c = (unsigned char *)buffer+3;
+				/* vet via sync code */
+				if(c[0]!=0x9d||c[1]!=0x01||c[2]!=0x2a) {
+					JANUS_LOG(LOG_WARN, "First 3-bytes after header not what they're supposed to be?\n");
+				} else {
+					int vp8w = swap2(*(unsigned short*)(c+3))&0x3fff;
+					int vp8ws = swap2(*(unsigned short*)(c+3))>>14;
+					int vp8h = swap2(*(unsigned short*)(c+5))&0x3fff;
+					int vp8hs = swap2(*(unsigned short*)(c+5))>>14;
+					JANUS_LOG(LOG_HUGE, "Got a VP8 key frame: %dx%d (scale=%dx%d)\n", vp8w, vp8h, vp8ws, vp8hs);
+					return TRUE;
+				}
+			}
+		}
+	}
+	/* If we got here it's not a key frame */
+	return FALSE;
+}
+
+gboolean janus_vp9_is_keyframe(char* buffer, int len) {
+	if(!buffer || len < 0)
+		return FALSE;
+	/* Parse VP9 header now */
+	uint8_t vp9pd = *buffer;
+	uint8_t ibit = (vp9pd & 0x80);
+	uint8_t pbit = (vp9pd & 0x40);
+	uint8_t lbit = (vp9pd & 0x20);
+	uint8_t fbit = (vp9pd & 0x10);
+	uint8_t vbit = (vp9pd & 0x02);
+	buffer++;
+	if(ibit) {
+		/* Read the PictureID octet */
+		vp9pd = *buffer;
+		uint16_t picid = vp9pd, wholepicid = picid;
+		uint8_t mbit = (vp9pd & 0x80);
+		if(!mbit) {
+			buffer++;
+		} else {
+			memcpy(&picid, buffer, sizeof(uint16_t));
+			wholepicid = ntohs(picid);
+			picid = (wholepicid & 0x7FFF);
+			buffer += 2;
+		}
+	}
+	if(lbit) {
+		buffer++;
+		if(!fbit) {
+			/* Non-flexible mode, skip TL0PICIDX */
+			buffer++;
+		}
+	}
+	if(fbit && pbit) {
+		/* Skip reference indices */
+		uint8_t nbit = 1;
+		while(nbit) {
+			vp9pd = *buffer;
+			nbit = (vp9pd & 0x01);
+			buffer++;
+		}
+	}
+	if(vbit) {
+		/* Parse and skip SS */
+		vp9pd = *buffer;
+		uint n_s = (vp9pd & 0xE0) >> 5;
+		n_s++;
+		uint8_t ybit = (vp9pd & 0x10);
+		if(ybit) {
+			/* Iterate on all spatial layers and get resolution */
+			buffer++;
+			uint i=0;
+			for(i=0; i<n_s; i++) {
+				/* Width */
+				uint16_t *w = (uint16_t *)buffer;
+				int vp9w = ntohs(*w);
+				buffer += 2;
+				/* Height */
+				uint16_t *h = (uint16_t *)buffer;
+				int vp9h = ntohs(*h);
+				buffer += 2;
+				JANUS_LOG(LOG_WARN, "Got a VP9 key frame: %dx%d\n", vp9w, vp9h);
+				return TRUE;
+			}
+		}
+	}
+	/* If we got here it's not a key frame */
+	return FALSE;
+}
+
+gboolean janus_h264_is_keyframe(char* buffer, int len) {
+	if(!buffer || len < 0)
+		return FALSE;
+	/* Parse H264 header now */
+	uint8_t fragment = *buffer & 0x1F;
+	uint8_t nal = *(buffer+1) & 0x1F;
+	uint8_t start_bit = *(buffer+1) & 0x80;
+	JANUS_LOG(LOG_HUGE, "Fragment=%d, NAL=%d, Start=%d\n", fragment, nal, start_bit);
+	if(fragment == 5 ||
+			((fragment == 28 || fragment == 29) && nal == 5 && start_bit == 128)) {
+		JANUS_LOG(LOG_HUGE, "Got an H264 key frame\n");
+		return TRUE;
+	}
+	/* If we got here it's not a key frame */
+	return FALSE;
+}
+
+static int janus_vp8_parse_descriptor(char *buffer, int len,
+		uint16_t *picid, uint8_t *tl0picidx, uint8_t *tid, uint8_t *y, uint8_t *keyidx) {
+	if(!buffer || len < 0)
+		return -1;
+	if(picid)
+		*picid = 0;
+	if(tl0picidx)
+		*tl0picidx = 0;
+	if(tid)
+		*tid = 0;
+	if(y)
+		*y = 0;
+	if(keyidx)
+		*keyidx = 0;
+	uint8_t vp8pd = *buffer;
+	uint8_t xbit = (vp8pd & 0x80);
+	/* Read the Extended control bits octet */
+	if(xbit) {
+		buffer++;
+		vp8pd = *buffer;
+		uint8_t ibit = (vp8pd & 0x80);
+		uint8_t lbit = (vp8pd & 0x40);
+		uint8_t tbit = (vp8pd & 0x20);
+		uint8_t kbit = (vp8pd & 0x10);
+		if(ibit) {
+			/* Read the PictureID octet */
+			buffer++;
+			vp8pd = *buffer;
+			uint16_t partpicid = vp8pd, wholepicid = partpicid;
+			uint8_t mbit = (vp8pd & 0x80);
+			if(mbit) {
+				memcpy(&partpicid, buffer, sizeof(uint16_t));
+				wholepicid = ntohs(partpicid);
+				partpicid = (wholepicid & 0x7FFF);
+				if(picid)
+					*picid = partpicid;
+				buffer++;
+			}
+		}
+		if(lbit) {
+			/* Read the TL0PICIDX octet */
+			buffer++;
+			vp8pd = *buffer;
+			if(tl0picidx)
+				*tl0picidx = vp8pd;
+		}
+		if(tbit || kbit) {
+			/* Read the TID/Y/KEYIDX octet */
+			buffer++;
+			vp8pd = *buffer;
+			if(tid)
+				*tid = (vp8pd & 0xC0) >> 6;
+			if(y)
+				*y = (vp8pd & 0x20) >> 5;
+			if(keyidx)
+				*keyidx = (vp8pd & 0x1F) >> 4;
+		}
+	}
+	return 0;
+}
+
+static int janus_vp8_replace_descriptor(char *buffer, int len, uint16_t picid, uint8_t tl0picidx) {
+	if(!buffer || len < 0)
+		return -1;
+	uint8_t vp8pd = *buffer;
+	uint8_t xbit = (vp8pd & 0x80);
+	/* Read the Extended control bits octet */
+	if(xbit) {
+		buffer++;
+		vp8pd = *buffer;
+		uint8_t ibit = (vp8pd & 0x80);
+		uint8_t lbit = (vp8pd & 0x40);
+		uint8_t tbit = (vp8pd & 0x20);
+		uint8_t kbit = (vp8pd & 0x10);
+		if(ibit) {
+			/* Overwrite the PictureID octet */
+			buffer++;
+			vp8pd = *buffer;
+			uint8_t mbit = (vp8pd & 0x80);
+			if(!mbit) {
+				*buffer = picid;
+			} else {
+				uint16_t wholepicid = htons(picid);
+				memcpy(buffer, &wholepicid, 2);
+				*buffer |= 0x80;
+				buffer++;
+			}
+		}
+		if(lbit) {
+			/* Overwrite the TL0PICIDX octet */
+			buffer++;
+			*buffer = tl0picidx;
+		}
+		if(tbit || kbit) {
+			/* TODO Overwrite the TID/Y/KEYIDX octet */
+			buffer++;
+		}
+	}
+	return 0;
+}
+
+void janus_vp8_simulcast_context_reset(janus_vp8_simulcast_context *context) {
+	if(context == NULL)
+		return;
+	/* Reset the context values */
+	context->last_picid = 0;
+	context->base_picid = 0;
+	context->base_picid_prev = 0;
+	context->last_tlzi = 0;
+	context->base_tlzi = 0;
+	context->base_tlzi_prev = 0;
+}
+
+void janus_vp8_simulcast_descriptor_update(char *buffer, int len, janus_vp8_simulcast_context *context, gboolean switched) {
+	if(!buffer || len < 0)
+		return;
+	uint16_t picid = 0;
+	uint8_t tlzi = 0;
+	uint8_t tid = 0;
+	uint8_t ybit = 0;
+	uint8_t keyidx = 0;
+	/* Parse the identifiers in the VP8 payload descriptor */
+	if(janus_vp8_parse_descriptor(buffer, len, &picid, &tlzi, &tid, &ybit, &keyidx) < 0)
+		return;
+	//~ JANUS_LOG(LOG_WARN, "%"SCNu16", %"SCNu8", %"SCNu8", %"SCNu8", %"SCNu8"\n",
+		//~ picid, tlzi, tid, ybit, keyidx);
+	if(switched) {
+		context->base_picid_prev = context->last_picid;
+		context->base_picid = picid;
+		context->base_tlzi_prev = context->last_tlzi;
+		context->base_tlzi = tlzi;
+	}
+	context->last_picid = (picid-context->base_picid)+context->base_picid_prev+1;
+	context->last_tlzi = (tlzi-context->base_tlzi)+context->base_tlzi_prev+1;
+	/* Overwrite the values in the VP8 payload descriptors with the ones we have */
+	janus_vp8_replace_descriptor(buffer, len, context->last_picid, context->last_tlzi);
+}
diff --git a/utils.h b/utils.h
index 487092e..a04170c 100644
--- a/utils.h
+++ b/utils.h
@@ -220,4 +220,39 @@ gboolean janus_json_is_valid(json_t *val, json_type jtype, unsigned int flags);
 		} \
 	} while(0)
 
+/*! \brief Helper method to check if a VP8 frame is a keyframe or not
+ * @param[in] buffer The RTP payload to process
+ * @param[in] len The length of the RTP payload
+ * @returns TRUE if it's a keyframe, FALSE otherwise */
+gboolean janus_vp8_is_keyframe(char* buffer, int len);
+
+/*! \brief Helper method to check if a VP9 frame is a keyframe or not
+ * @param[in] buffer The RTP payload to process
+ * @param[in] len The length of the RTP payload
+ * @returns TRUE if it's a keyframe, FALSE otherwise */
+gboolean janus_vp9_is_keyframe(char* buffer, int len);
+
+/*! \brief Helper method to check if an H.264 frame is a keyframe or not
+ * @param[in] buffer The RTP payload to process
+ * @param[in] len The length of the RTP payload
+ * @returns TRUE if it's a keyframe, FALSE otherwise */
+gboolean janus_h264_is_keyframe(char* buffer, int len);
+
+/*! \brief VP8 simulcasting context, in order to make sure SSRC changes result in coherent picid/temporal level increases */
+typedef struct janus_vp8_simulcast_context {
+	uint16_t last_picid, base_picid, base_picid_prev;
+	uint16_t last_tlzi, base_tlzi, base_tlzi_prev;
+} janus_vp8_simulcast_context;
+
+/*! \brief Set (or reset) the context fields to their default values
+ * @param[in] context The context to (re)set */
+void janus_vp8_simulcast_context_reset(janus_vp8_simulcast_context *context);
+
+/*! \brief Use the context info to update the RTP header of a packet, if needed
+ * @param[in] buffer The RTP payload to process
+ * @param[in] len The length of the RTP payload
+ * @param[in] context The context to use as a reference
+ * @param[in] switched Whether there has been a source switch or not (important to compute offsets) */
+void janus_vp8_simulcast_descriptor_update(char *buffer, int len, janus_vp8_simulcast_context *context, gboolean switched);
+
 #endif

-- 
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