Revision 242e4f31

View differences:

conf/janus.plugin.videoroom.cfg.sample
11 11
; fir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)
12 12
; audiocodec = opus|isac32|isac16|pcmu|pcma (audio codec to force on publishers, default=opus)
13 13
; videocodec = vp8|vp9|h264 (video codec to force on publishers, default=vp8)
14
; video_svc = yes|no (whether SVC support must be enabled; works only for VP9, default=no)
14 15
; audiolevel_ext = yes|no (whether the ssrc-audio-level RTP extension must
15 16
;		be negotiated/used or not for new publishers, default=yes)
16 17
; audiolevel_event = yes|no (whether to emit event to other users or not, default=no)
......
39 40
;videocodec = vp8
40 41
record = false
41 42
;rec_dir = /path/to/recordings-folder
43

  
44

  
45
; This other demo room here is only there in case you want to play with
46
; the VP9 SVC support. Notice that you'll need a Chrome launched with
47
; the flag that enables that support, or otherwise you'll be using just
48
; plain VP9 (which is good if you want to test how this indeed affect
49
; what receivers will get, whether they're encoding SVC or not).
50
[5678]
51
description = VP9-SVC Demo Room
52
secret = adminpwd
53
publishers = 6
54
bitrate = 1024000
55
fir_freq = 10
56
videocodec = vp9
57
video_svc = true
html/demos.html
32 32
			<div class="page-header">
33 33
				<h1>Janus WebRTC Gateway: Demo Tests</h1>
34 34
			</div>
35
			<table class="table">
35
			<table class="table table-striped">
36 36
				<tr>
37 37
					<td colspan=2><h3>Plugin demos</h3></td>
38 38
				</tr>
39
				<tr class="active">
39
				<tr>
40 40
					<td><a href="echotest.html">Echo Test</a></td>
41 41
					<td>A simple Echo Test demo, with knobs to control the bitrate.</td>
42 42
				</tr>
......
44 44
					<td><a href="streamingtest.html">Streaming</a></td>
45 45
					<td>A media Streaming demo, with sample live and on-demand streams.</td>
46 46
				</tr>
47
				<tr class="active">
47
				<tr>
48 48
					<td><a href="videocalltest.html">Video Call</a></td>
49 49
					<td>A Video Call demo, a bit like AppRTC but with media passing through the gateway.</td>
50 50
				</tr>
......
52 52
					<td><a href="siptest.html">SIP Gateway</a></td>
53 53
					<td>A SIP Gateway demo, allowing you to register at a SIP server and start/receive calls.</td>
54 54
				</tr>
55
				<tr class="active">
55
				<tr>
56 56
					<td><a href="videoroomtest.html">Video Room</a></td>
57 57
					<td>A videoconferencing demo, allowing you to join a video room with up to six users.</td>
58 58
				</tr>
......
60 60
					<td><a href="audiobridgetest.html">Audio Room</a></td>
61 61
					<td>An audio mixing/bridge demo, allowing you join an Audio Room room.</td>
62 62
				</tr>
63
				<tr class="active">
63
				<tr>
64 64
					<td><a href="textroomtest.html">Text Room</a></td>
65 65
					<td>A text room demo, using DataChannels only.</td>
66 66
				</tr>
......
68 68
					<td><a href="voicemailtest.html">Voice Mail</a></td>
69 69
					<td>A simple audio recorder demo, returning an .opus file after 10 seconds.</td>
70 70
				</tr>
71
				<tr class="active">
71
				<tr>
72 72
					<td><a href="recordplaytest.html">Recorder/Playout</a></td>
73 73
					<td>A demo to record audio/video messages, and subsequently replay them through WebRTC.</td>
74 74
				</tr>
......
77 77
					<td>A webinar-like screen sharing session, based on the Video Room plugin.</td>
78 78
				</tr>
79 79
			</table>
80
			<table class="table">
80
			<table class="table table-striped">
81 81
				<tr>
82 82
					<td colspan=2><h3>Other demos</h3></td>
83 83
				</tr>
84
				<tr class="active">
84
				<tr>
85 85
					<td><a href="devicetest.html">Device Selection</a></td>
86 86
					<td>A variant of the Echo Test demo, that allows you to choose a specific capture device.</td>
87 87
				</tr>
88 88
				<tr>
89
					<td><a href="vp9svctest.html">VP9-SVC Video Room</a></td>
90
					<td>A variant of the Video Room demo, that allows you to test the VP9 SVC layer selection, if available.</td>
91
				</tr>
92
				<tr>
89 93
					<td><a href="admin.html">Admin/Monitor</a></td>
90 94
					<td>A simple page showcasing how you can use the Janus Admin/Monitor API.</td>
91 95
				</tr>
html/navbar.html
26 26
					<li><a href="screensharingtest.html">Screen Sharing</a></li>
27 27
					<li class="divider"></li>
28 28
					<li><a href="devicetest.html">Device Selection</a></li>
29
					<li><a href="vp9svctest.html">VP9-SVC Video Room</a></li>
29 30
					<li class="divider"></li>
30 31
					<li><a href="admin.html">Admin/Monitor</a></li>
31 32
				</ul>
html/vp9svctest.html
1
<!DOCTYPE html>
2
<html xmlns="http://www.w3.org/1999/xhtml">
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7
<title>Janus WebRTC Gateway: VP9-SVC Video Room Demo</title>
8
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/3.4.3/adapter.min.js" ></script>
9
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.2/jquery.min.js" ></script>
10
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js" ></script>
11
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/js/bootstrap.min.js"></script>
12
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.1.0/bootbox.min.js"></script>
13
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js"></script>
14
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js"></script>
15
<script type="text/javascript" src="janus.js" ></script>
16
<script type="text/javascript" src="vp9svctest.js"></script>
17
<script>
18
	$(function() {
19
		$(".navbar-static-top").load("navbar.html", function() {
20
			$(".navbar-static-top li.dropdown").addClass("active");
21
			$(".navbar-static-top a[href='vp9svctest.html']").parent().addClass("active");
22
		});
23
		$(".footer").load("footer.html");
24
	});
25
</script>
26
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/cerulean/bootstrap.min.css" type="text/css"/>
27
<link rel="stylesheet" href="css/demo.css" type="text/css"/>
28
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.2/css/font-awesome.min.css"/>
29
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.css"/>
30
</head>
31
<body>
32

  
33
<a href="https://github.com/meetecho/janus-gateway"><img style="position: absolute; top: 0; left: 0; border: 0; z-index: 1001;" src="https://s3.amazonaws.com/github/ribbons/forkme_left_darkblue_121621.png" alt="Fork me on GitHub"></a>
34

  
35
<nav class="navbar navbar-default navbar-static-top">
36
</nav>
37

  
38
<div class="container">
39
	<div class="row">
40
		<div class="col-md-12">
41
			<div class="page-header">
42
				<h1>Plugin Demo: VP9-SVC Video Room
43
					<button class="btn btn-default" autocomplete="off" id="start">Start</button>
44
				</h1>
45
			</div>
46
			<div class="container" id="details">
47
				<div class="row">
48
					<div class="col-md-12">
49
						<h3>Demo details</h3>
50
						<p>This is basically a clone of the plain <a href="videoroomtest.hmtl">Video Room</a>
51
						demo, but with a key difference: it forces VP9 on all publishers, and supports
52
						the VP9 SVC layer selection (if you don't know what this means, check this
53
						<a target="_blank" href="https://webrtchacks.com/chrome-vp9-svc/">excellent blog post</a>).
54
						As such, it will allow viewers to select which layer, spatial or temporal, to receive
55
						from the publishers, in order to receive more or less data according to what they
56
						can/want to get.</p>
57
						<p>Notice that this only works if the publishers joining the room will use a recent
58
						Chrome version that has been started with the following flag:</p>
59
						<p><div class="alert alert-info"><code>--force-fieldtrials=WebRTC-SupportVP9SVC/EnabledByFlag_2SL3TL/</code></div></p>
60
						<p>If you join with a Chrome that doesn't have that option set, or with
61
						another browser, you'll be able to select a layer, but all request to set
62
						a custom layer on your video will fail, meaning you'll act as a plain VP9 client.
63
						If you ask for a layer from a publisher that doesn't support VP9 SVC either,
64
						then your request will be ignored as well.</p>
65
						<p>In case some publishers do support the VP9 SVC features, selecting a different
66
						layer from them should result in variations of the video quality, and a notable
67
						difference in the bandwidth used to receive their video as well.</p>
68
						<p>Press the <code>Start</code> button above to launch the demo.</p>
69
					</div>
70
				</div>
71
			</div>
72
			<div class="container hide" id="videojoin">
73
				<div class="row">
74
					<span class="label label-info" id="you"></span>
75
					<div class="col-md-12" id="controls">
76
						<div class="input-group margin-bottom-md hide" id="registernow">
77
							<span class="input-group-addon">@</span>
78
							<input autocomplete="off" class="form-control" autocomplete="off" type="text" placeholder="Choose a display name" id="username" onkeypress="return checkEnter(this, event);"></input>
79
							<span class="input-group-btn">
80
								<button class="btn btn-success" autocomplete="off" id="register">Join the room</button>
81
							</span>
82
						</div>
83
					</div>
84
				</div>
85
			</div>
86
			<div class="container hide" id="videos">
87
				<div class="row">
88
					<div class="col-md-4">
89
						<div class="panel panel-default">
90
							<div class="panel-heading">
91
								<h3 class="panel-title">Local Video <span class="label label-primary hide" id="publisher"></span></h3>
92
							</div>
93
							<div class="panel-body" id="videolocal"></div>
94
						</div>
95
					</div>
96
					<div class="col-md-4">
97
						<div class="panel panel-default">
98
							<div class="panel-heading">
99
								<h3 class="panel-title">Remote Video #1
100
									<span class="label label-info hide" id="remote1"></span>
101
									<div id="layers1" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
102
										<div class"row">
103
											<div class="btn-group btn-group-xs" style="width: 100%">
104
												<button id="sl1-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
105
												<button id="sl1-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
106
											</div>
107
										</div>
108
										<div class"row">
109
											<div class="btn-group btn-group-xs" style="width: 100%">
110
												<button id="tl1-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
111
												<button id="tl1-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
112
												<button id="tl1-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
113
											</div>
114
										</div>
115
									</div>
116
								</h3>
117
							</div>
118
							<div class="panel-body relative" id="videoremote1"></div>
119
						</div>
120
					</div>
121
					<div class="col-md-4">
122
						<div class="panel panel-default">
123
							<div class="panel-heading">
124
								<h3 class="panel-title">Remote Video #2
125
									<span class="label label-info hide" id="remote2"></span>
126
									<div id="layers2" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
127
										<div class"row">
128
											<div class="btn-group btn-group-xs" style="width: 100%">
129
												<button id="sl2-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
130
												<button id="sl2-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
131
											</div>
132
										</div>
133
										<div class"row">
134
											<div class="btn-group btn-group-xs" style="width: 100%">
135
												<button id="tl2-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
136
												<button id="tl2-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
137
												<button id="tl2-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
138
											</div>
139
										</div>
140
									</div>
141
								</h3>
142
							</div>
143
							<div class="panel-body relative" id="videoremote2"></div>
144
						</div>
145
					</div>
146
				</div>
147
				<div class="row">
148
					<div class="col-md-4">
149
						<div class="panel panel-default">
150
							<div class="panel-heading">
151
								<h3 class="panel-title">Remote Video #3
152
									<span class="label label-info hide" id="remote3"></span>
153
									<div id="layers3" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
154
										<div class"row">
155
											<div class="btn-group btn-group-xs" style="width: 100%">
156
												<button id="sl3-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
157
												<button id="sl3-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
158
											</div>
159
										</div>
160
										<div class"row">
161
											<div class="btn-group btn-group-xs" style="width: 100%">
162
												<button id="tl3-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
163
												<button id="tl3-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
164
												<button id="tl3-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
165
											</div>
166
										</div>
167
									</div>
168
								</h3>
169
							</div>
170
							<div class="panel-body relative" id="videoremote3"></div>
171
						</div>
172
					</div>
173
					<div class="col-md-4">
174
						<div class="panel panel-default">
175
							<div class="panel-heading">
176
								<h3 class="panel-title">Remote Video #4
177
									<span class="label label-info hide" id="remote4"></span>
178
									<div id="layers4" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
179
										<div class"row">
180
											<div class="btn-group btn-group-xs" style="width: 100%">
181
												<button id="sl4-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
182
												<button id="sl4-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
183
											</div>
184
										</div>
185
										<div class"row">
186
											<div class="btn-group btn-group-xs" style="width: 100%">
187
												<button id="tl4-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
188
												<button id="tl4-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
189
												<button id="tl4-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
190
											</div>
191
										</div>
192
									</div>
193
								</h3>
194
							</div>
195
							<div class="panel-body relative" id="videoremote4"></div>
196
						</div>
197
					</div>
198
					<div class="col-md-4">
199
						<div class="panel panel-default">
200
							<div class="panel-heading">
201
								<h3 class="panel-title">Remote Video #5
202
									<span class="label label-info hide" id="remote5"></span>
203
									<div id="layers5" class="btn-group-vertical btn-group-vertical-xs pull-right hide">
204
										<div class"row">
205
											<div class="btn-group btn-group-xs" style="width: 100%">
206
												<button id="sl5-1" type="button" class="btn btn-primary" style="width: 50%">SL 1</button>
207
												<button id="sl5-0" type="button" class="btn btn-primary" style="width: 50%">SL 0</button>
208
											</div>
209
										</div>
210
										<div class"row">
211
											<div class="btn-group btn-group-xs" style="width: 100%">
212
												<button id="tl5-2" type="button" class="btn btn-primary" style="width: 34%">TL 2</button>
213
												<button id="tl5-1" type="button" class="btn btn-primary" style="width: 33%">TL 1</button>
214
												<button id="tl5-0" type="button" class="btn btn-primary" style="width: 33%">TL 0</button>
215
											</div>
216
										</div>
217
									</div>
218
								</h3>
219
							</div>
220
							<div class="panel-body relative" id="videoremote5"></div>
221
						</div>
222
					</div>
223
				</div>
224
			</div>
225
		</div>
226
	</div>
227

  
228
	<hr>
229
	<div class="footer">
230
	</div>
231
</div>
232

  
233
</body>
234
</html>
html/vp9svctest.js
1
// We make use of this 'server' variable to provide the address of the
2
// REST Janus API. By default, in this example we assume that Janus is
3
// co-located with the web server hosting the HTML pages but listening
4
// on a different port (8088, the default for HTTP in Janus), which is
5
// why we make use of the 'window.location.hostname' base address. Since
6
// Janus can also do HTTPS, and considering we don't really want to make
7
// use of HTTP for Janus if your demos are served on HTTPS, we also rely
8
// on the 'window.location.protocol' prefix to build the variable, in
9
// particular to also change the port used to contact Janus (8088 for
10
// HTTP and 8089 for HTTPS, if enabled).
11
// In case you place Janus behind an Apache frontend (as we did on the
12
// online demos at http://janus.conf.meetecho.com) you can just use a
13
// relative path for the variable, e.g.:
14
//
15
// 		var server = "/janus";
16
//
17
// which will take care of this on its own.
18
//
19
//
20
// If you want to use the WebSockets frontend to Janus, instead, you'll
21
// have to pass a different kind of address, e.g.:
22
//
23
// 		var server = "ws://" + window.location.hostname + ":8188";
24
//
25
// Of course this assumes that support for WebSockets has been built in
26
// when compiling the gateway. WebSockets support has not been tested
27
// as much as the REST API, so handle with care!
28
//
29
//
30
// If you have multiple options available, and want to let the library
31
// autodetect the best way to contact your gateway (or pool of gateways),
32
// you can also pass an array of servers, e.g., to provide alternative
33
// means of access (e.g., try WebSockets first and, if that fails, fall
34
// back to plain HTTP) or just have failover servers:
35
//
36
//		var server = [
37
//			"ws://" + window.location.hostname + ":8188",
38
//			"/janus"
39
//		];
40
//
41
// This will tell the library to try connecting to each of the servers
42
// in the presented order. The first working server will be used for
43
// the whole session.
44
//
45
var server = null;
46
if(window.location.protocol === 'http:')
47
	server = "http://" + window.location.hostname + ":8088/janus";
48
else
49
	server = "https://" + window.location.hostname + ":8089/janus";
50

  
51
var janus = null;
52
var sfutest = null;
53
var opaqueId = "videoroomtest-"+Janus.randomString(12);
54

  
55
var started = false;
56

  
57
var myroom = 5678;
58
var myusername = null;
59
var myid = null;
60
var mystream = null;
61
// We use this other ID just to map our subscriptions to us
62
var mypvtid = null;
63

  
64
var feeds = [];
65
var bitrateTimer = [];
66

  
67

  
68
$(document).ready(function() {
69
	// Initialize the library (all console debuggers enabled)
70
	Janus.init({debug: "all", callback: function() {
71
		// Use a button to start the demo
72
		$('#start').click(function() {
73
			if(started)
74
				return;
75
			started = true;
76
			$(this).attr('disabled', true).unbind('click');
77
			// Make sure the browser supports WebRTC
78
			if(!Janus.isWebrtcSupported()) {
79
				bootbox.alert("No WebRTC support... ");
80
				return;
81
			}
82
			// Create session
83
			janus = new Janus(
84
				{
85
					server: server,
86
					success: function() {
87
						// Attach to video room test plugin
88
						janus.attach(
89
							{
90
								plugin: "janus.plugin.videoroom",
91
								opaqueId: opaqueId,
92
								success: function(pluginHandle) {
93
									$('#details').remove();
94
									sfutest = pluginHandle;
95
									Janus.log("Plugin attached! (" + sfutest.getPlugin() + ", id=" + sfutest.getId() + ")");
96
									Janus.log("  -- This is a publisher/manager");
97
									// Prepare the username registration
98
									$('#videojoin').removeClass('hide').show();
99
									$('#registernow').removeClass('hide').show();
100
									$('#register').click(registerUsername);
101
									$('#username').focus();
102
									$('#start').removeAttr('disabled').html("Stop")
103
										.click(function() {
104
											$(this).attr('disabled', true);
105
											janus.destroy();
106
										});
107
								},
108
								error: function(error) {
109
									Janus.error("  -- Error attaching plugin...", error);
110
									bootbox.alert("Error attaching plugin... " + error);
111
								},
112
								consentDialog: function(on) {
113
									Janus.debug("Consent dialog should be " + (on ? "on" : "off") + " now");
114
									if(on) {
115
										// Darken screen and show hint
116
										$.blockUI({ 
117
											message: '<div><img src="up_arrow.png"/></div>',
118
											css: {
119
												border: 'none',
120
												padding: '15px',
121
												backgroundColor: 'transparent',
122
												color: '#aaa',
123
												top: '10px',
124
												left: (navigator.mozGetUserMedia ? '-100px' : '300px')
125
											} });
126
									} else {
127
										// Restore screen
128
										$.unblockUI();
129
									}
130
								},
131
								mediaState: function(medium, on) {
132
									Janus.log("Janus " + (on ? "started" : "stopped") + " receiving our " + medium);
133
								},
134
								webrtcState: function(on) {
135
									Janus.log("Janus says our WebRTC PeerConnection is " + (on ? "up" : "down") + " now");
136
									$("#videolocal").parent().parent().unblock();
137
								},
138
								onmessage: function(msg, jsep) {
139
									Janus.debug(" ::: Got a message (publisher) :::");
140
									Janus.debug(JSON.stringify(msg));
141
									var event = msg["videoroom"];
142
									Janus.debug("Event: " + event);
143
									if(event != undefined && event != null) {
144
										if(event === "joined") {
145
											// Publisher/manager created, negotiate WebRTC and attach to existing feeds, if any
146
											myid = msg["id"];
147
											mypvtid = msg["private_id"];
148
											Janus.log("Successfully joined room " + msg["room"] + " with ID " + myid);
149
											publishOwnFeed(true);
150
											// Any new feed to attach to?
151
											if(msg["publishers"] !== undefined && msg["publishers"] !== null) {
152
												var list = msg["publishers"];
153
												Janus.debug("Got a list of available publishers/feeds:");
154
												Janus.debug(list);
155
												for(var f in list) {
156
													var id = list[f]["id"];
157
													var display = list[f]["display"];
158
													Janus.debug("  >> [" + id + "] " + display);
159
													newRemoteFeed(id, display)
160
												}
161
											}
162
										} else if(event === "destroyed") {
163
											// The room has been destroyed
164
											Janus.warn("The room has been destroyed!");
165
											bootbox.alert("The room has been destroyed", function() {
166
												window.location.reload();
167
											});
168
										} else if(event === "event") {
169
											// Any new feed to attach to?
170
											if(msg["publishers"] !== undefined && msg["publishers"] !== null) {
171
												var list = msg["publishers"];
172
												Janus.debug("Got a list of available publishers/feeds:");
173
												Janus.debug(list);
174
												for(var f in list) {
175
													var id = list[f]["id"];
176
													var display = list[f]["display"];
177
													Janus.debug("  >> [" + id + "] " + display);
178
													newRemoteFeed(id, display)
179
												}
180
											} else if(msg["leaving"] !== undefined && msg["leaving"] !== null) {
181
												// One of the publishers has gone away?
182
												var leaving = msg["leaving"];
183
												Janus.log("Publisher left: " + leaving);
184
												var remoteFeed = null;
185
												for(var i=1; i<6; i++) {
186
													if(feeds[i] != null && feeds[i] != undefined && feeds[i].rfid == leaving) {
187
														remoteFeed = feeds[i];
188
														break;
189
													}
190
												}
191
												if(remoteFeed != null) {
192
													Janus.debug("Feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") has left the room, detaching");
193
													$('#remote'+remoteFeed.rfindex).empty().hide();
194
													$('#videoremote'+remoteFeed.rfindex).empty();
195
													feeds[remoteFeed.rfindex] = null;
196
													remoteFeed.detach();
197
												}
198
											} else if(msg["unpublished"] !== undefined && msg["unpublished"] !== null) {
199
												// One of the publishers has unpublished?
200
												var unpublished = msg["unpublished"];
201
												Janus.log("Publisher left: " + unpublished);
202
												if(unpublished === 'ok') {
203
													// That's us
204
													sfutest.hangup();
205
													return;
206
												}
207
												var remoteFeed = null;
208
												for(var i=1; i<6; i++) {
209
													if(feeds[i] != null && feeds[i] != undefined && feeds[i].rfid == unpublished) {
210
														remoteFeed = feeds[i];
211
														break;
212
													}
213
												}
214
												if(remoteFeed != null) {
215
													Janus.debug("Feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") has left the room, detaching");
216
													$('#remote'+remoteFeed.rfindex).empty().hide();
217
													$('#videoremote'+remoteFeed.rfindex).empty();
218
													feeds[remoteFeed.rfindex] = null;
219
													remoteFeed.detach();
220
												}
221
											} else if(msg["error"] !== undefined && msg["error"] !== null) {
222
												if(msg["error_code"] === 426) {
223
													// This is a "no such room" error: give a more meaningful description
224
													bootbox.alert(
225
														"<p>Apparently room <code>" + myroom + "</code> (the one this demo uses for testing VP9 SVC) " +
226
														"does not exist...</p><p>Do you have an updated <code>janus.plugin.videoroom.cfg</code> " +
227
														"configuration file? If not, make sure you copy the details of room <code>" + myroom + "</code> " +
228
														"from that sample in your current configuration file, then restart Janus and try again."
229
													);
230
												} else {
231
													bootbox.alert(msg["error"]);
232
												}
233
											}
234
										}
235
									}
236
									if(jsep !== undefined && jsep !== null) {
237
										Janus.debug("Handling SDP as well...");
238
										Janus.debug(jsep);
239
										sfutest.handleRemoteJsep({jsep: jsep});
240
									}
241
								},
242
								onlocalstream: function(stream) {
243
									Janus.debug(" ::: Got a local stream :::");
244
									mystream = stream;
245
									Janus.debug(JSON.stringify(stream));
246
									$('#videolocal').empty();
247
									$('#videojoin').hide();
248
									$('#videos').removeClass('hide').show();
249
									if($('#myvideo').length === 0) {
250
										$('#videolocal').append('<video class="rounded centered" id="myvideo" width="100%" height="100%" autoplay muted="muted"/>');
251
										// Add a 'mute' button
252
										$('#videolocal').append('<button class="btn btn-warning btn-xs" id="mute" style="position: absolute; bottom: 0px; left: 0px; margin: 15px;">Mute</button>');
253
										$('#mute').click(toggleMute);
254
										// Add an 'unpublish' button
255
										$('#videolocal').append('<button class="btn btn-warning btn-xs" id="unpublish" style="position: absolute; bottom: 0px; right: 0px; margin: 15px;">Unpublish</button>');
256
										$('#unpublish').click(unpublishOwnFeed);
257
									}
258
									$('#publisher').removeClass('hide').html(myusername).show();
259
									Janus.attachMediaStream($('#myvideo').get(0), stream);
260
									$("#myvideo").get(0).muted = "muted";
261
									$("#videolocal").parent().parent().block({
262
										message: '<b>Publishing...</b>',
263
										css: {
264
											border: 'none',
265
											backgroundColor: 'transparent',
266
											color: 'white'
267
										}
268
									});
269
									var videoTracks = stream.getVideoTracks();
270
									if(videoTracks === null || videoTracks === undefined || videoTracks.length === 0) {
271
										// No webcam
272
										$('#myvideo').hide();
273
										$('#videolocal').append(
274
											'<div class="no-video-container">' +
275
												'<i class="fa fa-video-camera fa-5 no-video-icon" style="height: 100%;"></i>' +
276
												'<span class="no-video-text" style="font-size: 16px;">No webcam available</span>' +
277
											'</div>');
278
									}
279
								},
280
								onremotestream: function(stream) {
281
									// The publisher stream is sendonly, we don't expect anything here
282
								},
283
								oncleanup: function() {
284
									Janus.log(" ::: Got a cleanup notification: we are unpublished now :::");
285
									mystream = null;
286
									$('#videolocal').html('<button id="publish" class="btn btn-primary">Publish</button>');
287
									$('#publish').click(function() { publishOwnFeed(true); });
288
									$("#videolocal").parent().parent().unblock();
289
								}
290
							});
291
					},
292
					error: function(error) {
293
						Janus.error(error);
294
						bootbox.alert(error, function() {
295
							window.location.reload();
296
						});
297
					},
298
					destroyed: function() {
299
						window.location.reload();
300
					}
301
				});
302
		});
303
	}});
304
});
305

  
306
function checkEnter(field, event) {
307
	var theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
308
	if(theCode == 13) {
309
		registerUsername();
310
		return false;
311
	} else {
312
		return true;
313
	}
314
}
315

  
316
function registerUsername() {
317
	if($('#username').length === 0) {
318
		// Create fields to register
319
		$('#register').click(registerUsername);
320
		$('#username').focus();
321
	} else {
322
		// Try a registration
323
		$('#username').attr('disabled', true);
324
		$('#register').attr('disabled', true).unbind('click');
325
		var username = $('#username').val();
326
		if(username === "") {
327
			$('#you')
328
				.removeClass().addClass('label label-warning')
329
				.html("Insert your display name (e.g., pippo)");
330
			$('#username').removeAttr('disabled');
331
			$('#register').removeAttr('disabled').click(registerUsername);
332
			return;
333
		}
334
		if(/[^a-zA-Z0-9]/.test(username)) {
335
			$('#you')
336
				.removeClass().addClass('label label-warning')
337
				.html('Input is not alphanumeric');
338
			$('#username').removeAttr('disabled').val("");
339
			$('#register').removeAttr('disabled').click(registerUsername);
340
			return;
341
		}
342
		var register = { "request": "join", "room": myroom, "ptype": "publisher", "display": username };
343
		myusername = username;
344
		sfutest.send({"message": register});
345
	}
346
}
347

  
348
function publishOwnFeed(useAudio) {
349
	// Publish our stream
350
	$('#publish').attr('disabled', true).unbind('click');
351
	sfutest.createOffer(
352
		{
353
			// Add data:true here if you want to publish datachannels as well
354
			media: { audioRecv: false, videoRecv: false, audioSend: useAudio, videoSend: true },	// Publishers are sendonly
355
			success: function(jsep) {
356
				Janus.debug("Got publisher SDP!");
357
				Janus.debug(jsep);
358
				var publish = { "request": "configure", "audio": useAudio, "video": true };
359
				sfutest.send({"message": publish, "jsep": jsep});
360
			},
361
			error: function(error) {
362
				Janus.error("WebRTC error:", error);
363
				if (useAudio) {
364
					 publishOwnFeed(false);
365
				} else {
366
					bootbox.alert("WebRTC error... " + JSON.stringify(error));
367
					$('#publish').removeAttr('disabled').click(function() { publishOwnFeed(true); });
368
				}
369
			}
370
		});
371
}
372

  
373
function toggleMute() {
374
	var muted = sfutest.isAudioMuted();
375
	Janus.log((muted ? "Unmuting" : "Muting") + " local stream...");
376
	if(muted)
377
		sfutest.unmuteAudio();
378
	else
379
		sfutest.muteAudio();
380
	muted = sfutest.isAudioMuted();
381
	$('#mute').html(muted ? "Unmute" : "Mute");
382
}
383

  
384
function unpublishOwnFeed() {
385
	// Unpublish our stream
386
	$('#unpublish').attr('disabled', true).unbind('click');
387
	var unpublish = { "request": "unpublish" };
388
	sfutest.send({"message": unpublish});
389
}
390

  
391
function newRemoteFeed(id, display) {
392
	// A new feed has been published, create a new plugin handle and attach to it as a listener
393
	var remoteFeed = null;
394
	janus.attach(
395
		{
396
			plugin: "janus.plugin.videoroom",
397
			opaqueId: opaqueId,
398
			success: function(pluginHandle) {
399
				remoteFeed = pluginHandle;
400
				Janus.log("Plugin attached! (" + remoteFeed.getPlugin() + ", id=" + remoteFeed.getId() + ")");
401
				Janus.log("  -- This is a subscriber");
402
				// We wait for the plugin to send us an offer
403
				var listen = { "request": "join", "room": myroom, "ptype": "listener", "feed": id, "private_id": mypvtid };
404
				remoteFeed.send({"message": listen});
405
			},
406
			error: function(error) {
407
				Janus.error("  -- Error attaching plugin...", error);
408
				bootbox.alert("Error attaching plugin... " + error);
409
			},
410
			onmessage: function(msg, jsep) {
411
				Janus.debug(" ::: Got a message (listener) :::");
412
				Janus.debug(JSON.stringify(msg));
413
				var event = msg["videoroom"];
414
				Janus.debug("Event: " + event);
415
				if(event != undefined && event != null) {
416
					if(event === "attached") {
417
						// Subscriber created and attached
418
						for(var i=1;i<6;i++) {
419
							if(feeds[i] === undefined || feeds[i] === null) {
420
								feeds[i] = remoteFeed;
421
								remoteFeed.rfindex = i;
422
								break;
423
							}
424
						}
425
						remoteFeed.rfid = msg["id"];
426
						remoteFeed.rfdisplay = msg["display"];
427
						if(remoteFeed.spinner === undefined || remoteFeed.spinner === null) {
428
							var target = document.getElementById('videoremote'+remoteFeed.rfindex);
429
							remoteFeed.spinner = new Spinner({top:100}).spin(target);
430
						} else {
431
							remoteFeed.spinner.spin();
432
						}
433
						Janus.log("Successfully attached to feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") in room " + msg["room"]);
434
						$('#remote'+remoteFeed.rfindex).removeClass('hide').html(remoteFeed.rfdisplay).show();
435
					} else if(msg["error"] !== undefined && msg["error"] !== null) {
436
						bootbox.alert(msg["error"]);
437
					} else {
438
						// What has just happened?
439
					}
440
				}
441
				if(jsep !== undefined && jsep !== null) {
442
					Janus.debug("Handling SDP as well...");
443
					Janus.debug(jsep);
444
					// Answer and attach
445
					remoteFeed.createAnswer(
446
						{
447
							jsep: jsep,
448
							// Add data:true here if you want to subscribe to datachannels as well
449
							// (obviously only works if the publisher offered them in the first place)
450
							media: { audioSend: false, videoSend: false },	// We want recvonly audio/video
451
							success: function(jsep) {
452
								Janus.debug("Got SDP!");
453
								Janus.debug(jsep);
454
								var body = { "request": "start", "room": myroom };
455
								remoteFeed.send({"message": body, "jsep": jsep});
456
							},
457
							error: function(error) {
458
								Janus.error("WebRTC error:", error);
459
								bootbox.alert("WebRTC error... " + JSON.stringify(error));
460
							}
461
						});
462
				}
463
			},
464
			webrtcState: function(on) {
465
				Janus.log("Janus says this WebRTC PeerConnection (feed #" + remoteFeed.rfindex + ") is " + (on ? "up" : "down") + " now");
466
			},
467
			onlocalstream: function(stream) {
468
				// The subscriber stream is recvonly, we don't expect anything here
469
			},
470
			onremotestream: function(stream) {
471
				Janus.debug("Remote feed #" + remoteFeed.rfindex);
472
				if($('#remotevideo'+remoteFeed.rfindex).length === 0) {
473
					// No remote video yet
474
					$('#videoremote'+remoteFeed.rfindex).append('<video class="rounded centered" id="waitingvideo' + remoteFeed.rfindex + '" width=320 height=240 />');
475
					$('#videoremote'+remoteFeed.rfindex).append('<video class="rounded centered relative hide" id="remotevideo' + remoteFeed.rfindex + '" width="100%" height="100%" autoplay/>');
476
				}
477
				$('#videoremote'+remoteFeed.rfindex).append(
478
					'<span class="label label-primary hide" id="curres'+remoteFeed.rfindex+'" style="position: absolute; bottom: 0px; left: 0px; margin: 15px;"></span>' +
479
					'<span class="label label-info hide" id="curbitrate'+remoteFeed.rfindex+'" style="position: absolute; bottom: 0px; right: 0px; margin: 15px;"></span>');
480
				// Show the video, hide the spinner and show the resolution when we get a playing event
481
				$("#remotevideo"+remoteFeed.rfindex).bind("playing", function () {
482
					if(remoteFeed.spinner !== undefined && remoteFeed.spinner !== null)
483
						remoteFeed.spinner.stop();
484
					remoteFeed.spinner = null;
485
					$('#waitingvideo'+remoteFeed.rfindex).remove();
486
					$('#remotevideo'+remoteFeed.rfindex).removeClass('hide');
487
					var width = this.videoWidth;
488
					var height = this.videoHeight;
489
					$('#curres'+remoteFeed.rfindex).removeClass('hide').text(width+'x'+height).show();
490
					// Enable the layer selection buttons
491
					$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-success')
492
						.unbind('click').click(function() {
493
							toastr.success("Selected spatial layer 1 of " + remoteFeed.rfdisplay + "'s video (normal resolution)", null, {timeOut: 2000});
494
							$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-success');
495
							$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary');
496
							var body = { request: "configure", spatial_layer: 1};
497
							remoteFeed.send({message: body});
498
						});
499
					$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary')
500
						.unbind('click').click(function() {
501
							toastr.success("Selected spatial layer 0 of " + remoteFeed.rfdisplay + "'s video (smaller resolution)", null, {timeOut: 2000});
502
							$('#sl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary');
503
							$('#sl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-success');
504
							var body = { request: "configure", spatial_layer: 0};
505
							remoteFeed.send({message: body});
506
						});
507
					$('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-success')
508
						.unbind('click').click(function() {
509
							toastr.success("Selected temporal layer 2 of " + remoteFeed.rfdisplay + "'s video (normal FPS)", null, {timeOut: 2000});
510
							$('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-success');
511
							$('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary');
512
							$('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary');
513
							var body = { request: "configure", temporal_layer: 2};
514
							remoteFeed.send({message: body});
515
						});
516
					$('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary')
517
						.unbind('click').click(function() {
518
							toastr.success("Selected temporal layer 1 of " + remoteFeed.rfdisplay + "'s video (low FPS)", null, {timeOut: 2000});
519
							$('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-primary');
520
							$('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-success');
521
							$('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary');
522
							var body = { request: "configure", temporal_layer: 1};
523
							remoteFeed.send({message: body});
524
						});
525
					$('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-primary')
526
						.unbind('click').click(function() {
527
							toastr.success("Selected temporal layer 0 of " + remoteFeed.rfdisplay + "'s video (lowest FPS)", {timeOut: 2000});
528
							$('#tl'+remoteFeed.rfindex+'-2').removeClass('btn-primary btn-success').addClass('btn-primary');
529
							$('#tl'+remoteFeed.rfindex+'-1').removeClass('btn-primary btn-success').addClass('btn-primary');
530
							$('#tl'+remoteFeed.rfindex+'-0').removeClass('btn-primary btn-success').addClass('btn-success');
531
							var body = { request: "configure", temporal_layer: 0};
532
							remoteFeed.send({message: body});
533
						});
534
					$('#layers'+remoteFeed.rfindex).removeClass('hide');
535
				});
536
				Janus.attachMediaStream($('#remotevideo'+remoteFeed.rfindex).get(0), stream);
537
				var videoTracks = stream.getVideoTracks();
538
				if(videoTracks === null || videoTracks === undefined || videoTracks.length === 0 || videoTracks[0].muted) {
539
					// No remote video
540
					$('#remotevideo'+remoteFeed.rfindex).hide();
541
					$('#videoremote'+remoteFeed.rfindex).append(
542
						'<div class="no-video-container">' +
543
							'<i class="fa fa-video-camera fa-5 no-video-icon" style="height: 100%;"></i>' +
544
							'<span class="no-video-text" style="font-size: 16px;">No remote video available</span>' +
545
						'</div>');
546
				}
547
				if(adapter.browserDetails.browser === "chrome" || adapter.browserDetails.browser === "firefox") {
548
					$('#curbitrate'+remoteFeed.rfindex).removeClass('hide').show();
549
					bitrateTimer[remoteFeed.rfindex] = setInterval(function() {
550
						// Display updated bitrate, if supported
551
						var bitrate = remoteFeed.getBitrate();
552
						$('#curbitrate'+remoteFeed.rfindex).text(bitrate);
553
						var width = $("#remotevideo"+remoteFeed.rfindex).get(0).videoWidth;
554
						var height = $("#remotevideo"+remoteFeed.rfindex).get(0).videoHeight;
555
						if(width > 0 && height > 0)
556
							$('#curres'+remoteFeed.rfindex).removeClass('hide').text(width+'x'+height).show();
557
					}, 1000);
558
				}
559
			},
560
			oncleanup: function() {
561
				Janus.log(" ::: Got a cleanup notification (remote feed " + id + ") :::");
562
				if(remoteFeed.spinner !== undefined && remoteFeed.spinner !== null)
563
					remoteFeed.spinner.stop();
564
				remoteFeed.spinner = null;
565
				$('#waitingvideo'+remoteFeed.rfindex).remove();
566
				$('#curbitrate'+remoteFeed.rfindex).remove();
567
				$('#curres'+remoteFeed.rfindex).remove();
568
				if(bitrateTimer[remoteFeed.rfindex] !== null && bitrateTimer[remoteFeed.rfindex] !== null) 
569
					clearInterval(bitrateTimer[remoteFeed.rfindex]);
570
				bitrateTimer[remoteFeed.rfindex] = null;
571
				$('#sl'+remoteFeed.rfindex+'-1').unbind('click');
572
				$('#sl'+remoteFeed.rfindex+'-0').unbind('click');
573
				$('#tl'+remoteFeed.rfindex+'-2').unbind('click');
574
				$('#tl'+remoteFeed.rfindex+'-1').unbind('click');
575
				$('#tl'+remoteFeed.rfindex+'-0').unbind('click');
576
				$('#layers'+remoteFeed.rfindex).addClass('hide');
577
			}
578
		});
579
}
plugins/janus_videoroom.c
66 66
fir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)
67 67
audiocodec = opus|isac32|isac16|pcmu|pcma|g722 (audio codec to force on publishers, default=opus)
68 68
videocodec = vp8|vp9|h264 (video codec to force on publishers, default=vp8)
69
video_svc = yes|no (whether SVC support must be enabled; works only for VP9, default=no)
69 70
audiolevel_ext = yes|no (whether the ssrc-audio-level RTP extension must be
70 71
	negotiated/used or not for new publishers, default=yes)
71 72
audiolevel_event = yes|no (whether to emit event to other users or not)
......
148 149

  
149 150

  
150 151
/* Plugin information */
151
#define JANUS_VIDEOROOM_VERSION			8
152
#define JANUS_VIDEOROOM_VERSION_STRING	"0.0.8"
152
#define JANUS_VIDEOROOM_VERSION			9
153
#define JANUS_VIDEOROOM_VERSION_STRING	"0.0.9"
153 154
#define JANUS_VIDEOROOM_DESCRIPTION		"This is a plugin implementing a videoconferencing SFU (Selective Forwarding Unit) for Janus, that is an audio/video router."
154 155
#define JANUS_VIDEOROOM_NAME			"JANUS VideoRoom plugin"
155 156
#define JANUS_VIDEOROOM_AUTHOR			"Meetecho s.r.l."
......
229 230
	{"publishers", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
230 231
	{"audiocodec", JSON_STRING, 0},
231 232
	{"videocodec", JSON_STRING, 0},
233
	{"video_svc", JANUS_JSON_BOOL, 0},
232 234
	{"audiolevel_ext", JANUS_JSON_BOOL, 0},
233 235
	{"audiolevel_event", JANUS_JSON_BOOL, 0},
234 236
	{"audio_active_packets", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
......
300 302
static struct janus_json_parameter configure_parameters[] = {
301 303
	{"audio", JANUS_JSON_BOOL, 0},
302 304
	{"video", JANUS_JSON_BOOL, 0},
303
	{"data", JANUS_JSON_BOOL, 0}
305
	{"data", JANUS_JSON_BOOL, 0},
306
	{"spatial_layer", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
307
	{"temporal_layer", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}
304 308
};
305 309
static struct janus_json_parameter listener_parameters[] = {
306 310
	{"feed", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
......
447 451
			return VP8_PT;
448 452
	}
449 453
}
454
/* Helper method to parse an RTP video frame and get some SVC-related info
455
 * (note: this only works with VP9, right now, on an experimental basis) */
456
static int janus_videoroom_videocodec_parse_svc(janus_videoroom_videocodec vcodec,
457
	char *buf, int total, int *found,
458
	int *spatial_layer, int *temporal_layer,
459
	uint8_t *p, uint8_t *d, uint8_t *u, uint8_t *b, uint8_t *e);
460

  
450 461

  
451 462
typedef struct janus_videoroom {
452 463
	guint64 room_id;			/* Unique room ID */
......
460 471
	uint16_t fir_freq;			/* Regular FIR frequency (0=disabled) */
461 472
	janus_videoroom_audiocodec acodec;	/* Audio codec to force on publishers*/
462 473
	janus_videoroom_videocodec vcodec;	/* Video codec to force on publishers*/
474
	gboolean do_svc;			/* Whether SVC must be done for video (note: only available for VP9 right now) */
463 475
	gboolean audiolevel_ext;	/* Whether the ssrc-audio-level extension must be negotiated or not for new publishers */
464 476
	gboolean audiolevel_event;	/* Whether to emit event to other users about audiolevel */
465 477
	int audio_active_packets;	/* amount of packets with audio level for checkup */
......
559 571
	gboolean audio, video, data;		/* Whether audio, video and/or data must be sent to this publisher */
560 572
	gboolean paused;
561 573
	gboolean kicked;	/* Whether this subscription belongs to a participant that has been kicked */
574
	/* The following are only relevant if we're doing VP9 SVC*/
575
	int spatial_layer, target_spatial_layer;
576
	int temporal_layer, target_temporal_layer;
562 577
} janus_videoroom_listener;
563 578
static void janus_videoroom_listener_free(janus_videoroom_listener *l);
564 579

  
......
568 583
	gboolean is_video;
569 584
	uint32_t timestamp;
570 585
	uint16_t seq_number;
586
	/* The following are only relevant if we're doing VP9 SVC*/
587
	gboolean svc;
588
	int spatial_layer;
589
	int temporal_layer;
590
	uint8_t pbit, dbit, ubit, bbit, ebit;
571 591
} janus_videoroom_rtp_relay_packet;
572 592

  
573 593

  
......
785 805
			janus_config_item *firfreq = janus_config_get_item(cat, "fir_freq");
786 806
			janus_config_item *audiocodec = janus_config_get_item(cat, "audiocodec");
787 807
			janus_config_item *videocodec = janus_config_get_item(cat, "videocodec");
808
			janus_config_item *svc = janus_config_get_item(cat, "video_svc");
788 809
			janus_config_item *audiolevel_ext = janus_config_get_item(cat, "audiolevel_ext");
789 810
			janus_config_item *audiolevel_event = janus_config_get_item(cat, "audiolevel_event");
790 811
			janus_config_item *audio_active_packets = janus_config_get_item(cat, "audio_active_packets");
......
855 876
					videoroom->vcodec = JANUS_VIDEOROOM_VP8;
856 877
				}
857 878
			}
879
			if(svc && svc->value && janus_is_true(svc->value)) {
880
				if(videoroom->vcodec == JANUS_VIDEOROOM_VP9) {
881
					videoroom->do_svc = TRUE;
882
				} else {
883
					JANUS_LOG(LOG_WARN, "SVC is only supported, in an experimental way, for VP9, not %s: disabling it...\n",
884
						janus_videoroom_videocodec_name(videoroom->vcodec));
885
				}
886
			}
858 887
			videoroom->audiolevel_ext = TRUE;
859 888
			if(audiolevel_ext != NULL && audiolevel_ext->value != NULL)
860 889
				videoroom->audiolevel_ext = janus_is_true(audiolevel_ext->value);
......
1185 1214
				json_object_set_new(media, "video", json_integer(participant->video));
1186 1215
				json_object_set_new(media, "data", json_integer(participant->data));
1187 1216
				json_object_set_new(info, "media", media);
1217
				if(participant->room && participant->room->do_svc) {
1218
					json_t *svc = json_object();
1219
					json_object_set_new(svc, "spatial-layer", json_integer(participant->spatial_layer));
1220
					json_object_set_new(svc, "target-spatial-layer", json_integer(participant->target_spatial_layer));
1221
					json_object_set_new(svc, "temporal-layer", json_integer(participant->temporal_layer));
1222
					json_object_set_new(svc, "target-temporal-layer", json_integer(participant->target_temporal_layer));
1223
					json_object_set_new(info, "svc", svc);
1224
				}
1188 1225
			}
1189 1226
		}
1190 1227
	}
......
1329 1366
				goto plugin_response;
1330 1367
			}
1331 1368
		}
1369
		json_t *svc = json_object_get(root, "video_svc");
1332 1370
		json_t *audiolevel_ext = json_object_get(root, "audiolevel_ext");
1333 1371
		json_t *audiolevel_event = json_object_get(root, "audiolevel_event");
1334 1372
		json_t *audio_active_packets = json_object_get(root, "audio_active_packets");
......
1460 1498
				videoroom->vcodec = JANUS_VIDEOROOM_VP8;
1461 1499
			}
1462 1500
		}
1501
		if(svc && json_is_true(svc)) {
1502
			if(videoroom->vcodec == JANUS_VIDEOROOM_VP9) {
1503
				videoroom->do_svc = TRUE;
1504
			} else {
1505
				JANUS_LOG(LOG_WARN, "SVC is only supported, in an experimental way, for VP9, not %s: disabling it...\n",
1506
					janus_videoroom_videocodec_name(videoroom->vcodec));
1507
			}
1508
		}
1463 1509
		videoroom->audiolevel_ext = audiolevel_ext ? json_is_true(audiolevel_ext) : TRUE;
1464 1510
		videoroom->audiolevel_event = audiolevel_event ? json_is_true(audiolevel_event) : FALSE;
1465 1511
		if(videoroom->audiolevel_event) {
......
1537 1583
			}
1538 1584
			janus_config_add_item(config, cat, "audiocodec", janus_videoroom_audiocodec_name(videoroom->acodec));
1539 1585
			janus_config_add_item(config, cat, "videocodec", janus_videoroom_videocodec_name(videoroom->vcodec));
1586
			if(videoroom->do_svc)
1587
				janus_config_add_item(config, cat, "video_svc", "yes");
1540 1588
			if(videoroom->room_secret)
1541 1589
				janus_config_add_item(config, cat, "secret", videoroom->room_secret);
1542 1590
			if(videoroom->room_pin)
......
1674 1722
				json_object_set_new(rl, "fir_freq", json_integer(room->fir_freq));
1675 1723
				json_object_set_new(rl, "audiocodec", json_string(janus_videoroom_audiocodec_name(room->acodec)));
1676 1724
				json_object_set_new(rl, "videocodec", json_string(janus_videoroom_videocodec_name(room->vcodec)));
1725
				if(room->do_svc)
1726
					json_object_set_new(rl, "video_svc", json_true());
1677 1727
				json_object_set_new(rl, "record", room->record ? json_true() : json_false());
1678 1728
				json_object_set_new(rl, "rec_dir", json_string(room->rec_dir));
1679 1729
				/* TODO: Should we list participants as well? or should there be a separate API call on a specific room for this? */
......
2397 2447
		packet.data = rtp;
2398 2448
		packet.length = len;
2399 2449
		packet.is_video = video;
2450
		packet.svc = FALSE;
2451
		if(video && videoroom->do_svc) {
2452
			/* We're doing SVC: let's parse this packet to see which layers are there */
2453
			uint8_t pbit = 0, dbit = 0, ubit = 0, bbit = 0, ebit = 0;
2454
			int found = 0, spatial_layer = 0, temporal_layer = 0;
2455
			if(janus_videoroom_videocodec_parse_svc(videoroom->vcodec, buf, len, &found, &spatial_layer, &temporal_layer, &pbit, &dbit, &ubit, &bbit, &ebit) == 0) {
2456
				if(found) {
2457
					packet.svc = TRUE;
2458
					packet.spatial_layer = spatial_layer;
2459
					packet.temporal_layer = temporal_layer;
2460
					packet.pbit = pbit;
2461
					packet.dbit = dbit;
2462
					packet.ubit = ubit;
2463
					packet.bbit = bbit;
2464
					packet.ebit = ebit;
2465
					JANUS_LOG(LOG_WARN, "sl=%d, tl=%d, p=%u, d=%u, u=%u, b=%u, e=%u\n",
2466
						packet.spatial_layer, packet.temporal_layer,
2467
						packet.pbit, packet.dbit, packet.ubit, packet.bbit, packet.ebit);
2468
				}
2469
			}
2470
		}
2400 2471
		/* Backup the actual timestamp and sequence number set by the publisher, in case switching is involved */
2401 2472
		packet.timestamp = ntohl(packet.data->timestamp);
2402 2473
		packet.seq_number = ntohs(packet.data->seq_number);
......
3088 3159
						listener->data = FALSE;	/* ... unless the publisher isn't sending any data */
3089 3160
					listener->paused = TRUE;	/* We need an explicit start from the listener */
3090 3161
					session->participant = listener;
3162
					if(videoroom->do_svc) {
3163
						/* This listener belongs to a room where VP9 SVC has been enabled,
3164
						 * let's assume we're interested in all layers for the time being */
3165
						listener->spatial_layer = -1;
3166
						listener->target_spatial_layer = 1;		/* FIXME Chrome sends 0 and 1 */
3167
						listener->temporal_layer = -1;
3168
						listener->target_temporal_layer = 2;	/* FIXME Chrome sends 0, 1 and 2 */
3169
					}
3091 3170
					janus_mutex_lock(&publisher->listeners_mutex);
3092 3171
					publisher->listeners = g_slist_append(publisher->listeners, listener);
3093 3172
					janus_mutex_unlock(&publisher->listeners_mutex);
......
3388 3467
				json_t *audio = json_object_get(root, "audio");
3389 3468
				json_t *video = json_object_get(root, "video");
3390 3469
				json_t *data = json_object_get(root, "data");
3470
				json_t *spatial = json_object_get(root, "spatial_layer");
3471
				json_t *temporal = json_object_get(root, "temporal_layer");
3391 3472
				/* Update the audio/video/data flags, if set */
3392 3473
				janus_videoroom_participant *publisher = listener->feed;
3393 3474
				if(publisher) {
......
3398 3479
					if(data && publisher->data)
3399 3480
						listener->data = json_is_true(data);
3400 3481
				}
3482
				if(listener->room->do_svc) {
3483
					/* Also check if the viewer is trying to configure a layer change */
3484
					if(spatial) {
3485
						int spatial_layer = json_integer_value(spatial);
3486
						if(spatial_layer > 1) {
3487
							JANUS_LOG(LOG_WARN, "Spatial layer higher than 1, will probably be ignored\n");
3488
						}
3489
						if(spatial_layer != listener->target_spatial_layer) {
3490
							/* Send a FIR to the new RTP forward publisher */
3491
							char buf[20];
3492
							janus_rtcp_fir((char *)&buf, 20, &publisher->fir_seq);
3493
							JANUS_LOG(LOG_VERB, "Need to downscale spatially, sending FIR to %"SCNu64" (%s)\n", publisher->user_id, publisher->display ? publisher->display : "??");
3494
							gateway->relay_rtcp(publisher->session->handle, 1, buf, 20);
3495
							/* Send a PLI too, just in case... */
3496
							janus_rtcp_pli((char *)&buf, 12);
3497
							JANUS_LOG(LOG_VERB, "Need to downscale spatially, sending PLI to %"SCNu64" (%s)\n", publisher->user_id, publisher->display ? publisher->display : "??");
3498
							gateway->relay_rtcp(publisher->session->handle, 1, buf, 12);
3499
						}
3500
						listener->target_spatial_layer = spatial_layer;
3501
					}
3502
					if(temporal) {
3503
						int temporal_layer = json_integer_value(temporal);
3504
						if(temporal_layer > 2) {
3505
							JANUS_LOG(LOG_WARN, "Temporal layer higher than 2, will probably be ignored\n");
3506
						}
3507
						listener->target_temporal_layer = temporal_layer;
3508
					}
3509
				}
3401 3510
				event = json_object();
3402 3511
				json_object_set_new(event, "videoroom", json_string("event"));
3403 3512
				json_object_set_new(event, "room", json_integer(listener->room->room_id));
......
3462 3571
				listener->data = data ? json_is_true(data) : TRUE;	/* True by default */
3463 3572
				if(!publisher->data)
3464 3573
					listener->data = FALSE;	/* ... unless the publisher isn't sending any data */
3574
				if(listener->room && listener->room->do_svc) {
3575
					/* This listener belongs to a room where VP9 SVC has been enabled,
3576
					 * let's assume we're interested in all layers for the time being */
3577
					listener->spatial_layer = -1;
3578
					listener->target_spatial_layer = 1;		/* FIXME Chrome sends 0 and 1 */
3579
					listener->temporal_layer = -1;
3580
					listener->target_temporal_layer = 2;	/* FIXME Chrome sends 0, 1 and 2 */
3581
				}
3465 3582
				janus_mutex_lock(&publisher->listeners_mutex);
3466 3583
				publisher->listeners = g_slist_append(publisher->listeners, listener);
3467 3584
				janus_mutex_unlock(&publisher->listeners_mutex);
......
3828 3945
	return NULL;
3829 3946
}
3830 3947

  
3948
/* Helper method to parse an RTP video frame and get some SVC-related info */
3949
static int janus_videoroom_videocodec_parse_svc(janus_videoroom_videocodec vcodec,
3950
		char *buf, int total, int *found,
3951
		int *spatial_layer, int *temporal_layer,
3952
		uint8_t *p, uint8_t *d, uint8_t *u, uint8_t *b, uint8_t *e) {
3953
	if(found)
3954
		*found = 0;
3955
	if(!buf || total < 12)
3956
		return -1;
3957
	/* This only works with VP9, right now, on an experimental basis) */
3958
	if(vcodec != JANUS_VIDEOROOM_VP9)
3959
		return -2;
3960
	/* Skip RTP header and extensions */
3961
	int len = 0;
3962
	char *buffer = janus_rtp_payload(buf, total, &len);
3963
	/* VP9 depay: */
3964
		/* https://tools.ietf.org/html/draft-ietf-payload-vp9-03 */
3965
	/* Read the first octet (VP9 Payload Descriptor) */
3966
	uint8_t vp9pd = *buffer;
3967
	uint8_t ibit = (vp9pd & 0x80) >> 7;
3968
	uint8_t pbit = (vp9pd & 0x40) >> 6;
3969
	uint8_t lbit = (vp9pd & 0x20) >> 5;
3970
	uint8_t fbit = (vp9pd & 0x10) >> 4;
3971
	uint8_t bbit = (vp9pd & 0x08) >> 3;
3972
	uint8_t ebit = (vp9pd & 0x04) >> 2;
3973
	uint8_t vbit = (vp9pd & 0x02) >> 1;
3974
	if(!lbit) {
3975
		/* No Layer indices present, no need to go on */
3976
		if(found)
3977
			*found = 0;
3978
		return 0;
3979
	}
3980
	/* Move to the next octet and see what's there */
3981
	buffer++;
3982
	len--;
3983
	if(ibit) {
3984
		/* Read the PictureID octet */
3985
		vp9pd = *buffer;
3986
		uint16_t picid = vp9pd, wholepicid = picid;
3987
		uint8_t mbit = (vp9pd & 0x80);
3988
		if(!mbit) {
3989
			buffer++;
3990
			len--;
3991
		} else {
3992
			memcpy(&picid, buffer, sizeof(uint16_t));
3993
			wholepicid = ntohs(picid);
3994
			picid = (wholepicid & 0x7FFF);
3995
			buffer += 2;
3996
			len -= 2;
3997
		}
3998
	}
3999
	if(lbit) {
4000
		/* Read the octet and parse the layer indices now */
4001
		vp9pd = *buffer;
4002
		int tlid = (vp9pd & 0xE0) >> 5;
4003
		uint8_t ubit = (vp9pd & 0x10) >> 4;
4004
		int slid = (vp9pd & 0x0E) >> 1;
4005
		uint8_t dbit = (vp9pd & 0x01);
4006
		JANUS_LOG(LOG_HUGE, "Parsed Layer indices: Temporal: %d (%u), Spatial: %d (%u)\n",
4007
			tlid, ubit, slid, dbit);
4008
		if(temporal_layer)
4009
			*temporal_layer = tlid;
4010
		if(spatial_layer)
4011
			*spatial_layer = slid;
4012
		if(p)
4013
			*p = pbit;
4014
		if(d)
4015
			*d = dbit;
4016
		if(u)
4017
			*u = ubit;
4018
		if(b)
4019
			*b = bbit;
4020
		if(e)
4021
			*e = ebit;
4022
		if(found)
4023
			*found = 1;
4024
		/* Go on, just to get to the SS, if available (which we currently ignore anyway) */
4025
		buffer++;
4026
		len--;
4027
		if(!fbit) {
4028
			/* Non-flexible mode, skip TL0PICIDX */
4029
			buffer++;
4030
			len--;
4031
		}
4032
	}
4033
	if(fbit && pbit) {
4034
		/* Skip reference indices */
4035
		uint8_t nbit = 1;
4036
		while(nbit) {
4037
			vp9pd = *buffer;
4038
			nbit = (vp9pd & 0x01);
4039
			buffer++;
4040
			len--;
4041
		}
4042
	}
4043
	if(vbit) {
4044
		/* Parse and skip SS */
4045
		vp9pd = *buffer;
4046
		int n_s = (vp9pd & 0xE0) >> 5;
4047
		n_s++;
4048
		JANUS_LOG(LOG_HUGE, "There are %d spatial layers\n", n_s);
4049
		uint8_t ybit = (vp9pd & 0x10);
4050
		uint8_t gbit = (vp9pd & 0x08);
4051
		if(ybit) {
4052
			/* Iterate on all spatial layers and get resolution */
4053
			buffer++;
4054
			len--;
4055
			int i=0;
4056
			for(i=0; i<n_s; i++) {
4057
				/* Been there, done that: skip skip skip */
4058
				buffer += 4;
4059
				len -= 4;
4060
			}
4061
		}
4062
		if(gbit) {
4063
			if(!ybit) {
4064
				buffer++;
4065
				len--;
4066
			}
4067
			uint8_t n_g = *buffer;
4068
			JANUS_LOG(LOG_HUGE, "There are %u frames in a GOF\n", n_g);
4069
			buffer++;
4070
			len--;
4071
			if(n_g > 0) {
4072
				int i=0;
4073
				for(i=0; i<n_g; i++) {
4074
					/* Read the R bits */
4075
					vp9pd = *buffer;
4076
					int r = (vp9pd & 0x0C) >> 2;
4077
					if(r > 0) {
4078
						/* Skip reference indices */
4079
						buffer += r;
4080
						len -= r;
4081
					}
4082
					buffer++;
4083
					len--;
4084
				}
4085
			}
4086
		}
4087
	}
4088
	return 0;
4089
}
3831 4090

  
3832 4091
/* Helper to quickly relay RTP packets from publishers to subscribers */
3833 4092
static void janus_videoroom_relay_rtp_packet(gpointer data, gpointer user_data) {
......
3862 4121
			/* Nope, don't relay */
3863 4122
			return;
3864 4123
		}
4124
		/* Check if there's any SVC info to take into account */
4125
		gboolean override_mark_bit = FALSE, has_marker_bit = packet->data->markerbit;
4126
		if(packet->svc) {
4127
			/* There is: check if this is a layer that can be dropped for this viewer
4128
			 * Note: Following core inspired by the excellent job done by Sergio Garcia Murillo here:
4129
			 * https://github.com/medooze/media-server/blob/master/src/vp9/VP9LayerSelector.cpp */
4130
			int temporal_layer = listener->temporal_layer;
4131
			if(listener->target_temporal_layer > listener->temporal_layer) {
4132
				/* We need to upscale */
4133
				JANUS_LOG(LOG_WARN, "We need to upscale temporally:\n");
4134
				if(packet->ubit && packet->bbit && packet->temporal_layer <= listener->target_temporal_layer) {
4135
					JANUS_LOG(LOG_WARN, "  -- Upscaling temporal layer: %u --> %u\n",
4136
						packet->temporal_layer, listener->target_temporal_layer);
4137
					listener->temporal_layer = packet->temporal_layer;
4138
					temporal_layer = listener->temporal_layer;
4139
				}
4140
			} else if(listener->target_temporal_layer < listener->temporal_layer) {
4141
				/* We need to downscale */
4142
				JANUS_LOG(LOG_WARN, "We need to downscale temporally:\n");
4143
				if(packet->ebit) {
4144
					JANUS_LOG(LOG_WARN, "  -- Downscaling temporal layer: %u --> %u\n",
4145
						listener->temporal_layer, listener->target_temporal_layer);
4146
					listener->temporal_layer = listener->target_temporal_layer;
4147
				}
4148
			}
4149
			if(temporal_layer < packet->temporal_layer) {
4150
				/* Drop the packet: update the context to make sure sequence number is increased normally later */
4151
				JANUS_LOG(LOG_WARN, "Dropping packet (temporal layer %d < %d)\n", temporal_layer, packet->temporal_layer);
4152
				listener->context.v_base_seq++;
4153
				return;
4154
			}
4155
			int spatial_layer = listener->spatial_layer;
4156
			if(listener->target_spatial_layer > listener->spatial_layer) {
4157
				JANUS_LOG(LOG_WARN, "We need to upscale spatially:\n");
4158
				/* We need to upscale */
4159
				if(packet->pbit == 0 && packet->bbit && packet->spatial_layer == listener->spatial_layer+1) {
4160
					JANUS_LOG(LOG_WARN, "  -- Upscaling spatial layer: %u --> %u\n",
4161
						packet->spatial_layer, listener->target_spatial_layer);
4162
					listener->spatial_layer = packet->spatial_layer;
4163
					spatial_layer = listener->spatial_layer;
4164
				}
4165
			} else if(listener->target_spatial_layer < listener->spatial_layer) {
4166
				/* We need to downscale */
4167
				JANUS_LOG(LOG_WARN, "We need to downscale spatially:\n");
4168
				if(packet->ebit) {
4169
					JANUS_LOG(LOG_WARN, "  -- Downscaling spatial layer: %u --> %u\n",
4170
						listener->spatial_layer, listener->target_spatial_layer);
4171
					listener->spatial_layer = listener->target_spatial_layer;
4172
				}
4173
			}
4174
			if(spatial_layer < packet->spatial_layer) {
4175
				/* Drop the packet: update the context to make sure sequence number is increased normally later */
4176
				JANUS_LOG(LOG_HUGE, "Dropping packet (spatial layer %d < %d)\n", spatial_layer, packet->spatial_layer);
4177
				listener->context.v_base_seq++;
4178
				return;
4179
			} else if(packet->ebit && spatial_layer == packet->spatial_layer) {
4180
				/* If we stop at layer 0, we need a marker bit now, as the one from layer 1 will not be received */
4181
				override_mark_bit = TRUE;
4182
			}
4183
			/* If we got here, we can send the frame: this doesn't necessarily mean it's
4184
			 * one of the layers the user wants, as there may be dependencies involved */
4185
			JANUS_LOG(LOG_HUGE, "Sending packet (spatial=%d, temporal=%d)\n",
4186
				packet->spatial_layer, packet->temporal_layer);
4187
		}
3865 4188
		/* Fix sequence number and timestamp (publisher switching may be involved) */
3866 4189
		janus_rtp_header_update(packet->data, &listener->context, TRUE, 4500);
4190
		if(override_mark_bit && !has_marker_bit) {
4191
			packet->data->markerbit = 1;
4192
		}
3867 4193
		if(gateway != NULL)
3868 4194
			gateway->relay_rtp(session->handle, packet->is_video, (char *)packet->data, packet->length);
4195
		if(override_mark_bit && !has_marker_bit) {
4196
			packet->data->markerbit = 0;
4197
		}
3869 4198
		/* Restore the timestamp and sequence number to what the publisher set them to */
3870 4199
		packet->data->timestamp = htonl(packet->timestamp);
3871 4200
		packet->data->seq_number = htons(packet->seq_number);

Also available in: Unified diff