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