blob: 1d99fe6fcf1d79f5fcb1d68cc4cb9e00181e3f76 [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
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000121def make_media_constraints(audio, video, min_resolution, max_resolution):
122 if not audio or audio.lower() == 'true':
123 audio_constraints = True
124 elif audio.lower() == 'false':
125 audio_constraints = False
126 else:
127 audio_constraints = { 'mandatory': {}, 'optional': [] }
128 if audio:
129 for constraint in audio.split(','):
130 # TODO(ajm): We should probably be using the optional list here, but
131 # Chrome M31 won't override its default settings unless the constraints
132 # are mandatory. Chrome M32+, however, won't override optional settings.
133 if constraint.startswith('-'):
134 audio_constraints['mandatory'][constraint[1:]] = False
135 else:
136 audio_constraints['mandatory'][constraint] = True
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000137
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000138 if not video or video.lower() == 'true':
139 video_constraints = True
140 elif video.lower() == 'false':
141 video_constraints = False
142 else:
143 video_constraints = { 'mandatory': {}, 'optional': [] }
144 if min_resolution:
145 min_sizes = min_resolution.split('x')
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000146 if len(min_sizes) == 2:
147 video_constraints['mandatory']['minWidth'] = min_sizes[0]
148 video_constraints['mandatory']['minHeight'] = min_sizes[1]
149 else:
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000150 logging.info('Ignored invalid minre: ' + min_resolution)
151 if max_resolution:
152 max_sizes = max_resolution.split('x')
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000153 if len(max_sizes) == 2:
154 video_constraints['mandatory']['maxWidth'] = max_sizes[0]
155 video_constraints['mandatory']['maxHeight'] = max_sizes[1]
156 else:
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000157 logging.info('Ignored invalid maxre: ' + max_resolution)
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000158
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000159 return { 'audio': audio_constraints, 'video': video_constraints }
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000160
161def make_pc_constraints(compat):
162 constraints = { 'optional': [] }
163 # For interop with FireFox. Enable DTLS in peerConnection ctor.
164 if compat.lower() == 'true':
165 constraints['optional'].append({'DtlsSrtpKeyAgreement': True})
vikasmarwaha@webrtc.orgcee0dfb2013-09-20 21:26:07 +0000166 # Disable DTLS in peerConnection ctor for loopback call. The value
167 # of compat is false for loopback mode.
168 else:
169 constraints['optional'].append({'DtlsSrtpKeyAgreement': False})
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000170 return constraints
171
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000172def make_offer_constraints():
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000173 constraints = { 'mandatory': {}, 'optional': [] }
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000174 return constraints
175
176def append_url_arguments(request, link):
177 for argument in request.arguments():
178 if argument != 'r':
179 link += ('&' + cgi.escape(argument, True) + '=' +
180 cgi.escape(request.get(argument), True))
181 return link
182
183# This database is to store the messages from the sender client when the
184# receiver client is not ready to receive the messages.
185# Use TextProperty instead of StringProperty for msg because
186# the session description can be more than 500 characters.
187class Message(db.Model):
188 client_id = db.StringProperty()
189 msg = db.TextProperty()
190
191class Room(db.Model):
192 """All the data we store for a room"""
193 user1 = db.StringProperty()
194 user2 = db.StringProperty()
195 user1_connected = db.BooleanProperty(default=False)
196 user2_connected = db.BooleanProperty(default=False)
197
198 def __str__(self):
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000199 result = '['
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000200 if self.user1:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000201 result += "%s-%r" % (self.user1, self.user1_connected)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000202 if self.user2:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000203 result += ", %s-%r" % (self.user2, self.user2_connected)
204 result += ']'
205 return result
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000206
207 def get_occupancy(self):
208 occupancy = 0
209 if self.user1:
210 occupancy += 1
211 if self.user2:
212 occupancy += 1
213 return occupancy
214
215 def get_other_user(self, user):
216 if user == self.user1:
217 return self.user2
218 elif user == self.user2:
219 return self.user1
220 else:
221 return None
222
223 def has_user(self, user):
224 return (user and (user == self.user1 or user == self.user2))
225
226 def add_user(self, user):
227 if not self.user1:
228 self.user1 = user
229 elif not self.user2:
230 self.user2 = user
231 else:
232 raise RuntimeError('room is full')
233 self.put()
234
235 def remove_user(self, user):
236 delete_saved_messages(make_client_id(self, user))
237 if user == self.user2:
238 self.user2 = None
239 self.user2_connected = False
240 if user == self.user1:
241 if self.user2:
242 self.user1 = self.user2
243 self.user1_connected = self.user2_connected
244 self.user2 = None
245 self.user2_connected = False
246 else:
247 self.user1 = None
248 self.user1_connected = False
249 if self.get_occupancy() > 0:
250 self.put()
251 else:
252 self.delete()
253
254 def set_connected(self, user):
255 if user == self.user1:
256 self.user1_connected = True
257 if user == self.user2:
258 self.user2_connected = True
259 self.put()
260
261 def is_connected(self, user):
262 if user == self.user1:
263 return self.user1_connected
264 if user == self.user2:
265 return self.user2_connected
266
267class ConnectPage(webapp2.RequestHandler):
268 def post(self):
269 key = self.request.get('from')
270 room_key, user = key.split('/')
271 with LOCK:
272 room = Room.get_by_key_name(room_key)
273 # Check if room has user in case that disconnect message comes before
274 # connect message with unknown reason, observed with local AppEngine SDK.
275 if room and room.has_user(user):
276 room.set_connected(user)
277 send_saved_messages(make_client_id(room, user))
278 logging.info('User ' + user + ' connected to room ' + room_key)
279 logging.info('Room ' + room_key + ' has state ' + str(room))
280 else:
281 logging.warning('Unexpected Connect Message to room ' + room_key)
282
283
284class DisconnectPage(webapp2.RequestHandler):
285 def post(self):
286 key = self.request.get('from')
287 room_key, user = key.split('/')
288 with LOCK:
289 room = Room.get_by_key_name(room_key)
290 if room and room.has_user(user):
291 other_user = room.get_other_user(user)
292 room.remove_user(user)
293 logging.info('User ' + user + ' removed from room ' + room_key)
294 logging.info('Room ' + room_key + ' has state ' + str(room))
295 if other_user and other_user != user:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000296 channel.send_message(make_client_id(room, other_user),
297 '{"type":"bye"}')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000298 logging.info('Sent BYE to ' + other_user)
299 logging.warning('User ' + user + ' disconnected from room ' + room_key)
300
301
302class MessagePage(webapp2.RequestHandler):
303 def post(self):
304 message = self.request.body
305 room_key = self.request.get('r')
306 user = self.request.get('u')
307 with LOCK:
308 room = Room.get_by_key_name(room_key)
309 if room:
310 handle_message(room, user, message)
311 else:
312 logging.warning('Unknown room ' + room_key)
313
314class MainPage(webapp2.RequestHandler):
315 """The main UI page, renders the 'index.html' template."""
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000316 def get(self):
317 """Renders the main page. When this page is shown, we create a new
318 channel to push asynchronous updates to the client."""
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000319 # Get the base url without arguments.
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000320 base_url = self.request.path_url
vikasmarwaha@webrtc.org6e7c2032013-08-05 22:05:20 +0000321 user_agent = self.request.headers['User-Agent']
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000322 room_key = sanitize(self.request.get('r'))
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000323 stun_server = self.request.get('ss')
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000324 if not stun_server:
325 stun_server = get_default_stun_server(user_agent)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000326 turn_server = self.request.get('ts')
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000327
328 ts_pwd = self.request.get('tp')
329
330 # Use "audio" and "video" to set the media stream constraints. "true" and
331 # "false" are recognized and interpreted as bools, for example:
332 # "?audio=true&video=false" (start an audio-only call).
333 # "?audio=false" (start a video-only call)
334 # If unspecified, the constraint defaults to True.
335 #
336 # audio-specific parsing:
337 # To set certain constraints, pass in a comma-separated list of audio
338 # constraint strings. If preceded by a "-", the constraint will be set to
339 # False, and otherwise to True. There is no validation of constraint
340 # strings. Examples:
341 # "?audio=googEchoCancellation" (enables echo cancellation)
342 # "?audio=-googEchoCancellation,googAutoGainControl" (disables echo
343 # cancellation and enables gain control)
344 # The strings are defined here:
345 # https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/localaudiosource.cc
346 #
347 # TODO(ajm): There is currently no video functionality beyond True/False.
348 # Move the resolution settings here instead?
349 audio = self.request.get('audio')
350 video = self.request.get('video')
351 min_resolution = self.request.get('minre')
352 max_resolution = self.request.get('maxre')
353 hd_video = self.request.get('hd')
354 if hd_video.lower() == 'true':
355 min_resolution = '1280x720'
356
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000357 audio_send_codec = self.request.get('asc')
358 if not audio_send_codec:
359 audio_send_codec = get_preferred_audio_send_codec(user_agent)
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000360
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000361 audio_receive_codec = self.request.get('arc')
362 if not audio_receive_codec:
363 audio_receive_codec = get_preferred_audio_receive_codec()
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000364
365 # Set stereo to false by default.
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000366 stereo = 'false'
367 if self.request.get('stereo'):
368 stereo = self.request.get('stereo')
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000369
370 # Set compat to true by default.
371 compat = 'true'
372 if self.request.get('compat'):
373 compat = self.request.get('compat')
374
375 debug = self.request.get('debug')
376 if debug == 'loopback':
377 # Set compat to false as DTLS does not work for loopback.
378 compat = 'false'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000379
380 # token_timeout for channel creation, default 30min, max 2 days, min 3min.
381 token_timeout = self.request.get_range('tt',
382 min_value = 3,
383 max_value = 3000,
384 default = 30)
385
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000386 unittest = self.request.get('unittest')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000387 if unittest:
388 # Always create a new room for the unit tests.
389 room_key = generate_random(8)
390
391 if not room_key:
392 room_key = generate_random(8)
393 redirect = '/?r=' + room_key
394 redirect = append_url_arguments(self.request, redirect)
395 self.redirect(redirect)
396 logging.info('Redirecting visitor to base URL to ' + redirect)
397 return
398
399 user = None
400 initiator = 0
401 with LOCK:
402 room = Room.get_by_key_name(room_key)
403 if not room and debug != "full":
404 # New room.
405 user = generate_random(8)
406 room = Room(key_name = room_key)
407 room.add_user(user)
408 if debug != 'loopback':
409 initiator = 0
410 else:
411 room.add_user(user)
412 initiator = 1
413 elif room and room.get_occupancy() == 1 and debug != 'full':
414 # 1 occupant.
415 user = generate_random(8)
416 room.add_user(user)
417 initiator = 1
418 else:
419 # 2 occupants (full).
420 template = jinja_environment.get_template('full.html')
421 self.response.out.write(template.render({ 'room_key': room_key }))
422 logging.info('Room ' + room_key + ' is full')
423 return
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000424
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000425 room_link = base_url + '?r=' + room_key
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000426 room_link = append_url_arguments(self.request, room_link)
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000427 turn_url = 'https://computeengineondemand.appspot.com/'
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000428 turn_url = turn_url + 'turn?' + 'username=' + user + '&key=4080218913'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000429 token = create_channel(room, user, token_timeout)
430 pc_config = make_pc_config(stun_server, turn_server, ts_pwd)
431 pc_constraints = make_pc_constraints(compat)
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000432 offer_constraints = make_offer_constraints()
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000433 media_constraints = make_media_constraints(audio, video, min_resolution,
434 max_resolution)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000435 template_values = {'token': token,
436 'me': user,
437 'room_key': room_key,
438 'room_link': room_link,
439 'initiator': initiator,
440 'pc_config': json.dumps(pc_config),
441 'pc_constraints': json.dumps(pc_constraints),
442 'offer_constraints': json.dumps(offer_constraints),
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000443 'media_constraints': json.dumps(media_constraints),
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000444 'turn_url': turn_url,
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000445 'stereo': stereo,
446 'audio_send_codec': audio_send_codec,
447 'audio_receive_codec': audio_receive_codec
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000448 }
449 if unittest:
450 target_page = 'test/test_' + unittest + '.html'
451 else:
452 target_page = 'index.html'
453
454 template = jinja_environment.get_template(target_page)
455 self.response.out.write(template.render(template_values))
456 logging.info('User ' + user + ' added to room ' + room_key)
457 logging.info('Room ' + room_key + ' has state ' + str(room))
458
459
460app = webapp2.WSGIApplication([
461 ('/', MainPage),
462 ('/message', MessagePage),
463 ('/_ah/channel/connected/', ConnectPage),
464 ('/_ah/channel/disconnected/', DisconnectPage)
465 ], debug=True)