From 5f8f112a7bbd66ef5a7a04aa4369b4d00f53fb59 Mon Sep 17 00:00:00 2001 From: "braveyao@webrtc.org" Date: Wed, 22 May 2013 07:27:05 +0000 Subject: [PATCH] Not to request to TURN server for local tests. Follow-up work to issue1197. BUG=1197 TEST=Manual test R=dutton@google.com Review URL: https://webrtc-codereview.appspot.com/1340004 git-svn-id: http://webrtc.googlecode.com/svn/trunk@4083 4adac7df-926f-26a2-2b94-8c16560cd09d --- samples/js/apprtc/app.yaml | 3 + samples/js/apprtc/css/main.css | 95 +++++ samples/js/apprtc/index.html | 620 ++------------------------------- samples/js/apprtc/js/main.js | 541 ++++++++++++++++++++++++++++ 4 files changed, 662 insertions(+), 597 deletions(-) create mode 100644 samples/js/apprtc/css/main.css create mode 100644 samples/js/apprtc/js/main.js diff --git a/samples/js/apprtc/app.yaml b/samples/js/apprtc/app.yaml index 8da5040b7..445a95a95 100644 --- a/samples/js/apprtc/app.yaml +++ b/samples/js/apprtc/app.yaml @@ -14,6 +14,9 @@ handlers: - url: /js static_dir: js +- url: /css + static_dir: css + - url: /.* script: apprtc.app secure: always diff --git a/samples/js/apprtc/css/main.css b/samples/js/apprtc/css/main.css new file mode 100644 index 000000000..7228e5c25 --- /dev/null +++ b/samples/js/apprtc/css/main.css @@ -0,0 +1,95 @@ + a:link { color: #ffffff; } + a:visited {color: #ffffff; } + html, body { + background-color: #000000; + height: 100%; + font-family: Verdana, Arial, Helvetica, sans-serif; + } + body { + margin: 0; + padding: 0; + } + #container { + background-color: #000000; + position: absolute; + height: 100%; + width: 100%; + margin: 0px auto; + -webkit-perspective: 1000; + } + #card { + -webkit-transition-property: rotation; + -webkit-transition-duration: 2s; + -webkit-transform-style: preserve-3d; + } + #local { + position: absolute; + width: 100%; + -webkit-transform: scale(-1, 1); + -webkit-backface-visibility: hidden; + } + #remote { + position: absolute; + width: 100%; + -webkit-transform: rotateY(180deg); + -webkit-backface-visibility: hidden; + } + #mini { + position: absolute; + height: 30%; + width: 30%; + bottom: 32px; + right: 4px; + -webkit-transform: scale(-1, 1); + opacity: 1.0; + } + #localVideo { + width: 100%; + height: 100%; + opacity: 0; + -webkit-transition-property: opacity; + -webkit-transition-duration: 2s; + } + #remoteVideo { + width: 100%; + height: 100%; + opacity: 0; + -webkit-transition-property: opacity; + -webkit-transition-duration: 2s; + } + #miniVideo { + width: 100%; + height: 100%; + opacity: 0; + -webkit-transition-property: opacity; + -webkit-transition-duration: 2s; + } + #footer { + position: absolute; + bottom: 0; + width: 100%; + height: 28px; + background-color: #3F3F3F; + color: rgb(255, 255, 255); + font-size: 13px; font-weight: bold; + line-height: 28px; + text-align: center; + } + #hangup { + font-size: 13px; font-weight: bold; + color: #FFFFFF; + width: 128px; + height: 24px; + background-color: #808080; + border-style: solid; + border-color: #FFFFFF; + margin: 2px; + } + #logo { + display: block; + top: 4; + right: 4; + position: absolute; + float: right; + opacity: 0.5; + } diff --git a/samples/js/apprtc/index.html b/samples/js/apprtc/index.html index 3d40d910d..470395e12 100644 --- a/samples/js/apprtc/index.html +++ b/samples/js/apprtc/index.html @@ -1,624 +1,50 @@ - + WebRTC Reference App - - - + + + + + + + -
-
-
-
+ diff --git a/samples/js/apprtc/js/main.js b/samples/js/apprtc/js/main.js new file mode 100644 index 000000000..89a2bfdc1 --- /dev/null +++ b/samples/js/apprtc/js/main.js @@ -0,0 +1,541 @@ + 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; + // Set up audio and video regardless of what devices are present. + var sdpConstraints = {'mandatory': { + 'OfferToReceiveAudio': true, + 'OfferToReceiveVideo': true }}; + var isVideoMuted = false; + var isAudioMuted = false; + var aspectRatio; + + function initialize() { + console.log('Initializing; room=' + roomKey + '.'); + card = document.getElementById('card'); + localVideo = document.getElementById('localVideo'); + // Reset localVideo display to center. + localVideo.addEventListener("loadedmetadata", function(){ + aspectRatio = this.videoWidth / this.videoHeight; + 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(); + } + + 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() { + 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.appspot.com') === -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 && xmlhttp.status == 200) { + var turnServer = JSON.parse(xmlhttp.responseText); + pcConfig.iceServers.push({ + 'url': 'turn:' + turnServer.username + '@' + turnServer.turn, + 'credential': turnServer.password + }); + } else { + console.log("Request for TURN server failed.") + } + // If TURN request failed, continue the call with default STUN. + turnDone = true; + } + + 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 && localStream && channelReady && turnDone) { + setStatus('Connecting...'); + console.log('Creating PeerConnection.'); + createPeerConnection(); + console.log('Adding local stream.'); + pc.addStream(localStream); + started = true; + // Caller initiates offer to peer. + if (initiator) + doCall(); + } else { + setTimeout(maybeStart, 100); + } + } + + 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 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) { + var msg = JSON.parse(message); + + if (msg.type === 'offer') { + // Callee creates PeerConnection + if (!initiator && !started) + maybeStart(); + // Set Opus in Stereo, if stereo enabled. + if (stereo) + msg.sdp = addStereo(msg.sdp); + pc.setRemoteDescription(new RTCSessionDescription(msg)); + doAnswer(); + } else if (msg.type === 'answer' && started) { + // Set Opus in Stereo, if stereo enabled. + if (stereo) + msg.sdp = addStereo(msg.sdp); + pc.setRemoteDescription(new RTCSessionDescription(msg)); + } else if (msg.type === 'candidate' && started) { + var candidate = new RTCIceCandidate({sdpMLineIndex: msg.label, + candidate: msg.candidate}); + pc.addIceCandidate(candidate); + } else if (msg.type === 'bye' && started) { + onRemoteHangup(); + } + } + + function onChannelOpened() { + console.log('Channel opened.'); + channelReady = true; + } + function onChannelMessage(message) { + console.log('S->C: ' + message.data); + processSignalingMessage(message.data); + } + 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. + if (initiator) 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 onIceCandidate(event) { + if (event.candidate) { + sendMessage({type: 'candidate', + label: event.candidate.sdpMLineIndex, + id: event.candidate.sdpMid, + candidate: 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(); + stop(); + // will trigger BYE from server + socket.close(); + } + + function onRemoteHangup() { + console.log('Session terminated.'); + initiator = 0; + transitionToWaiting(); + stop(); + } + + function stop() { + started = false; + isAudioMuted = false; + isVideoMuted = false; + pc.close(); + pc = null; + } + + 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 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; + } + + // Ctrl-D: toggle audio mute; Ctrl-E: toggle video mute. + // On Mac, Command key is instead of Ctrl. + // Return false to screen out original Chrome shortcuts. + document.onkeydown = function() { + if (navigator.appVersion.indexOf('Mac') != -1) { + if (event.metaKey && event.keyCode === 68) { + toggleAudioMute(); + return false; + } + if (event.metaKey && event.keyCode === 69) { + toggleVideoMute(); + return false; + } + } else { + if (event.ctrlKey && event.keyCode === 68) { + toggleAudioMute(); + return false; + } + if (event.ctrlKey && event.keyCode === 69) { + toggleVideoMute(); + return false; + } + } + } + + // 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(){ + 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"; + };