diff --git a/samples/js/apprtc/app.yaml b/samples/js/apprtc/app.yaml index 445a95a95..6ef5e7505 100644 --- a/samples/js/apprtc/app.yaml +++ b/samples/js/apprtc/app.yaml @@ -1,29 +1,29 @@ -application: apprtc -version: 6 -runtime: python27 -threadsafe: true -api_version: 1 - -handlers: -- url: /html - static_dir: html - -- url: /images - static_dir: images - -- url: /js - static_dir: js - -- url: /css - static_dir: css - -- url: /.* - script: apprtc.app - secure: always - -inbound_services: -- channel_presence - -libraries: -- name: jinja2 - version: latest +application: apprtc +version: 6 +runtime: python27 +threadsafe: true +api_version: 1 + +handlers: +- url: /html + static_dir: html + +- url: /images + static_dir: images + +- url: /js + static_dir: js + +- url: /css + static_dir: css + +- url: /.* + script: apprtc.app + secure: always + +inbound_services: +- channel_presence + +libraries: +- name: jinja2 + version: latest diff --git a/samples/js/apprtc/full.html b/samples/js/apprtc/full.html index 2c82728dc..b14ac6009 100644 --- a/samples/js/apprtc/full.html +++ b/samples/js/apprtc/full.html @@ -1,54 +1,55 @@ - - - - - - - -
- -
- - - + + + + + + + +
+ +
+ + + diff --git a/samples/js/apprtc/html/help.html b/samples/js/apprtc/html/help.html index bda9f0be3..7fd2bf624 100644 --- a/samples/js/apprtc/html/help.html +++ b/samples/js/apprtc/html/help.html @@ -1,11 +1,11 @@ - - - - - WebRtc Demo App Help - - -TODO - - + + + + + WebRtc Demo App Help + + +TODO + + diff --git a/samples/js/apprtc/js/main.js b/samples/js/apprtc/js/main.js index 33ad5f9c3..538db7c82 100644 --- a/samples/js/apprtc/js/main.js +++ b/samples/js/apprtc/js/main.js @@ -1,635 +1,635 @@ - var localVideo; - var miniVideo; - var remoteVideo; - var localStream; - var remoteStream; - var channel; - var pc; - var socket; - var xmlhttp; - var started = false; - var turnDone = false; - var channelReady = false; - var signalingReady = false; - var msgQueue = []; - // Set up audio and video regardless of what devices are present. - var sdpConstraints = {'mandatory': { - 'OfferToReceiveAudio': true, - 'OfferToReceiveVideo': true }}; - var isVideoMuted = false; - var isAudioMuted = false; - // Types of gathered ICE Candidates. - var gatheredIceCandidateTypes = { Local: {}, Remote: {} }; - - function initialize() { - console.log('Initializing; room=' + roomKey + '.'); - card = document.getElementById('card'); - localVideo = document.getElementById('localVideo'); - // Reset localVideo display to center. - localVideo.addEventListener('loadedmetadata', function(){ - window.onresize();}); - miniVideo = document.getElementById('miniVideo'); - remoteVideo = document.getElementById('remoteVideo'); - resetStatus(); - // NOTE: AppRTCClient.java searches & parses this line; update there when - // changing here. - openChannel(); - maybeRequestTurn(); - doGetUserMedia(); - // Caller is always ready to create peerConnection. - signalingReady = initiator; - } - - function openChannel() { - console.log('Opening channel.'); - var channel = new goog.appengine.Channel(channelToken); - var handler = { - 'onopen': onChannelOpened, - 'onmessage': onChannelMessage, - 'onerror': onChannelError, - 'onclose': onChannelClosed - }; - socket = channel.open(handler); - } - - function maybeRequestTurn() { - // Skipping TURN Http request for Firefox version <=22. - // Firefox does not support TURN for version <=22. - if (webrtcDetectedBrowser === 'firefox' && webrtcDetectedVersion <=22) { - turnDone = true; - return; - } - - for (var i = 0, len = pcConfig.iceServers.length; i < len; i++) { - if (pcConfig.iceServers[i].url.substr(0, 5) === 'turn:') { - turnDone = true; - return; - } - } - - var currentDomain = document.domain; - if (currentDomain.search('localhost') === -1 && - currentDomain.search('apprtc') === -1) { - // Not authorized domain. Try with default STUN instead. - turnDone = true; - return; - } - - // No TURN server. Get one from computeengineondemand.appspot.com. - xmlhttp = new XMLHttpRequest(); - xmlhttp.onreadystatechange = onTurnResult; - xmlhttp.open('GET', turnUrl, true); - xmlhttp.send(); - } - - function onTurnResult() { - if (xmlhttp.readyState !== 4) - return; - - if (xmlhttp.status === 200) { - var turnServer = JSON.parse(xmlhttp.responseText); - for (i = 0; i < turnServer.uris.length; i++) { - // Create a turnUri using the polyfill (adapter.js). - var iceServer = createIceServer(turnServer.uris[i], - turnServer.username, - turnServer.password); - if (iceServer !== null) { - pcConfig.iceServers.push(iceServer); - } - } - } else { - console.log('Request for TURN server failed.'); - } - // If TURN request failed, continue the call with default STUN. - turnDone = true; - maybeStart(); - } - - function resetStatus() { - if (!initiator) { - setStatus('Waiting for someone to join: \ - ' + roomLink + ''); - } else { - setStatus('Initializing...'); - } - } - - function doGetUserMedia() { - // Call into getUserMedia via the polyfill (adapter.js). - try { - getUserMedia(mediaConstraints, onUserMediaSuccess, - onUserMediaError); - console.log('Requested access to local media with mediaConstraints:\n' + - ' \'' + JSON.stringify(mediaConstraints) + '\''); - } catch (e) { - alert('getUserMedia() failed. Is this a WebRTC capable browser?'); - console.log('getUserMedia failed with exception: ' + e.message); - } - } - - function createPeerConnection() { - try { - // Create an RTCPeerConnection via the polyfill (adapter.js). - pc = new RTCPeerConnection(pcConfig, pcConstraints); - pc.onicecandidate = onIceCandidate; - console.log('Created RTCPeerConnnection with:\n' + - ' config: \'' + JSON.stringify(pcConfig) + '\';\n' + - ' constraints: \'' + JSON.stringify(pcConstraints) + '\'.'); - } catch (e) { - console.log('Failed to create PeerConnection, exception: ' + e.message); - alert('Cannot create RTCPeerConnection object; \ - WebRTC is not supported by this browser.'); - return; - } - pc.onaddstream = onRemoteStreamAdded; - pc.onremovestream = onRemoteStreamRemoved; - } - - function maybeStart() { - if (!started && signalingReady && - localStream && channelReady && turnDone) { - setStatus('Connecting...'); - console.log('Creating PeerConnection.'); - createPeerConnection(); - console.log('Adding local stream.'); - pc.addStream(localStream); - started = true; - - if (initiator) - doCall(); - else - calleeStart(); - } - } - - function setStatus(state) { - document.getElementById('footer').innerHTML = state; - } - - function doCall() { - var constraints = mergeConstraints(offerConstraints, sdpConstraints); - console.log('Sending offer to peer, with constraints: \n' + - ' \'' + JSON.stringify(constraints) + '\'.') - pc.createOffer(setLocalAndSendMessage, null, constraints); - } - - function calleeStart() { - // Callee starts to process cached offer and other messages. - while (msgQueue.length > 0) { - processSignalingMessage(msgQueue.shift()); - } - } - - function doAnswer() { - console.log('Sending answer to peer.'); - pc.createAnswer(setLocalAndSendMessage, null, sdpConstraints); - } - - function mergeConstraints(cons1, cons2) { - var merged = cons1; - for (var name in cons2.mandatory) { - merged.mandatory[name] = cons2.mandatory[name]; - } - merged.optional.concat(cons2.optional); - return merged; - } - - function setLocalAndSendMessage(sessionDescription) { - // Set Opus as the preferred codec in SDP if Opus is present. - sessionDescription.sdp = preferOpus(sessionDescription.sdp); - pc.setLocalDescription(sessionDescription); - sendMessage(sessionDescription); - } - - function sendMessage(message) { - var msgString = JSON.stringify(message); - console.log('C->S: ' + msgString); - // NOTE: AppRTCClient.java searches & parses this line; update there when - // changing here. - path = '/message?r=' + roomKey + '&u=' + me; - var xhr = new XMLHttpRequest(); - xhr.open('POST', path, true); - xhr.send(msgString); - } - - function processSignalingMessage(message) { - if (!started) { - console.log('peerConnection has not been created yet!'); - return; - } - - if (message.type === 'offer') { - // Set Opus in Stereo, if stereo enabled. - if (stereo) - message.sdp = addStereo(message.sdp); - pc.setRemoteDescription(new RTCSessionDescription(message)); - doAnswer(); - } else if (message.type === 'answer') { - // Set Opus in Stereo, if stereo enabled. - if (stereo) - message.sdp = addStereo(message.sdp); - pc.setRemoteDescription(new RTCSessionDescription(message)); - } else if (message.type === 'candidate') { - var candidate = new RTCIceCandidate({sdpMLineIndex: message.label, - candidate: message.candidate}); - noteIceCandidate("Remote", iceCandidateType(message.candidate)); - pc.addIceCandidate(candidate); - } else if (message.type === 'bye') { - onRemoteHangup(); - } - } - - function onChannelOpened() { - console.log('Channel opened.'); - channelReady = true; - maybeStart(); - } - function onChannelMessage(message) { - console.log('S->C: ' + message.data); - var msg = JSON.parse(message.data); - // Since the turn response is async and also GAE might disorder the - // Message delivery due to possible datastore query at server side, - // So callee needs to cache messages before peerConnection is created. - if (!initiator && !started) { - if (msg.type === 'offer') { - // Add offer to the beginning of msgQueue, since we can't handle - // Early candidates before offer at present. - msgQueue.unshift(msg); - // Callee creates PeerConnection - signalingReady = true; - maybeStart(); - } else { - msgQueue.push(msg); - } - } else { - processSignalingMessage(msg); - } - } - function onChannelError() { - console.log('Channel error.'); - } - function onChannelClosed() { - console.log('Channel closed.'); - } - - function onUserMediaSuccess(stream) { - console.log('User has granted access to local media.'); - // Call the polyfill wrapper to attach the media stream to this element. - attachMediaStream(localVideo, stream); - localVideo.style.opacity = 1; - localStream = stream; - // Caller creates PeerConnection. - maybeStart(); - } - - function onUserMediaError(error) { - console.log('Failed to get access to local media. Error code was ' + - error.code); - alert('Failed to get access to local media. Error code was ' + - error.code + '.'); - } - - function iceCandidateType(candidateSDP) { - if (candidateSDP.indexOf("typ relay ") >= 0) - return "TURN"; - if (candidateSDP.indexOf("typ srflx ") >= 0) - return "STUN"; - if (candidateSDP.indexOf("typ host ") >= 0) - return "HOST"; - return "UNKNOWN"; - } - - function onIceCandidate(event) { - if (event.candidate) { - sendMessage({type: 'candidate', - label: event.candidate.sdpMLineIndex, - id: event.candidate.sdpMid, - candidate: event.candidate.candidate}); - noteIceCandidate("Local", iceCandidateType(event.candidate.candidate)); - } else { - console.log('End of candidates.'); - } - } - - function onRemoteStreamAdded(event) { - console.log('Remote stream added.'); - reattachMediaStream(miniVideo, localVideo); - attachMediaStream(remoteVideo, event.stream); - remoteStream = event.stream; - waitForRemoteVideo(); - } - - function onRemoteStreamRemoved(event) { - console.log('Remote stream removed.'); - } - - function onHangup() { - console.log('Hanging up.'); - transitionToDone(); - localStream.stop(); - stop(); - // will trigger BYE from server - socket.close(); - } - - function onRemoteHangup() { - console.log('Session terminated.'); - initiator = 0; - transitionToWaiting(); - stop(); - } - - function stop() { - started = false; - signalingReady = false; - isAudioMuted = false; - isVideoMuted = false; - pc.close(); - pc = null; - msgQueue.length = 0; - } - - function waitForRemoteVideo() { - // Call the getVideoTracks method via adapter.js. - videoTracks = remoteStream.getVideoTracks(); - if (videoTracks.length === 0 || remoteVideo.currentTime > 0) { - transitionToActive(); - } else { - setTimeout(waitForRemoteVideo, 100); - } - } - - function transitionToActive() { - remoteVideo.style.opacity = 1; - card.style.webkitTransform = 'rotateY(180deg)'; - setTimeout(function() { localVideo.src = ''; }, 500); - setTimeout(function() { miniVideo.style.opacity = 1; }, 1000); - // Reset window display according to the asperio of remote video. - window.onresize(); - setStatus(''); - } - - function transitionToWaiting() { - card.style.webkitTransform = 'rotateY(0deg)'; - setTimeout(function() { - localVideo.src = miniVideo.src; - miniVideo.src = ''; - remoteVideo.src = '' }, 500); - miniVideo.style.opacity = 0; - remoteVideo.style.opacity = 0; - resetStatus(); - } - - function transitionToDone() { - localVideo.style.opacity = 0; - remoteVideo.style.opacity = 0; - miniVideo.style.opacity = 0; - setStatus('You have left the call. \ - Click here to rejoin.'); - } - - function enterFullScreen() { - container.webkitRequestFullScreen(); - } - - function noteIceCandidate(location, type) { - if (gatheredIceCandidateTypes[location][type]) - return; - gatheredIceCandidateTypes[location][type] = 1; - updateInfoDiv(); - } - - function getInfoDiv() { - return document.getElementById("infoDiv"); - } - - function updateInfoDiv() { - var contents = "
Gathered ICE Candidates\n";
-    for (var endpoint in gatheredIceCandidateTypes) {
-      contents += endpoint + ":\n";
-      for (var type in gatheredIceCandidateTypes[endpoint])
-        contents += "  " + type + "\n";
-    }
-    var div = getInfoDiv();
-    div.innerHTML = contents + "
"; - } - - function toggleInfoDivDisplay() { - var div = getInfoDiv(); - if (div.style.display == "block") { - div.style.display = "none"; - } else { - div.style.display = "block"; - } - } - - function toggleVideoMute() { - // Call the getVideoTracks method via adapter.js. - videoTracks = localStream.getVideoTracks(); - - if (videoTracks.length === 0) { - console.log('No local video available.'); - return; - } - - if (isVideoMuted) { - for (i = 0; i < videoTracks.length; i++) { - videoTracks[i].enabled = true; - } - console.log('Video unmuted.'); - } else { - for (i = 0; i < videoTracks.length; i++) { - videoTracks[i].enabled = false; - } - console.log('Video muted.'); - } - - isVideoMuted = !isVideoMuted; - } - - function toggleAudioMute() { - // Call the getAudioTracks method via adapter.js. - audioTracks = localStream.getAudioTracks(); - - if (audioTracks.length === 0) { - console.log('No local audio available.'); - return; - } - - if (isAudioMuted) { - for (i = 0; i < audioTracks.length; i++) { - audioTracks[i].enabled = true; - } - console.log('Audio unmuted.'); - } else { - for (i = 0; i < audioTracks.length; i++){ - audioTracks[i].enabled = false; - } - console.log('Audio muted.'); - } - - isAudioMuted = !isAudioMuted; - } - - // Mac: hotkey is Command. - // Non-Mac: hotkey is Control. - // -D: toggle audio mute. - // -E: toggle video mute. - // -I: toggle Info box. - // Return false to screen out original Chrome shortcuts. - document.onkeydown = function(event) { - var hotkey = event.ctrlKey; - if (navigator.appVersion.indexOf('Mac') != -1) - hotkey = event.metaKey; - if (!hotkey) - return; - switch (event.keyCode) { - case 68: - toggleAudioMute(); - return false; - case 69: - toggleVideoMute(); - return false; - case 73: - toggleInfoDivDisplay(); - return false; - default: - return; - } - } - - // Set Opus as the default audio codec if it's present. - function preferOpus(sdp) { - var sdpLines = sdp.split('\r\n'); - - // Search for m line. - for (var i = 0; i < sdpLines.length; i++) { - if (sdpLines[i].search('m=audio') !== -1) { - var mLineIndex = i; - break; - } - } - if (mLineIndex === null) - return sdp; - - // If Opus is available, set it as the default in m line. - for (var i = 0; i < sdpLines.length; i++) { - if (sdpLines[i].search('opus/48000') !== -1) { - var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); - if (opusPayload) - sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], - opusPayload); - break; - } - } - - // Remove CN in m line and sdp. - sdpLines = removeCN(sdpLines, mLineIndex); - - sdp = sdpLines.join('\r\n'); - return sdp; - } - - // Set Opus in stereo if stereo is enabled. - function addStereo(sdp) { - var sdpLines = sdp.split('\r\n'); - - // Find opus payload. - for (var i = 0; i < sdpLines.length; i++) { - if (sdpLines[i].search('opus/48000') !== -1) { - var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); - break; - } - } - - // Find the payload in fmtp line. - for (var i = 0; i < sdpLines.length; i++) { - if (sdpLines[i].search('a=fmtp') !== -1) { - var payload = extractSdp(sdpLines[i], /a=fmtp:(\d+)/ ); - if (payload === opusPayload) { - var fmtpLineIndex = i; - break; - } - } - } - // No fmtp line found. - if (fmtpLineIndex === null) - return sdp; - - // Append stereo=1 to fmtp line. - sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1'); - - sdp = sdpLines.join('\r\n'); - return sdp; - } - - function extractSdp(sdpLine, pattern) { - var result = sdpLine.match(pattern); - return (result && result.length == 2)? result[1]: null; - } - - // Set the selected codec to the first in m line. - function setDefaultCodec(mLine, payload) { - var elements = mLine.split(' '); - var newLine = new Array(); - var index = 0; - for (var i = 0; i < elements.length; i++) { - if (index === 3) // Format of media starts from the fourth. - newLine[index++] = payload; // Put target payload to the first. - if (elements[i] !== payload) - newLine[index++] = elements[i]; - } - return newLine.join(' '); - } - - // Strip CN from sdp before CN constraints is ready. - function removeCN(sdpLines, mLineIndex) { - var mLineElements = sdpLines[mLineIndex].split(' '); - // Scan from end for the convenience of removing an item. - for (var i = sdpLines.length-1; i >= 0; i--) { - var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); - if (payload) { - var cnPos = mLineElements.indexOf(payload); - if (cnPos !== -1) { - // Remove CN payload from m line. - mLineElements.splice(cnPos, 1); - } - // Remove CN line in sdp - sdpLines.splice(i, 1); - } - } - - sdpLines[mLineIndex] = mLineElements.join(' '); - return sdpLines; - } - - // Send BYE on refreshing(or leaving) a demo page - // to ensure the room is cleaned for next session. - window.onbeforeunload = function() { - sendMessage({type: 'bye'}); - } - - // Set the video diplaying in the center of window. - window.onresize = function(){ - var aspectRatio; - if (remoteVideo.style.opacity === '1') { - aspectRatio = remoteVideo.videoWidth/remoteVideo.videoHeight; - } else if (localVideo.style.opacity === '1') { - aspectRatio = localVideo.videoWidth/localVideo.videoHeight; - } else { - return; - } - - var innerHeight = this.innerHeight; - var innerWidth = this.innerWidth; - var videoWidth = innerWidth < aspectRatio * window.innerHeight ? - innerWidth : aspectRatio * window.innerHeight; - var videoHeight = innerHeight < window.innerWidth / aspectRatio ? - innerHeight : window.innerWidth / aspectRatio; - containerDiv = document.getElementById('container'); - containerDiv.style.width = videoWidth + 'px'; - containerDiv.style.height = videoHeight + 'px'; - containerDiv.style.left = (innerWidth - videoWidth) / 2 + 'px'; - containerDiv.style.top = (innerHeight - videoHeight) / 2 + 'px'; - }; +var localVideo; +var miniVideo; +var remoteVideo; +var localStream; +var remoteStream; +var channel; +var pc; +var socket; +var xmlhttp; +var started = false; +var turnDone = false; +var channelReady = false; +var signalingReady = false; +var msgQueue = []; +// Set up audio and video regardless of what devices are present. +var sdpConstraints = {'mandatory': { + 'OfferToReceiveAudio': true, + 'OfferToReceiveVideo': true }}; +var isVideoMuted = false; +var isAudioMuted = false; +// Types of gathered ICE Candidates. +var gatheredIceCandidateTypes = { Local: {}, Remote: {} }; + +function initialize() { + console.log('Initializing; room=' + roomKey + '.'); + card = document.getElementById('card'); + localVideo = document.getElementById('localVideo'); + // Reset localVideo display to center. + localVideo.addEventListener('loadedmetadata', function(){ + window.onresize();}); + miniVideo = document.getElementById('miniVideo'); + remoteVideo = document.getElementById('remoteVideo'); + resetStatus(); + // NOTE: AppRTCClient.java searches & parses this line; update there when + // changing here. + openChannel(); + maybeRequestTurn(); + doGetUserMedia(); + // Caller is always ready to create peerConnection. + signalingReady = initiator; +} + +function openChannel() { + console.log('Opening channel.'); + var channel = new goog.appengine.Channel(channelToken); + var handler = { + 'onopen': onChannelOpened, + 'onmessage': onChannelMessage, + 'onerror': onChannelError, + 'onclose': onChannelClosed + }; + socket = channel.open(handler); +} + +function maybeRequestTurn() { + // Skipping TURN Http request for Firefox version <=22. + // Firefox does not support TURN for version <=22. + if (webrtcDetectedBrowser === 'firefox' && webrtcDetectedVersion <=22) { + turnDone = true; + return; + } + + for (var i = 0, len = pcConfig.iceServers.length; i < len; i++) { + if (pcConfig.iceServers[i].url.substr(0, 5) === 'turn:') { + turnDone = true; + return; + } + } + + var currentDomain = document.domain; + if (currentDomain.search('localhost') === -1 && + currentDomain.search('apprtc') === -1) { + // Not authorized domain. Try with default STUN instead. + turnDone = true; + return; + } + + // No TURN server. Get one from computeengineondemand.appspot.com. + xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = onTurnResult; + xmlhttp.open('GET', turnUrl, true); + xmlhttp.send(); +} + +function onTurnResult() { + if (xmlhttp.readyState !== 4) + return; + + if (xmlhttp.status === 200) { + var turnServer = JSON.parse(xmlhttp.responseText); + for (i = 0; i < turnServer.uris.length; i++) { + // Create a turnUri using the polyfill (adapter.js). + var iceServer = createIceServer(turnServer.uris[i], + turnServer.username, + turnServer.password); + if (iceServer !== null) { + pcConfig.iceServers.push(iceServer); + } + } + } else { + console.log('Request for TURN server failed.'); + } + // If TURN request failed, continue the call with default STUN. + turnDone = true; + maybeStart(); +} + +function resetStatus() { + if (!initiator) { + setStatus('Waiting for someone to join: \ + ' + roomLink + ''); + } else { + setStatus('Initializing...'); + } +} + +function doGetUserMedia() { + // Call into getUserMedia via the polyfill (adapter.js). + try { + getUserMedia(mediaConstraints, onUserMediaSuccess, + onUserMediaError); + console.log('Requested access to local media with mediaConstraints:\n' + + ' \'' + JSON.stringify(mediaConstraints) + '\''); + } catch (e) { + alert('getUserMedia() failed. Is this a WebRTC capable browser?'); + console.log('getUserMedia failed with exception: ' + e.message); + } +} + +function createPeerConnection() { + try { + // Create an RTCPeerConnection via the polyfill (adapter.js). + pc = new RTCPeerConnection(pcConfig, pcConstraints); + pc.onicecandidate = onIceCandidate; + console.log('Created RTCPeerConnnection with:\n' + + ' config: \'' + JSON.stringify(pcConfig) + '\';\n' + + ' constraints: \'' + JSON.stringify(pcConstraints) + '\'.'); + } catch (e) { + console.log('Failed to create PeerConnection, exception: ' + e.message); + alert('Cannot create RTCPeerConnection object; \ + WebRTC is not supported by this browser.'); + return; + } + pc.onaddstream = onRemoteStreamAdded; + pc.onremovestream = onRemoteStreamRemoved; +} + +function maybeStart() { + if (!started && signalingReady && + localStream && channelReady && turnDone) { + setStatus('Connecting...'); + console.log('Creating PeerConnection.'); + createPeerConnection(); + console.log('Adding local stream.'); + pc.addStream(localStream); + started = true; + + if (initiator) + doCall(); + else + calleeStart(); + } +} + +function setStatus(state) { + document.getElementById('footer').innerHTML = state; +} + +function doCall() { + var constraints = mergeConstraints(offerConstraints, sdpConstraints); + console.log('Sending offer to peer, with constraints: \n' + + ' \'' + JSON.stringify(constraints) + '\'.') + pc.createOffer(setLocalAndSendMessage, null, constraints); +} + +function calleeStart() { + // Callee starts to process cached offer and other messages. + while (msgQueue.length > 0) { + processSignalingMessage(msgQueue.shift()); + } +} + +function doAnswer() { + console.log('Sending answer to peer.'); + pc.createAnswer(setLocalAndSendMessage, null, sdpConstraints); +} + +function mergeConstraints(cons1, cons2) { + var merged = cons1; + for (var name in cons2.mandatory) { + merged.mandatory[name] = cons2.mandatory[name]; + } + merged.optional.concat(cons2.optional); + return merged; +} + +function setLocalAndSendMessage(sessionDescription) { + // Set Opus as the preferred codec in SDP if Opus is present. + sessionDescription.sdp = preferOpus(sessionDescription.sdp); + pc.setLocalDescription(sessionDescription); + sendMessage(sessionDescription); +} + +function sendMessage(message) { + var msgString = JSON.stringify(message); + console.log('C->S: ' + msgString); + // NOTE: AppRTCClient.java searches & parses this line; update there when + // changing here. + path = '/message?r=' + roomKey + '&u=' + me; + var xhr = new XMLHttpRequest(); + xhr.open('POST', path, true); + xhr.send(msgString); +} + +function processSignalingMessage(message) { + if (!started) { + console.log('peerConnection has not been created yet!'); + return; + } + + if (message.type === 'offer') { + // Set Opus in Stereo, if stereo enabled. + if (stereo) + message.sdp = addStereo(message.sdp); + pc.setRemoteDescription(new RTCSessionDescription(message)); + doAnswer(); + } else if (message.type === 'answer') { + // Set Opus in Stereo, if stereo enabled. + if (stereo) + message.sdp = addStereo(message.sdp); + pc.setRemoteDescription(new RTCSessionDescription(message)); + } else if (message.type === 'candidate') { + var candidate = new RTCIceCandidate({sdpMLineIndex: message.label, + candidate: message.candidate}); + noteIceCandidate("Remote", iceCandidateType(message.candidate)); + pc.addIceCandidate(candidate); + } else if (message.type === 'bye') { + onRemoteHangup(); + } +} + +function onChannelOpened() { + console.log('Channel opened.'); + channelReady = true; + maybeStart(); +} +function onChannelMessage(message) { + console.log('S->C: ' + message.data); + var msg = JSON.parse(message.data); + // Since the turn response is async and also GAE might disorder the + // Message delivery due to possible datastore query at server side, + // So callee needs to cache messages before peerConnection is created. + if (!initiator && !started) { + if (msg.type === 'offer') { + // Add offer to the beginning of msgQueue, since we can't handle + // Early candidates before offer at present. + msgQueue.unshift(msg); + // Callee creates PeerConnection + signalingReady = true; + maybeStart(); + } else { + msgQueue.push(msg); + } + } else { + processSignalingMessage(msg); + } +} +function onChannelError() { + console.log('Channel error.'); +} +function onChannelClosed() { + console.log('Channel closed.'); +} + +function onUserMediaSuccess(stream) { + console.log('User has granted access to local media.'); + // Call the polyfill wrapper to attach the media stream to this element. + attachMediaStream(localVideo, stream); + localVideo.style.opacity = 1; + localStream = stream; + // Caller creates PeerConnection. + maybeStart(); +} + +function onUserMediaError(error) { + console.log('Failed to get access to local media. Error code was ' + + error.code); + alert('Failed to get access to local media. Error code was ' + + error.code + '.'); +} + +function iceCandidateType(candidateSDP) { + if (candidateSDP.indexOf("typ relay ") >= 0) + return "TURN"; + if (candidateSDP.indexOf("typ srflx ") >= 0) + return "STUN"; + if (candidateSDP.indexOf("typ host ") >= 0) + return "HOST"; + return "UNKNOWN"; +} + +function onIceCandidate(event) { + if (event.candidate) { + sendMessage({type: 'candidate', + label: event.candidate.sdpMLineIndex, + id: event.candidate.sdpMid, + candidate: event.candidate.candidate}); + noteIceCandidate("Local", iceCandidateType(event.candidate.candidate)); + } else { + console.log('End of candidates.'); + } +} + +function onRemoteStreamAdded(event) { + console.log('Remote stream added.'); + reattachMediaStream(miniVideo, localVideo); + attachMediaStream(remoteVideo, event.stream); + remoteStream = event.stream; + waitForRemoteVideo(); +} + +function onRemoteStreamRemoved(event) { + console.log('Remote stream removed.'); +} + +function onHangup() { + console.log('Hanging up.'); + transitionToDone(); + localStream.stop(); + stop(); + // will trigger BYE from server + socket.close(); +} + +function onRemoteHangup() { + console.log('Session terminated.'); + initiator = 0; + transitionToWaiting(); + stop(); +} + +function stop() { + started = false; + signalingReady = false; + isAudioMuted = false; + isVideoMuted = false; + pc.close(); + pc = null; + msgQueue.length = 0; +} + +function waitForRemoteVideo() { + // Call the getVideoTracks method via adapter.js. + videoTracks = remoteStream.getVideoTracks(); + if (videoTracks.length === 0 || remoteVideo.currentTime > 0) { + transitionToActive(); + } else { + setTimeout(waitForRemoteVideo, 100); + } +} + +function transitionToActive() { + remoteVideo.style.opacity = 1; + card.style.webkitTransform = 'rotateY(180deg)'; + setTimeout(function() { localVideo.src = ''; }, 500); + setTimeout(function() { miniVideo.style.opacity = 1; }, 1000); + // Reset window display according to the asperio of remote video. + window.onresize(); + setStatus(''); +} + +function transitionToWaiting() { + card.style.webkitTransform = 'rotateY(0deg)'; + setTimeout(function() { + localVideo.src = miniVideo.src; + miniVideo.src = ''; + remoteVideo.src = '' }, 500); + miniVideo.style.opacity = 0; + remoteVideo.style.opacity = 0; + resetStatus(); +} + +function transitionToDone() { + localVideo.style.opacity = 0; + remoteVideo.style.opacity = 0; + miniVideo.style.opacity = 0; + setStatus('You have left the call. \ + Click here to rejoin.'); +} + +function enterFullScreen() { + container.webkitRequestFullScreen(); +} + +function noteIceCandidate(location, type) { + if (gatheredIceCandidateTypes[location][type]) + return; + gatheredIceCandidateTypes[location][type] = 1; + updateInfoDiv(); +} + +function getInfoDiv() { + return document.getElementById("infoDiv"); +} + +function updateInfoDiv() { + var contents = "
Gathered ICE Candidates\n";
+  for (var endpoint in gatheredIceCandidateTypes) {
+    contents += endpoint + ":\n";
+    for (var type in gatheredIceCandidateTypes[endpoint])
+      contents += "  " + type + "\n";
+  }
+  var div = getInfoDiv();
+  div.innerHTML = contents + "
"; +} + +function toggleInfoDivDisplay() { + var div = getInfoDiv(); + if (div.style.display == "block") { + div.style.display = "none"; + } else { + div.style.display = "block"; + } +} + +function toggleVideoMute() { + // Call the getVideoTracks method via adapter.js. + videoTracks = localStream.getVideoTracks(); + + if (videoTracks.length === 0) { + console.log('No local video available.'); + return; + } + + if (isVideoMuted) { + for (i = 0; i < videoTracks.length; i++) { + videoTracks[i].enabled = true; + } + console.log('Video unmuted.'); + } else { + for (i = 0; i < videoTracks.length; i++) { + videoTracks[i].enabled = false; + } + console.log('Video muted.'); + } + + isVideoMuted = !isVideoMuted; +} + +function toggleAudioMute() { + // Call the getAudioTracks method via adapter.js. + audioTracks = localStream.getAudioTracks(); + + if (audioTracks.length === 0) { + console.log('No local audio available.'); + return; + } + + if (isAudioMuted) { + for (i = 0; i < audioTracks.length; i++) { + audioTracks[i].enabled = true; + } + console.log('Audio unmuted.'); + } else { + for (i = 0; i < audioTracks.length; i++){ + audioTracks[i].enabled = false; + } + console.log('Audio muted.'); + } + + isAudioMuted = !isAudioMuted; +} + +// Mac: hotkey is Command. +// Non-Mac: hotkey is Control. +// -D: toggle audio mute. +// -E: toggle video mute. +// -I: toggle Info box. +// Return false to screen out original Chrome shortcuts. +document.onkeydown = function(event) { + var hotkey = event.ctrlKey; + if (navigator.appVersion.indexOf('Mac') != -1) + hotkey = event.metaKey; + if (!hotkey) + return; + switch (event.keyCode) { + case 68: + toggleAudioMute(); + return false; + case 69: + toggleVideoMute(); + return false; + case 73: + toggleInfoDivDisplay(); + return false; + default: + return; + } +} + +// Set Opus as the default audio codec if it's present. +function preferOpus(sdp) { + var sdpLines = sdp.split('\r\n'); + + // Search for m line. + for (var i = 0; i < sdpLines.length; i++) { + if (sdpLines[i].search('m=audio') !== -1) { + var mLineIndex = i; + break; + } + } + if (mLineIndex === null) + return sdp; + + // If Opus is available, set it as the default in m line. + for (var i = 0; i < sdpLines.length; i++) { + if (sdpLines[i].search('opus/48000') !== -1) { + var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); + if (opusPayload) + sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], + opusPayload); + break; + } + } + + // Remove CN in m line and sdp. + sdpLines = removeCN(sdpLines, mLineIndex); + + sdp = sdpLines.join('\r\n'); + return sdp; +} + +// Set Opus in stereo if stereo is enabled. +function addStereo(sdp) { + var sdpLines = sdp.split('\r\n'); + + // Find opus payload. + for (var i = 0; i < sdpLines.length; i++) { + if (sdpLines[i].search('opus/48000') !== -1) { + var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); + break; + } + } + + // Find the payload in fmtp line. + for (var i = 0; i < sdpLines.length; i++) { + if (sdpLines[i].search('a=fmtp') !== -1) { + var payload = extractSdp(sdpLines[i], /a=fmtp:(\d+)/ ); + if (payload === opusPayload) { + var fmtpLineIndex = i; + break; + } + } + } + // No fmtp line found. + if (fmtpLineIndex === null) + return sdp; + + // Append stereo=1 to fmtp line. + sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1'); + + sdp = sdpLines.join('\r\n'); + return sdp; +} + +function extractSdp(sdpLine, pattern) { + var result = sdpLine.match(pattern); + return (result && result.length == 2)? result[1]: null; +} + +// Set the selected codec to the first in m line. +function setDefaultCodec(mLine, payload) { + var elements = mLine.split(' '); + var newLine = new Array(); + var index = 0; + for (var i = 0; i < elements.length; i++) { + if (index === 3) // Format of media starts from the fourth. + newLine[index++] = payload; // Put target payload to the first. + if (elements[i] !== payload) + newLine[index++] = elements[i]; + } + return newLine.join(' '); +} + +// Strip CN from sdp before CN constraints is ready. +function removeCN(sdpLines, mLineIndex) { + var mLineElements = sdpLines[mLineIndex].split(' '); + // Scan from end for the convenience of removing an item. + for (var i = sdpLines.length-1; i >= 0; i--) { + var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); + if (payload) { + var cnPos = mLineElements.indexOf(payload); + if (cnPos !== -1) { + // Remove CN payload from m line. + mLineElements.splice(cnPos, 1); + } + // Remove CN line in sdp + sdpLines.splice(i, 1); + } + } + + sdpLines[mLineIndex] = mLineElements.join(' '); + return sdpLines; +} + +// Send BYE on refreshing(or leaving) a demo page +// to ensure the room is cleaned for next session. +window.onbeforeunload = function() { + sendMessage({type: 'bye'}); +} + +// Set the video diplaying in the center of window. +window.onresize = function(){ + var aspectRatio; + if (remoteVideo.style.opacity === '1') { + aspectRatio = remoteVideo.videoWidth/remoteVideo.videoHeight; + } else if (localVideo.style.opacity === '1') { + aspectRatio = localVideo.videoWidth/localVideo.videoHeight; + } else { + return; + } + + var innerHeight = this.innerHeight; + var innerWidth = this.innerWidth; + var videoWidth = innerWidth < aspectRatio * window.innerHeight ? + innerWidth : aspectRatio * window.innerHeight; + var videoHeight = innerHeight < window.innerWidth / aspectRatio ? + innerHeight : window.innerWidth / aspectRatio; + containerDiv = document.getElementById('container'); + containerDiv.style.width = videoWidth + 'px'; + containerDiv.style.height = videoHeight + 'px'; + containerDiv.style.left = (innerWidth - videoWidth) / 2 + 'px'; + containerDiv.style.top = (innerHeight - videoHeight) / 2 + 'px'; +};