diff --git a/src/server.js b/src/server.js
index c433e33..cff1513 100644
--- a/src/server.js
+++ b/src/server.js
@@ -212,12 +212,16 @@ function setupRest(app) {
return;
}
whep.debug("/endpoint/:", id);
- // If we received an SDP, the client is providing an offer
+ // If we received a payload, make sure it's an SDP
whep.debug(req.body);
- if(req.headers["content-type"] === "application/sdp" && req.body.indexOf('v=0') >= 0) {
- res.status(403);
- res.send('Client offers unsupported');
- return;
+ let offer = null;
+ if(req.headers["content-type"]) {
+ if(req.headers["content-type"] !== "application/sdp" || req.body.indexOf('v=0') < 0) {
+ res.status(406);
+ res.send('Unsupported content type');
+ return;
+ }
+ offer = req.body;
}
// Check the Bearer token
let auth = req.headers["authorization"];
@@ -264,11 +268,12 @@ function setupRest(app) {
let details = {
uuid: uuid,
mountpoint: endpoint.mountpoint,
- pin: endpoint.pin
+ pin: endpoint.pin,
+ sdp: offer
};
subscriber.enabled = true;
janus.subscribe(details, function(err, result) {
- // Make sure we got an OFFER back
+ // Make sure we got an SDP back
if(err) {
delete subscribers[uuid];
res.status(500);
diff --git a/src/whep-janus.js b/src/whep-janus.js
index 3f11253..6123db2 100644
--- a/src/whep-janus.js
+++ b/src/whep-janus.js
@@ -205,6 +205,7 @@ var whepJanus = function(janusConfig) {
}
let mountpoint = details.mountpoint;
let pin = details.pin;
+ let sdp = details.sdp;
let uuid = details.uuid;
let session = sessions[uuid];
if(!session) {
@@ -263,6 +264,10 @@ var whepJanus = function(janusConfig) {
pin: pin
}
};
+ if(sdp) {
+ // We're going to let the user provide the SDP offer
+ subscribe.jsep = { type: 'offer', sdp: sdp };
+ }
janusSend(subscribe, function(response) {
let event = response["janus"];
if(event === "error") {
@@ -284,7 +289,7 @@ var whepJanus = function(janusConfig) {
callback({ error: data.error });
return;
}
- whep.debug("Got an offer for session " + uuid + ":", data);
+ whep.debug("Got an SDP for session " + uuid + ":", data);
if(data["reason"]) {
// Unsubscribe from the transaction
delete that.config.janus.transactions[response["transaction"]];
diff --git a/web/watch.js b/web/watch.js
index 0481eb2..a19e6d9 100644
--- a/web/watch.js
+++ b/web/watch.js
@@ -1,10 +1,10 @@
// Base path for the REST WHEP API
var rest = '/whep';
-var resource = null;
+var resource = null, token = null;
// PeerConnection
var pc = null;
-var iceUfrag = null, icePwd = null;
+var iceUfrag = null, icePwd = null, candidates = [];
// Helper function to get query string arguments
function getQueryStringValue(name) {
@@ -15,6 +15,8 @@ function getQueryStringValue(name) {
}
// Get the endpoint ID to subscribe to
var id = getQueryStringValue('id');
+// Check if we should let the endpoint send the offer
+var sendOffer = (getQueryStringValue('offer') === 'true')
$(document).ready(function() {
// Make sure WebRTC is supported by the browser
@@ -30,88 +32,93 @@ $(document).ready(function() {
title: 'Insert the endpoint token (leave it empty if not needed)',
inputType: 'password',
callback: function(result) {
- subscribeToEndpoint(result);
+ token = result;
+ subscribeToEndpoint();
}
});
});
// Function to subscribe to the WHEP endpoint
-function subscribeToEndpoint(token) {
- let headers = null;
+async function subscribeToEndpoint() {
+ let headers = null, offer = null;
if(token)
headers = { Authorization: 'Bearer ' + token };
+ if(sendOffer) {
+ // We need to prepare an offer ourselves, do it now
+ let iceServers = [{urls: "stun:stun.l.google.com:19302"}];
+ createPeerConnectionIfNeeded(iceServers);
+ let transceiver = await pc.addTransceiver('audio');
+ if(transceiver.setDirection)
+ transceiver.setDirection('recvonly');
+ else
+ transceiver.direction = 'recvonly';
+ transceiver = await pc.addTransceiver('video');
+ if(transceiver.setDirection)
+ transceiver.setDirection('recvonly');
+ else
+ transceiver.direction = 'recvonly';
+ offer = await pc.createOffer({});
+ await pc.setLocalDescription(offer);
+ // Extract ICE ufrag and pwd (for trickle)
+ iceUfrag = offer.sdp.match(/a=ice-ufrag:(.*)\r\n/)[1];
+ icePwd = offer.sdp.match(/a=ice-pwd:(.*)\r\n/)[1];
+ }
+ // Contact the WHEP endpoint
$.ajax({
url: rest + '/endpoint/' + id,
type: 'POST',
headers: headers,
- data: {}
+ contentType: offer ? 'application/sdp' : null,
+ data: offer ? offer.sdp : {}
}).error(function(xhr, textStatus, errorThrown) {
bootbox.alert(xhr.status + ": " + xhr.responseText);
}).success(function(sdp, textStatus, request) {
- console.log('Got offer:', sdp);
+ console.log('Got SDP:', sdp);
resource = request.getResponseHeader('Location');
console.log('WHEP resource:', resource);
// TODO Parse ICE servers
// let ice = request.getResponseHeader('Link');
let iceServers = [{urls: "stun:stun.l.google.com:19302"}];
- // Create PeerConnection
- let pc_config = {
- sdpSemantics: 'unified-plan',
- iceServers: iceServers
+ // Create PeerConnection, if needed
+ createPeerConnectionIfNeeded(iceServers);
+ // Pass the SDP to the PeerConnection
+ let jsep = {
+ type: sendOffer ? 'answer' : 'offer',
+ sdp: sdp
};
- pc = new RTCPeerConnection(pc_config);
- pc.oniceconnectionstatechange = function() {
- console.log('[ICE] ', pc.iceConnectionState);
- };
- pc.onicecandidate = function(event) {
- let end = false;
- if(!event.candidate || (event.candidate.candidate && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
- console.log('End of candidates');
- end = true;
- } else {
- console.log('Got candidate:', event.candidate.candidate);
- }
- if(!resource) {
- console.warn('No resource URL, ignoring candidate');
- return;
- }
- if(!iceUfrag || !icePwd) {
- console.warn('No ICE credentials, ignoring candidate');
- return;
- }
- // FIXME Trickle candidate
- let candidate =
- 'a=ice-ufrag:' + iceUfrag + '\r\n' +
- 'a=ice-pwd:' + icePwd + '\r\n' +
- 'm=audio 9 RTP/AVP 0\r\n' +
- 'a=' + (end ? 'end-of-candidates' : event.candidate.candidate) + '\r\n';
- $.ajax({
- url: resource,
- type: 'PATCH',
- headers: headers,
- contentType: 'application/trickle-ice-sdpfrag',
- data: candidate
- }).error(function(xhr, textStatus, errorThrown) {
- bootbox.alert(xhr.status + ": " + xhr.responseText);
- }).done(function(response) {
- console.log('Candidate sent');
- });
- };
- pc.ontrack = function(event) {
- console.log('Handling Remote Track', event);
- if(!event.streams)
- return;
- if($('#whepvideo').length === 0) {
- $('#video').removeClass('hide').show();
- $('#videoremote').append('');
- }
- attachMediaStream($('#whepvideo').get(0), event.streams[0]);
- };
- // Pass the offer to the PeerConnection
- let jsep = { type: 'offer', sdp: sdp };
pc.setRemoteDescription(jsep)
.then(function() {
console.log('Remote description accepted');
+ if(sendOffer) {
+ // We're done: just check if we have candidates to send
+ if(candidates.length > 0) {
+ // FIXME Trickle candidate
+ let headers = null;
+ if(token)
+ headers = { Authorization: 'Bearer ' + token };
+ let candidate =
+ 'a=ice-ufrag:' + iceUfrag + '\r\n' +
+ 'a=ice-pwd:' + icePwd + '\r\n' +
+ 'm=audio 9 RTP/AVP 0\r\n';
+ for(let c of candidates)
+ candidate += 'a=' + c + '\r\n';
+ candidates = [];
+ $.ajax({
+ url: resource,
+ type: 'PATCH',
+ headers: headers,
+ contentType: 'application/trickle-ice-sdpfrag',
+ data: candidate
+ }).error(function(xhr, textStatus, errorThrown) {
+ bootbox.alert(xhr.status + ": " + xhr.responseText);
+ }).done(function(response) {
+ console.log('Candidate sent');
+ });
+ }
+ return;
+ }
+ // If we got here, we're in the "WHIP server sends offer" mode,
+ // so we have to prepare an answer to send back via a PATCH
pc.createAnswer({})
.then(function(answer) {
console.log('Prepared answer:', answer.sdp);
@@ -157,3 +164,71 @@ function attachMediaStream(element, stream) {
}
}
};
+
+// Helper function to create a PeerConnection, if needed, since we can either
+// expect an offer from the WHEP server, or provide one ourselves
+function createPeerConnectionIfNeeded(iceServers) {
+ if(pc)
+ return;
+ let pc_config = {
+ sdpSemantics: 'unified-plan',
+ iceServers: iceServers
+ };
+ pc = new RTCPeerConnection(pc_config);
+ pc.oniceconnectionstatechange = function() {
+ console.log('[ICE] ', pc.iceConnectionState);
+ };
+ pc.onicecandidate = function(event) {
+ let end = false;
+ if(!event.candidate || (event.candidate.candidate && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
+ console.log('End of candidates');
+ end = true;
+ } else {
+ console.log('Got candidate:', event.candidate.candidate);
+ }
+ if(!resource) {
+ console.log('No resource URL yet, queueing candidate');
+ candidates.push(end ? 'end-of-candidates' : event.candidate.candidate);
+ return;
+ }
+ if(!iceUfrag || !icePwd) {
+ console.log('No ICE credentials yet, queueing candidate');
+ candidates.push(end ? 'end-of-candidates' : event.candidate.candidate);
+ return;
+ }
+ // FIXME Trickle candidate
+ let headers = null;
+ if(token)
+ headers = { Authorization: 'Bearer ' + token };
+ let candidate =
+ 'a=ice-ufrag:' + iceUfrag + '\r\n' +
+ 'a=ice-pwd:' + icePwd + '\r\n' +
+ 'm=audio 9 RTP/AVP 0\r\n' +
+ 'a=' + (end ? 'end-of-candidates' : event.candidate.candidate) + '\r\n';
+ $.ajax({
+ url: resource,
+ type: 'PATCH',
+ headers: headers,
+ contentType: 'application/trickle-ice-sdpfrag',
+ data: candidate
+ }).error(function(xhr, textStatus, errorThrown) {
+ bootbox.alert(xhr.status + ": " + xhr.responseText);
+ }).done(function(response) {
+ console.log('Candidate sent');
+ });
+ };
+ pc.ontrack = function(event) {
+ console.log('Handling Remote Track', event);
+ if(!event.streams)
+ return;
+ console.warn(event.streams[0].getTracks());
+ if($('#whepvideo').length === 0) {
+ $('#video').removeClass('hide').show();
+ $('#videoremote').append('');
+ $('#whepvideo').get(0).volume = 0;
+ }
+ attachMediaStream($('#whepvideo').get(0), event.streams[0]);
+ $('#whepvideo').get(0).play();
+ $('#whepvideo').get(0).volume = 1;
+ };
+}