Adding webrtc-sample demos under trunk/samples.

Review URL: https://webrtc-codereview.appspot.com/1126005

git-svn-id: http://webrtc.googlecode.com/svn/trunk@3578 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
vikasmarwaha@webrtc.org
2013-02-27 23:22:10 +00:00
parent 132c15de30
commit 98fce15c6f
30 changed files with 3409 additions and 0 deletions

5
samples/js/apprtc/OWNERS Normal file
View File

@@ -0,0 +1,5 @@
juberti@webrtc.org
braveyao@webrtc.org
hta@webrtc.org
wu@webrtc.org
vikasmarwaha@webrtc.org

View File

@@ -0,0 +1,26 @@
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: /.*
script: apprtc.app
secure: always
inbound_services:
- channel_presence
libraries:
- name: jinja2
version: latest

390
samples/js/apprtc/apprtc.py Normal file
View File

@@ -0,0 +1,390 @@
#!/usr/bin/python2.4
#
# Copyright 2011 Google Inc. All Rights Reserved.
# pylint: disable-msg=C6310
"""WebRTC Demo
This module demonstrates the WebRTC API by implementing a simple video chat app.
"""
import cgi
import datetime
import logging
import os
import random
import re
import json
import jinja2
import webapp2
import threading
from google.appengine.api import channel
from google.appengine.ext import db
jinja_environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))
# Lock for syncing DB operation in concurrent requests handling.
# TODO(brave): keeping working on improving performance with thread syncing.
# One possible method for near future is to reduce the message caching.
LOCK = threading.RLock()
def generate_random(len):
word = ''
for i in range(len):
word += random.choice('0123456789')
return word
def sanitize(key):
return re.sub('[^a-zA-Z0-9\-]', '-', key)
def make_client_id(room, user):
return room.key().id_or_name() + '/' + user
def make_pc_config(stun_server, turn_server, ts_pwd):
servers = []
if turn_server:
turn_config = 'turn:{}'.format(turn_server)
servers.append({'url':turn_config, 'credential':ts_pwd})
if stun_server:
stun_config = 'stun:{}'.format(stun_server)
else:
stun_config = 'stun:' + 'stun.l.google.com:19302'
servers.append({'url':stun_config})
return {'iceServers':servers}
def create_channel(room, user, duration_minutes):
client_id = make_client_id(room, user)
return channel.create_channel(client_id, duration_minutes)
def make_loopback_answer(message):
message = message.replace("\"offer\"", "\"answer\"")
message = message.replace("a=ice-options:google-ice\\r\\n", "")
return message
def maybe_add_fake_crypto(message):
if message.find("a=crypto") == -1:
index = len(message)
crypto_line = "a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\\r\\n"
# reverse find for multiple find and insert operations.
index = message.rfind("c=IN", 0, index)
while (index != -1):
message = message[:index] + crypto_line + message[index:]
index = message.rfind("c=IN", 0, index)
return message
def handle_message(room, user, message):
message_obj = json.loads(message)
other_user = room.get_other_user(user)
room_key = room.key().id_or_name();
if message_obj['type'] == 'bye':
# This would remove the other_user in loopback test too.
# So check its availability before forwarding Bye message.
room.remove_user(user)
logging.info('User ' + user + ' quit from room ' + room_key)
logging.info('Room ' + room_key + ' has state ' + str(room))
if other_user and room.has_user(other_user):
if message_obj['type'] == 'offer':
# Special case the loopback scenario.
if other_user == user:
message = make_loopback_answer(message)
# Workaround Chrome bug.
# Insert a=crypto line into offer from FireFox.
# TODO(juberti): Remove this call.
message = maybe_add_fake_crypto(message)
on_message(room, other_user, message)
def get_saved_messages(client_id):
return Message.gql("WHERE client_id = :id", id=client_id)
def delete_saved_messages(client_id):
messages = get_saved_messages(client_id)
for message in messages:
message.delete()
logging.info('Deleted the saved message for ' + client_id)
def send_saved_messages(client_id):
messages = get_saved_messages(client_id)
for message in messages:
channel.send_message(client_id, message.msg)
logging.info('Delivered saved message to ' + client_id);
message.delete()
def on_message(room, user, message):
client_id = make_client_id(room, user)
if room.is_connected(user):
channel.send_message(client_id, message)
logging.info('Delivered message to user ' + user);
else:
new_message = Message(client_id = client_id, msg = message)
new_message.put()
logging.info('Saved message for user ' + user)
def make_media_constraints(hd_video):
constraints = { 'optional': [], 'mandatory': {} }
# Demo 16:9 video with media constraints.
if hd_video.lower() == 'true':
# Demo with WHD by setting size with 1280x720.
constraints['mandatory']['minHeight'] = 720
constraints['mandatory']['minWidth'] = 1280
# Disabled for now due to weird stretching behavior on Mac.
#else:
# Demo with WVGA by setting Aspect Ration;
#constraints['mandatory']['maxAspectRatio'] = 1.778
#constraints['mandatory']['minAspectRatio'] = 1.777
return constraints
def make_pc_constraints(compat):
constraints = { 'optional': [] }
# For interop with FireFox. Enable DTLS in peerConnection ctor.
if compat.lower() == 'true':
constraints['optional'].append({'DtlsSrtpKeyAgreement': True})
return constraints
def make_offer_constraints(compat):
constraints = { 'mandatory': {}, 'optional': [] }
# For interop with FireFox. Disable Data Channel in createOffer.
if compat.lower() == 'true':
constraints['mandatory']['MozDontOfferDataChannel'] = True
return constraints
def append_url_arguments(request, link):
for argument in request.arguments():
if argument != 'r':
link += ('&' + cgi.escape(argument, True) + '=' +
cgi.escape(request.get(argument), True))
return link
# This database is to store the messages from the sender client when the
# receiver client is not ready to receive the messages.
# Use TextProperty instead of StringProperty for msg because
# the session description can be more than 500 characters.
class Message(db.Model):
client_id = db.StringProperty()
msg = db.TextProperty()
class Room(db.Model):
"""All the data we store for a room"""
user1 = db.StringProperty()
user2 = db.StringProperty()
user1_connected = db.BooleanProperty(default=False)
user2_connected = db.BooleanProperty(default=False)
def __str__(self):
str = '['
if self.user1:
str += "%s-%r" % (self.user1, self.user1_connected)
if self.user2:
str += ", %s-%r" % (self.user2, self.user2_connected)
str += ']'
return str
def get_occupancy(self):
occupancy = 0
if self.user1:
occupancy += 1
if self.user2:
occupancy += 1
return occupancy
def get_other_user(self, user):
if user == self.user1:
return self.user2
elif user == self.user2:
return self.user1
else:
return None
def has_user(self, user):
return (user and (user == self.user1 or user == self.user2))
def add_user(self, user):
if not self.user1:
self.user1 = user
elif not self.user2:
self.user2 = user
else:
raise RuntimeError('room is full')
self.put()
def remove_user(self, user):
delete_saved_messages(make_client_id(self, user))
if user == self.user2:
self.user2 = None
self.user2_connected = False
if user == self.user1:
if self.user2:
self.user1 = self.user2
self.user1_connected = self.user2_connected
self.user2 = None
self.user2_connected = False
else:
self.user1 = None
self.user1_connected = False
if self.get_occupancy() > 0:
self.put()
else:
self.delete()
def set_connected(self, user):
if user == self.user1:
self.user1_connected = True
if user == self.user2:
self.user2_connected = True
self.put()
def is_connected(self, user):
if user == self.user1:
return self.user1_connected
if user == self.user2:
return self.user2_connected
class ConnectPage(webapp2.RequestHandler):
def post(self):
key = self.request.get('from')
room_key, user = key.split('/')
with LOCK:
room = Room.get_by_key_name(room_key)
# Check if room has user in case that disconnect message comes before
# connect message with unknown reason, observed with local AppEngine SDK.
if room and room.has_user(user):
room.set_connected(user)
send_saved_messages(make_client_id(room, user))
logging.info('User ' + user + ' connected to room ' + room_key)
logging.info('Room ' + room_key + ' has state ' + str(room))
else:
logging.warning('Unexpected Connect Message to room ' + room_key)
class DisconnectPage(webapp2.RequestHandler):
def post(self):
key = self.request.get('from')
room_key, user = key.split('/')
with LOCK:
room = Room.get_by_key_name(room_key)
if room and room.has_user(user):
other_user = room.get_other_user(user)
room.remove_user(user)
logging.info('User ' + user + ' removed from room ' + room_key)
logging.info('Room ' + room_key + ' has state ' + str(room))
if other_user and other_user != user:
channel.send_message(make_client_id(room, other_user), '{"type":"bye"}')
logging.info('Sent BYE to ' + other_user)
logging.warning('User ' + user + ' disconnected from room ' + room_key)
class MessagePage(webapp2.RequestHandler):
def post(self):
message = self.request.body
room_key = self.request.get('r')
user = self.request.get('u')
with LOCK:
room = Room.get_by_key_name(room_key)
if room:
handle_message(room, user, message)
else:
logging.warning('Unknown room ' + room_key)
class MainPage(webapp2.RequestHandler):
"""The main UI page, renders the 'index.html' template."""
def get(self):
"""Renders the main page. When this page is shown, we create a new
channel to push asynchronous updates to the client."""
# get the base url without arguments.
base_url = self.request.path_url
room_key = sanitize(self.request.get('r'))
debug = self.request.get('debug')
unittest = self.request.get('unittest')
stun_server = self.request.get('ss')
turn_server = self.request.get('ts')
hd_video = self.request.get('hd')
ts_pwd = self.request.get('tp')
# set compat to true by default.
compat = 'true'
if self.request.get('compat'):
compat = self.request.get('compat')
if debug == 'loopback':
# set compat to false as DTLS does not work for loopback.
compat = 'false'
# token_timeout for channel creation, default 30min, max 2 days, min 3min.
token_timeout = self.request.get_range('tt',
min_value = 3,
max_value = 3000,
default = 30)
if unittest:
# Always create a new room for the unit tests.
room_key = generate_random(8)
if not room_key:
room_key = generate_random(8)
redirect = '/?r=' + room_key
redirect = append_url_arguments(self.request, redirect)
self.redirect(redirect)
logging.info('Redirecting visitor to base URL to ' + redirect)
return
user = None
initiator = 0
with LOCK:
room = Room.get_by_key_name(room_key)
if not room and debug != "full":
# New room.
user = generate_random(8)
room = Room(key_name = room_key)
room.add_user(user)
if debug != 'loopback':
initiator = 0
else:
room.add_user(user)
initiator = 1
elif room and room.get_occupancy() == 1 and debug != 'full':
# 1 occupant.
user = generate_random(8)
room.add_user(user)
initiator = 1
else:
# 2 occupants (full).
template = jinja_environment.get_template('full.html')
self.response.out.write(template.render({ 'room_key': room_key }))
logging.info('Room ' + room_key + ' is full')
return
room_link = base_url + '/?r=' + room_key
room_link = append_url_arguments(self.request, room_link)
token = create_channel(room, user, token_timeout)
pc_config = make_pc_config(stun_server, turn_server, ts_pwd)
pc_constraints = make_pc_constraints(compat)
offer_constraints = make_offer_constraints(compat)
media_constraints = make_media_constraints(hd_video)
template_values = {'token': token,
'me': user,
'room_key': room_key,
'room_link': room_link,
'initiator': initiator,
'pc_config': json.dumps(pc_config),
'pc_constraints': json.dumps(pc_constraints),
'offer_constraints': json.dumps(offer_constraints),
'media_constraints': json.dumps(media_constraints)
}
if unittest:
target_page = 'test/test_' + unittest + '.html'
else:
target_page = 'index.html'
template = jinja_environment.get_template(target_page)
self.response.out.write(template.render(template_values))
logging.info('User ' + user + ' added to room ' + room_key)
logging.info('Room ' + room_key + ' has state ' + str(room))
app = webapp2.WSGIApplication([
('/', MainPage),
('/message', MessagePage),
('/_ah/channel/connected/', ConnectPage),
('/_ah/channel/disconnected/', DisconnectPage)
], debug=True)

View File

@@ -0,0 +1,54 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<script src="/_ah/channel/jsapi"></script>
<style type="text/css">
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 {
position: relative;
min-height: 100%;
width: 100%;
margin: 0px auto;
}
#footer {
spacing: 4px;
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;
}
#logo {
display: block;
top:4;
right:4;
position:absolute;
float:right;
#opacity: 0.8;
}
</style>
</head>
<body>
<div id="container">
<div id="footer">
Sorry, this room is full. <a href="{{room_link}}">Click here</a> to try again.
</div>
</div>
<img id="logo" alt="WebRTC" src="images/webrtc_black_20p.png">
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta content="text/html; charset=ISO-8859-1"
http-equiv="content-type">
<title>WebRtc Demo App Help</title>
</head>
<body>
TODO
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,561 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>WebRTC Reference App</title>
<link rel="canonical" href="{{ room_link }}"/>
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<script src="/_ah/channel/jsapi"></script>
<!-- Load the polyfill to switch-hit between Chrome and Firefox -->
<script src="../base/adapter.js"></script>
<style type="text/css">
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: relative;
min-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 {
opacity: 0;
-webkit-transition-property: opacity;
-webkit-transition-duration: 2s;
}
#remoteVideo {
opacity: 0;
-webkit-transition-property: opacity;
-webkit-transition-duration: 2s;
}
#miniVideo {
opacity: 0;
-webkit-transition-property: opacity;
-webkit-transition-duration: 2s;
}
#footer {
spacing: 4px;
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;
}
</style>
</head>
<body>
<script type="text/javascript">
var localVideo;
var miniVideo;
var remoteVideo;
var localStream;
var remoteStream;
var channel;
var channelReady = false;
var pc;
var socket;
var initiator = {{ initiator }};
var started = 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;
function initialize() {
console.log("Initializing; room={{ room_key }}.");
card = document.getElementById("card");
localVideo = document.getElementById("localVideo");
miniVideo = document.getElementById("miniVideo");
remoteVideo = document.getElementById("remoteVideo");
resetStatus();
openChannel('{{ token }}');
doGetUserMedia();
}
function openChannel(channelToken) {
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 resetStatus() {
if (!initiator) {
setStatus("Waiting for someone to join: <a href=\"{{ room_link }}\">{{ room_link }}</a>");
} else {
setStatus("Initializing...");
}
}
function doGetUserMedia() {
// Call into getUserMedia via the polyfill (adapter.js).
var constraints = {{ media_constraints|safe }};
try {
getUserMedia({'audio':true, 'video':constraints}, onUserMediaSuccess,
onUserMediaError);
console.log("Requested access to local media with mediaConstraints:\n" +
" \"" + JSON.stringify(constraints) + "\"");
} catch (e) {
alert("getUserMedia() failed. Is this a WebRTC capable browser?");
console.log("getUserMedia failed with exception: " + e.message);
}
}
function createPeerConnection() {
var pc_config = {{ pc_config|safe }};
var pc_constraints = {{ pc_constraints|safe }};
// Force the use of a number IP STUN server for Firefox.
if (webrtcDetectedBrowser == "firefox") {
pc_config = {"iceServers":[{"url":"stun:23.21.150.121"}]};
}
try {
// Create an RTCPeerConnection via the polyfill (adapter.js).
pc = new RTCPeerConnection(pc_config, pc_constraints);
pc.onicecandidate = onIceCandidate;
console.log("Created RTCPeerConnnection with:\n" +
" config: \"" + JSON.stringify(pc_config) + "\";\n" +
" constraints: \"" + JSON.stringify(pc_constraints) + "\".");
} 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) {
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();
}
}
function setStatus(state) {
footer.innerHTML = state;
}
function doCall() {
var constraints = {{ offer_constraints | safe }};
// temporary measure to remove Moz* constraints in Chrome
if (webrtcDetectedBrowser === "chrome") {
for (prop in constraints.mandatory) {
if (prop.indexOf("Moz") != -1) {
delete constraints.mandatory[prop];
}
}
}
constraints = mergeConstraints(constraints, 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);
path = '/message?r={{ room_key }}' + '&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();
pc.setRemoteDescription(new RTCSessionDescription(msg));
doAnswer();
} else if (msg.type === 'answer' && started) {
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;
if (initiator) maybeStart();
}
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.');
transitionToWaiting();
stop();
initiator = 0;
}
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);
setStatus("<input type=\"button\" id=\"hangup\" value=\"Hang up\" onclick=\"onHangup()\" />");
}
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. <a href=\"{{ room_link }}\">Click here</a> 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;
}
setTimeout(initialize, 1);
// Send BYE on refreshing(or leaving) a demo page
// to ensure the room is cleaned for next session.
window.onbeforeunload = function() {
sendMessage({type: 'bye'});
}
// 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;
}
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;
}
</script>
<div id="container" ondblclick="enterFullScreen()">
<div id="card">
<div id="local">
<video width="100%" height="100%" id="localVideo" autoplay="autoplay" muted="true"/>
</div>
<div id="remote">
<video width="100%" height="100%" id="remoteVideo" autoplay="autoplay">
</video>
<div id="mini">
<video width="100%" height="100%" id="miniVideo" autoplay="autoplay" muted="true"/>
</div>
</div>
</div>
<div id="footer">
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,93 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!--This is the test page for the message channel.
To run this test:
?debug=loopback&unittest=channel
-->
<html>
<head>
<link rel="canonical" href="{{ room_link }}"/>
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<script src="/_ah/channel/jsapi"></script>
<script type="text/javascript">
var channel;
var pc;
var socket;
var expected_message_num = 8;
var receive = 0;
var test_msg =
'01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
var msg_larger_than_500 = "";
function trace(txt) {
// This function is used for logging.
var elem = document.getElementById("debug");
elem.innerHTML += txt + "<br>";
}
function runTest() {
trace("Initializing; room={{ room_key }}.");
var channel = new goog.appengine.Channel('{{ token }}');
var handler = {
'onopen': onChannelOpened,
'onmessage': onChannelMessage,
'onerror': onChannelError,
'onclose': onChannelClosed
};
for (i = 0; i < 9; ++i) {
msg_larger_than_500 += test_msg;
}
for (i = 0; i < 4; ++i) {
sendMessage({type: 'test', msgid: i, msg: msg_larger_than_500});
}
trace('channel.open');
socket = channel.open(handler);
for (i = 4; i < expected_message_num; ++i) {
sendMessage({type: 'test', msgid: i, msg: msg_larger_than_500});
}
}
function sendMessage(message) {
var msgString = JSON.stringify(message);
trace('C->S: ' + msgString);
path = '/message?r={{ room_key }}' + '&u={{ me }}';
var xhr = new XMLHttpRequest();
xhr.open('POST', path, true);
xhr.send(msgString);
}
function onChannelOpened() {
trace('Channel opened.');
}
function onChannelMessage(message) {
if (message.data != JSON.stringify({type: 'test', msgid: receive,
msg: msg_larger_than_500})) {
trace('ERROR: Expect: ' + receive + ' Actual: ' + message.data);
} else {
trace('S->C: ' + message.data);
}
++receive;
if (receive == expected_message_num) {
trace('Received all the ' + expected_message_num + ' messages.');
trace('Test passed!');
} else if (receive > expected_message_num) {
trace('Received more than expected message');
trace('Test failed!');
}
}
function onChannelError() {
trace('Channel error.');
}
function onChannelClosed() {
trace('Channel closed.');
}
</script>
</head>
<body onload="runTest()">
<pre id="debug"></pre>
</body>
</html>