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:
5
samples/js/apprtc/OWNERS
Normal file
5
samples/js/apprtc/OWNERS
Normal file
@@ -0,0 +1,5 @@
|
||||
juberti@webrtc.org
|
||||
braveyao@webrtc.org
|
||||
hta@webrtc.org
|
||||
wu@webrtc.org
|
||||
vikasmarwaha@webrtc.org
|
26
samples/js/apprtc/app.yaml
Normal file
26
samples/js/apprtc/app.yaml
Normal 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
390
samples/js/apprtc/apprtc.py
Normal 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)
|
54
samples/js/apprtc/full.html
Normal file
54
samples/js/apprtc/full.html
Normal 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>
|
11
samples/js/apprtc/html/help.html
Normal file
11
samples/js/apprtc/html/help.html
Normal 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>
|
BIN
samples/js/apprtc/images/webrtc_black_20p.png
Normal file
BIN
samples/js/apprtc/images/webrtc_black_20p.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
561
samples/js/apprtc/index.html
Normal file
561
samples/js/apprtc/index.html
Normal 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>
|
93
samples/js/apprtc/test/test_channel.html
Normal file
93
samples/js/apprtc/test/test_channel.html
Normal 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>
|
Reference in New Issue
Block a user