blob: b5a8fcfa75b94860643eac14a0bfb9763d3574e5 [file] [log] [blame]
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +00001#!/usr/bin/python2.4
2#
3# Copyright 2011 Google Inc. All Rights Reserved.
4
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +00005"""WebRTC Demo
6
7This module demonstrates the WebRTC API by implementing a simple video chat app.
8"""
9
10import cgi
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000011import logging
12import os
13import random
14import re
15import json
16import jinja2
17import webapp2
18import threading
19from google.appengine.api import channel
20from google.appengine.ext import db
21
22jinja_environment = jinja2.Environment(
23 loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))
24
25# Lock for syncing DB operation in concurrent requests handling.
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000026# TODO(brave): keeping working on improving performance with thread syncing.
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000027# One possible method for near future is to reduce the message caching.
28LOCK = threading.RLock()
29
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000030def generate_random(length):
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000031 word = ''
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000032 for _ in range(length):
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000033 word += random.choice('0123456789')
34 return word
35
36def sanitize(key):
37 return re.sub('[^a-zA-Z0-9\-]', '-', key)
38
39def make_client_id(room, user):
40 return room.key().id_or_name() + '/' + user
41
vikasmarwaha@webrtc.org6e7c2032013-08-05 22:05:20 +000042def get_default_stun_server(user_agent):
43 default_stun_server = 'stun.l.google.com:19302'
44 if 'Firefox' in user_agent:
45 default_stun_server = 'stun.services.mozilla.com'
46 return default_stun_server
47
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +000048def get_preferred_audio_receive_codec():
49 return 'opus/48000'
50
51def get_preferred_audio_send_codec(user_agent):
52 # Empty string means no preference.
53 preferred_audio_send_codec = ''
54 # Prefer to send ISAC on Chrome for Android.
55 if 'Android' in user_agent and 'Chrome' in user_agent:
56 preferred_audio_send_codec = 'ISAC/16000'
57 return preferred_audio_send_codec
58
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000059def make_pc_config(stun_server, turn_server, ts_pwd):
60 servers = []
61 if turn_server:
62 turn_config = 'turn:{}'.format(turn_server)
63 servers.append({'url':turn_config, 'credential':ts_pwd})
64 if stun_server:
65 stun_config = 'stun:{}'.format(stun_server)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000066 servers.append({'url':stun_config})
67 return {'iceServers':servers}
68
69def create_channel(room, user, duration_minutes):
70 client_id = make_client_id(room, user)
71 return channel.create_channel(client_id, duration_minutes)
72
73def make_loopback_answer(message):
74 message = message.replace("\"offer\"", "\"answer\"")
75 message = message.replace("a=ice-options:google-ice\\r\\n", "")
76 return message
77
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000078def handle_message(room, user, message):
79 message_obj = json.loads(message)
80 other_user = room.get_other_user(user)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000081 room_key = room.key().id_or_name()
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000082 if message_obj['type'] == 'bye':
83 # This would remove the other_user in loopback test too.
84 # So check its availability before forwarding Bye message.
85 room.remove_user(user)
86 logging.info('User ' + user + ' quit from room ' + room_key)
87 logging.info('Room ' + room_key + ' has state ' + str(room))
88 if other_user and room.has_user(other_user):
89 if message_obj['type'] == 'offer':
90 # Special case the loopback scenario.
91 if other_user == user:
92 message = make_loopback_answer(message)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000093 on_message(room, other_user, message)
94
95def get_saved_messages(client_id):
96 return Message.gql("WHERE client_id = :id", id=client_id)
97
98def delete_saved_messages(client_id):
99 messages = get_saved_messages(client_id)
100 for message in messages:
101 message.delete()
102 logging.info('Deleted the saved message for ' + client_id)
103
104def send_saved_messages(client_id):
105 messages = get_saved_messages(client_id)
106 for message in messages:
107 channel.send_message(client_id, message.msg)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000108 logging.info('Delivered saved message to ' + client_id)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000109 message.delete()
110
111def on_message(room, user, message):
112 client_id = make_client_id(room, user)
113 if room.is_connected(user):
114 channel.send_message(client_id, message)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000115 logging.info('Delivered message to user ' + user)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000116 else:
117 new_message = Message(client_id = client_id, msg = message)
118 new_message.put()
119 logging.info('Saved message for user ' + user)
120
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000121def make_media_constraints(media, min_re, max_re):
122 video_constraints = { 'optional': [], 'mandatory': {} }
123 media_constraints = { 'video':video_constraints, 'audio':True }
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000124
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000125 # Media: audio:audio only; video:video only; (default):both.
126 if media.lower() == 'audio':
127 media_constraints['video'] = False
128 elif media.lower() == 'video':
129 media_constraints['audio'] = False
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000130
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000131 if media.lower() != 'audio' :
132 if min_re:
133 min_sizes = min_re.split('x')
134 if len(min_sizes) == 2:
135 video_constraints['mandatory']['minWidth'] = min_sizes[0]
136 video_constraints['mandatory']['minHeight'] = min_sizes[1]
137 else:
pbos@webrtc.orgb4a06232013-04-08 15:59:24 +0000138 logging.info('Ignored invalid min_re: ' + min_re)
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000139 if max_re:
140 max_sizes = max_re.split('x')
141 if len(max_sizes) == 2:
142 video_constraints['mandatory']['maxWidth'] = max_sizes[0]
143 video_constraints['mandatory']['maxHeight'] = max_sizes[1]
144 else:
pbos@webrtc.orgb4a06232013-04-08 15:59:24 +0000145 logging.info('Ignored invalid max_re: ' + max_re)
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000146 media_constraints['video'] = video_constraints
147
148 return media_constraints
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000149
150def make_pc_constraints(compat):
151 constraints = { 'optional': [] }
152 # For interop with FireFox. Enable DTLS in peerConnection ctor.
153 if compat.lower() == 'true':
154 constraints['optional'].append({'DtlsSrtpKeyAgreement': True})
vikasmarwaha@webrtc.orgcee0dfb2013-09-20 21:26:07 +0000155 # Disable DTLS in peerConnection ctor for loopback call. The value
156 # of compat is false for loopback mode.
157 else:
158 constraints['optional'].append({'DtlsSrtpKeyAgreement': False})
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000159 return constraints
160
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000161def make_offer_constraints():
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000162 constraints = { 'mandatory': {}, 'optional': [] }
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000163 return constraints
164
165def append_url_arguments(request, link):
166 for argument in request.arguments():
167 if argument != 'r':
168 link += ('&' + cgi.escape(argument, True) + '=' +
169 cgi.escape(request.get(argument), True))
170 return link
171
172# This database is to store the messages from the sender client when the
173# receiver client is not ready to receive the messages.
174# Use TextProperty instead of StringProperty for msg because
175# the session description can be more than 500 characters.
176class Message(db.Model):
177 client_id = db.StringProperty()
178 msg = db.TextProperty()
179
180class Room(db.Model):
181 """All the data we store for a room"""
182 user1 = db.StringProperty()
183 user2 = db.StringProperty()
184 user1_connected = db.BooleanProperty(default=False)
185 user2_connected = db.BooleanProperty(default=False)
186
187 def __str__(self):
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000188 result = '['
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000189 if self.user1:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000190 result += "%s-%r" % (self.user1, self.user1_connected)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000191 if self.user2:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000192 result += ", %s-%r" % (self.user2, self.user2_connected)
193 result += ']'
194 return result
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000195
196 def get_occupancy(self):
197 occupancy = 0
198 if self.user1:
199 occupancy += 1
200 if self.user2:
201 occupancy += 1
202 return occupancy
203
204 def get_other_user(self, user):
205 if user == self.user1:
206 return self.user2
207 elif user == self.user2:
208 return self.user1
209 else:
210 return None
211
212 def has_user(self, user):
213 return (user and (user == self.user1 or user == self.user2))
214
215 def add_user(self, user):
216 if not self.user1:
217 self.user1 = user
218 elif not self.user2:
219 self.user2 = user
220 else:
221 raise RuntimeError('room is full')
222 self.put()
223
224 def remove_user(self, user):
225 delete_saved_messages(make_client_id(self, user))
226 if user == self.user2:
227 self.user2 = None
228 self.user2_connected = False
229 if user == self.user1:
230 if self.user2:
231 self.user1 = self.user2
232 self.user1_connected = self.user2_connected
233 self.user2 = None
234 self.user2_connected = False
235 else:
236 self.user1 = None
237 self.user1_connected = False
238 if self.get_occupancy() > 0:
239 self.put()
240 else:
241 self.delete()
242
243 def set_connected(self, user):
244 if user == self.user1:
245 self.user1_connected = True
246 if user == self.user2:
247 self.user2_connected = True
248 self.put()
249
250 def is_connected(self, user):
251 if user == self.user1:
252 return self.user1_connected
253 if user == self.user2:
254 return self.user2_connected
255
256class ConnectPage(webapp2.RequestHandler):
257 def post(self):
258 key = self.request.get('from')
259 room_key, user = key.split('/')
260 with LOCK:
261 room = Room.get_by_key_name(room_key)
262 # Check if room has user in case that disconnect message comes before
263 # connect message with unknown reason, observed with local AppEngine SDK.
264 if room and room.has_user(user):
265 room.set_connected(user)
266 send_saved_messages(make_client_id(room, user))
267 logging.info('User ' + user + ' connected to room ' + room_key)
268 logging.info('Room ' + room_key + ' has state ' + str(room))
269 else:
270 logging.warning('Unexpected Connect Message to room ' + room_key)
271
272
273class DisconnectPage(webapp2.RequestHandler):
274 def post(self):
275 key = self.request.get('from')
276 room_key, user = key.split('/')
277 with LOCK:
278 room = Room.get_by_key_name(room_key)
279 if room and room.has_user(user):
280 other_user = room.get_other_user(user)
281 room.remove_user(user)
282 logging.info('User ' + user + ' removed from room ' + room_key)
283 logging.info('Room ' + room_key + ' has state ' + str(room))
284 if other_user and other_user != user:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000285 channel.send_message(make_client_id(room, other_user),
286 '{"type":"bye"}')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000287 logging.info('Sent BYE to ' + other_user)
288 logging.warning('User ' + user + ' disconnected from room ' + room_key)
289
290
291class MessagePage(webapp2.RequestHandler):
292 def post(self):
293 message = self.request.body
294 room_key = self.request.get('r')
295 user = self.request.get('u')
296 with LOCK:
297 room = Room.get_by_key_name(room_key)
298 if room:
299 handle_message(room, user, message)
300 else:
301 logging.warning('Unknown room ' + room_key)
302
303class MainPage(webapp2.RequestHandler):
304 """The main UI page, renders the 'index.html' template."""
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000305 def get(self):
306 """Renders the main page. When this page is shown, we create a new
307 channel to push asynchronous updates to the client."""
308 # get the base url without arguments.
309 base_url = self.request.path_url
vikasmarwaha@webrtc.org6e7c2032013-08-05 22:05:20 +0000310 user_agent = self.request.headers['User-Agent']
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000311 room_key = sanitize(self.request.get('r'))
312 debug = self.request.get('debug')
313 unittest = self.request.get('unittest')
314 stun_server = self.request.get('ss')
315 turn_server = self.request.get('ts')
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000316 min_re = self.request.get('minre')
317 max_re = self.request.get('maxre')
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000318 audio_send_codec = self.request.get('asc')
319 if not audio_send_codec:
320 audio_send_codec = get_preferred_audio_send_codec(user_agent)
321 audio_receive_codec = self.request.get('arc')
322 if not audio_receive_codec:
323 audio_receive_codec = get_preferred_audio_receive_codec()
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000324 hd_video = self.request.get('hd')
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000325 turn_url = 'https://computeengineondemand.appspot.com/'
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000326 if hd_video.lower() == 'true':
327 min_re = '1280x720'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000328 ts_pwd = self.request.get('tp')
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000329 media = self.request.get('media')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000330 # set compat to true by default.
331 compat = 'true'
332 if self.request.get('compat'):
333 compat = self.request.get('compat')
334 if debug == 'loopback':
335 # set compat to false as DTLS does not work for loopback.
336 compat = 'false'
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000337 # set stereo to false by default
338 stereo = 'false'
339 if self.request.get('stereo'):
340 stereo = self.request.get('stereo')
vikasmarwaha@webrtc.org6e7c2032013-08-05 22:05:20 +0000341 if not stun_server:
342 stun_server = get_default_stun_server(user_agent)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000343
344 # token_timeout for channel creation, default 30min, max 2 days, min 3min.
345 token_timeout = self.request.get_range('tt',
346 min_value = 3,
347 max_value = 3000,
348 default = 30)
349
350 if unittest:
351 # Always create a new room for the unit tests.
352 room_key = generate_random(8)
353
354 if not room_key:
355 room_key = generate_random(8)
356 redirect = '/?r=' + room_key
357 redirect = append_url_arguments(self.request, redirect)
358 self.redirect(redirect)
359 logging.info('Redirecting visitor to base URL to ' + redirect)
360 return
361
362 user = None
363 initiator = 0
364 with LOCK:
365 room = Room.get_by_key_name(room_key)
366 if not room and debug != "full":
367 # New room.
368 user = generate_random(8)
369 room = Room(key_name = room_key)
370 room.add_user(user)
371 if debug != 'loopback':
372 initiator = 0
373 else:
374 room.add_user(user)
375 initiator = 1
376 elif room and room.get_occupancy() == 1 and debug != 'full':
377 # 1 occupant.
378 user = generate_random(8)
379 room.add_user(user)
380 initiator = 1
381 else:
382 # 2 occupants (full).
383 template = jinja_environment.get_template('full.html')
384 self.response.out.write(template.render({ 'room_key': room_key }))
385 logging.info('Room ' + room_key + ' is full')
386 return
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000387
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000388 room_link = base_url + '?r=' + room_key
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000389 room_link = append_url_arguments(self.request, room_link)
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000390 turn_url = turn_url + 'turn?' + 'username=' + user + '&key=4080218913'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000391 token = create_channel(room, user, token_timeout)
392 pc_config = make_pc_config(stun_server, turn_server, ts_pwd)
393 pc_constraints = make_pc_constraints(compat)
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000394 offer_constraints = make_offer_constraints()
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000395 media_constraints = make_media_constraints(media, min_re, max_re)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000396 template_values = {'token': token,
397 'me': user,
398 'room_key': room_key,
399 'room_link': room_link,
400 'initiator': initiator,
401 'pc_config': json.dumps(pc_config),
402 'pc_constraints': json.dumps(pc_constraints),
403 'offer_constraints': json.dumps(offer_constraints),
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000404 'media_constraints': json.dumps(media_constraints),
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000405 'turn_url': turn_url,
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000406 'stereo': stereo,
407 'audio_send_codec': audio_send_codec,
408 'audio_receive_codec': audio_receive_codec
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000409 }
410 if unittest:
411 target_page = 'test/test_' + unittest + '.html'
412 else:
413 target_page = 'index.html'
414
415 template = jinja_environment.get_template(target_page)
416 self.response.out.write(template.render(template_values))
417 logging.info('User ' + user + ' added to room ' + room_key)
418 logging.info('Room ' + room_key + ' has state ' + str(room))
419
420
421app = webapp2.WSGIApplication([
422 ('/', MainPage),
423 ('/message', MessagePage),
424 ('/_ah/channel/connected/', ConnectPage),
425 ('/_ah/channel/disconnected/', DisconnectPage)
426 ], debug=True)