blob: 824e10a706938d774f20ff8204eac3a29155e2d2 [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.org20078e22013-10-05 02:26:50 +0000121def make_media_track_constraints(constraints_string):
122 if not constraints_string or constraints_string.lower() == 'true':
123 track_constraints = True
124 elif constraints_string.lower() == 'false':
125 track_constraints = False
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000126 else:
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000127 track_constraints = {'mandatory': {}, 'optional': []}
128 for constraint_string in constraints_string.split(','):
129 constraint = constraint_string.split('=')
130 if len(constraint) != 2:
131 logging.error('Ignoring malformed constraint: ' + constraint_string)
132 continue
133 if constraint[0].startswith('goog'):
134 track_constraints['optional'].append({constraint[0]: constraint[1]})
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000135 else:
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000136 track_constraints['mandatory'][constraint[0]] = constraint[1]
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000137
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000138 return track_constraints
139
140def make_media_stream_constraints(audio, video):
141 stream_constraints = (
142 {'audio': make_media_track_constraints(audio),
143 'video': make_media_track_constraints(video)})
144 logging.info('Applying media constraints: ' + str(stream_constraints))
145 return stream_constraints
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000146
147def make_pc_constraints(compat):
148 constraints = { 'optional': [] }
149 # For interop with FireFox. Enable DTLS in peerConnection ctor.
150 if compat.lower() == 'true':
151 constraints['optional'].append({'DtlsSrtpKeyAgreement': True})
vikasmarwaha@webrtc.orgcee0dfb2013-09-20 21:26:07 +0000152 # Disable DTLS in peerConnection ctor for loopback call. The value
153 # of compat is false for loopback mode.
154 else:
155 constraints['optional'].append({'DtlsSrtpKeyAgreement': False})
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000156 return constraints
157
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000158def make_offer_constraints():
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000159 constraints = { 'mandatory': {}, 'optional': [] }
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000160 return constraints
161
162def append_url_arguments(request, link):
163 for argument in request.arguments():
164 if argument != 'r':
165 link += ('&' + cgi.escape(argument, True) + '=' +
166 cgi.escape(request.get(argument), True))
167 return link
168
169# This database is to store the messages from the sender client when the
170# receiver client is not ready to receive the messages.
171# Use TextProperty instead of StringProperty for msg because
172# the session description can be more than 500 characters.
173class Message(db.Model):
174 client_id = db.StringProperty()
175 msg = db.TextProperty()
176
177class Room(db.Model):
178 """All the data we store for a room"""
179 user1 = db.StringProperty()
180 user2 = db.StringProperty()
181 user1_connected = db.BooleanProperty(default=False)
182 user2_connected = db.BooleanProperty(default=False)
183
184 def __str__(self):
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000185 result = '['
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000186 if self.user1:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000187 result += "%s-%r" % (self.user1, self.user1_connected)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000188 if self.user2:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000189 result += ", %s-%r" % (self.user2, self.user2_connected)
190 result += ']'
191 return result
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000192
193 def get_occupancy(self):
194 occupancy = 0
195 if self.user1:
196 occupancy += 1
197 if self.user2:
198 occupancy += 1
199 return occupancy
200
201 def get_other_user(self, user):
202 if user == self.user1:
203 return self.user2
204 elif user == self.user2:
205 return self.user1
206 else:
207 return None
208
209 def has_user(self, user):
210 return (user and (user == self.user1 or user == self.user2))
211
212 def add_user(self, user):
213 if not self.user1:
214 self.user1 = user
215 elif not self.user2:
216 self.user2 = user
217 else:
218 raise RuntimeError('room is full')
219 self.put()
220
221 def remove_user(self, user):
222 delete_saved_messages(make_client_id(self, user))
223 if user == self.user2:
224 self.user2 = None
225 self.user2_connected = False
226 if user == self.user1:
227 if self.user2:
228 self.user1 = self.user2
229 self.user1_connected = self.user2_connected
230 self.user2 = None
231 self.user2_connected = False
232 else:
233 self.user1 = None
234 self.user1_connected = False
235 if self.get_occupancy() > 0:
236 self.put()
237 else:
238 self.delete()
239
240 def set_connected(self, user):
241 if user == self.user1:
242 self.user1_connected = True
243 if user == self.user2:
244 self.user2_connected = True
245 self.put()
246
247 def is_connected(self, user):
248 if user == self.user1:
249 return self.user1_connected
250 if user == self.user2:
251 return self.user2_connected
252
253class ConnectPage(webapp2.RequestHandler):
254 def post(self):
255 key = self.request.get('from')
256 room_key, user = key.split('/')
257 with LOCK:
258 room = Room.get_by_key_name(room_key)
259 # Check if room has user in case that disconnect message comes before
260 # connect message with unknown reason, observed with local AppEngine SDK.
261 if room and room.has_user(user):
262 room.set_connected(user)
263 send_saved_messages(make_client_id(room, user))
264 logging.info('User ' + user + ' connected to room ' + room_key)
265 logging.info('Room ' + room_key + ' has state ' + str(room))
266 else:
267 logging.warning('Unexpected Connect Message to room ' + room_key)
268
269
270class DisconnectPage(webapp2.RequestHandler):
271 def post(self):
272 key = self.request.get('from')
273 room_key, user = key.split('/')
274 with LOCK:
275 room = Room.get_by_key_name(room_key)
276 if room and room.has_user(user):
277 other_user = room.get_other_user(user)
278 room.remove_user(user)
279 logging.info('User ' + user + ' removed from room ' + room_key)
280 logging.info('Room ' + room_key + ' has state ' + str(room))
281 if other_user and other_user != user:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000282 channel.send_message(make_client_id(room, other_user),
283 '{"type":"bye"}')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000284 logging.info('Sent BYE to ' + other_user)
285 logging.warning('User ' + user + ' disconnected from room ' + room_key)
286
287
288class MessagePage(webapp2.RequestHandler):
289 def post(self):
290 message = self.request.body
291 room_key = self.request.get('r')
292 user = self.request.get('u')
293 with LOCK:
294 room = Room.get_by_key_name(room_key)
295 if room:
296 handle_message(room, user, message)
297 else:
298 logging.warning('Unknown room ' + room_key)
299
300class MainPage(webapp2.RequestHandler):
301 """The main UI page, renders the 'index.html' template."""
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000302 def get(self):
303 """Renders the main page. When this page is shown, we create a new
304 channel to push asynchronous updates to the client."""
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000305
306 # Append strings to this list to have them thrown up in message boxes. This
307 # will also cause the app to fail.
308 error_messages = []
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000309 # Get the base url without arguments.
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000310 base_url = self.request.path_url
vikasmarwaha@webrtc.org6e7c2032013-08-05 22:05:20 +0000311 user_agent = self.request.headers['User-Agent']
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000312 room_key = sanitize(self.request.get('r'))
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000313 stun_server = self.request.get('ss')
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000314 if not stun_server:
315 stun_server = get_default_stun_server(user_agent)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000316 turn_server = self.request.get('ts')
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000317
318 ts_pwd = self.request.get('tp')
319
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000320 # Use "audio" and "video" to set the media stream constraints. Defined here:
321 # http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamConstraints
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000322 #
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000323 # "true" and "false" are recognized and interpreted as bools, for example:
324 # "?audio=true&video=false" (Start an audio-only call.)
325 # "?audio=false" (Start a video-only call.)
326 # If unspecified, the stream constraint defaults to True.
327 #
328 # To specify media track constraints, pass in a comma-separated list of
329 # key/value pairs, separated by a "=". Examples:
330 # "?audio=googEchoCancellation=false,googAutoGainControl=true"
331 # (Disable echo cancellation and enable gain control.)
332 #
333 # "?video=minWidth=1280,minHeight=720,googNoiseReduction=true"
334 # (Set the minimum resolution to 1280x720 and enable noise reduction.)
335 #
336 # Keys starting with "goog" will be added to the "optional" key; all others
337 # will be added to the "mandatory" key.
338 #
339 # The audio keys are defined here:
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000340 # https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/localaudiosource.cc
341 #
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000342 # The video keys are defined here:
343 # https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/videosource.cc
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000344 audio = self.request.get('audio')
345 video = self.request.get('video')
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000346
347 if self.request.get('hd').lower() == 'true':
348 if video:
349 message = 'The "hd" parameter has overridden video=' + str(video)
350 logging.error(message)
351 error_messages.append(message)
352 video = 'minWidth=1280,minHeight=720'
353
354 if self.request.get('minre') or self.request.get('maxre'):
355 message = ('The "minre" and "maxre" parameters are no longer supported. '
356 'Use "video" instead.')
357 logging.error(message)
358 error_messages.append(message)
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000359
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000360 audio_send_codec = self.request.get('asc')
361 if not audio_send_codec:
362 audio_send_codec = get_preferred_audio_send_codec(user_agent)
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000363
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000364 audio_receive_codec = self.request.get('arc')
365 if not audio_receive_codec:
366 audio_receive_codec = get_preferred_audio_receive_codec()
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000367
368 # Set stereo to false by default.
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000369 stereo = 'false'
370 if self.request.get('stereo'):
371 stereo = self.request.get('stereo')
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000372
373 # Set compat to true by default.
374 compat = 'true'
375 if self.request.get('compat'):
376 compat = self.request.get('compat')
377
378 debug = self.request.get('debug')
379 if debug == 'loopback':
380 # Set compat to false as DTLS does not work for loopback.
381 compat = 'false'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000382
383 # token_timeout for channel creation, default 30min, max 2 days, min 3min.
384 token_timeout = self.request.get_range('tt',
385 min_value = 3,
386 max_value = 3000,
387 default = 30)
388
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000389 unittest = self.request.get('unittest')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000390 if unittest:
391 # Always create a new room for the unit tests.
392 room_key = generate_random(8)
393
394 if not room_key:
395 room_key = generate_random(8)
396 redirect = '/?r=' + room_key
397 redirect = append_url_arguments(self.request, redirect)
398 self.redirect(redirect)
399 logging.info('Redirecting visitor to base URL to ' + redirect)
400 return
401
402 user = None
403 initiator = 0
404 with LOCK:
405 room = Room.get_by_key_name(room_key)
406 if not room and debug != "full":
407 # New room.
408 user = generate_random(8)
409 room = Room(key_name = room_key)
410 room.add_user(user)
411 if debug != 'loopback':
412 initiator = 0
413 else:
414 room.add_user(user)
415 initiator = 1
416 elif room and room.get_occupancy() == 1 and debug != 'full':
417 # 1 occupant.
418 user = generate_random(8)
419 room.add_user(user)
420 initiator = 1
421 else:
422 # 2 occupants (full).
423 template = jinja_environment.get_template('full.html')
424 self.response.out.write(template.render({ 'room_key': room_key }))
425 logging.info('Room ' + room_key + ' is full')
426 return
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000427
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000428 room_link = base_url + '?r=' + room_key
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000429 room_link = append_url_arguments(self.request, room_link)
andrew@webrtc.orgbab2aa52013-10-03 22:37:29 +0000430 turn_url = 'https://computeengineondemand.appspot.com/'
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000431 turn_url = turn_url + 'turn?' + 'username=' + user + '&key=4080218913'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000432 token = create_channel(room, user, token_timeout)
433 pc_config = make_pc_config(stun_server, turn_server, ts_pwd)
434 pc_constraints = make_pc_constraints(compat)
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000435 offer_constraints = make_offer_constraints()
andrew@webrtc.org20078e22013-10-05 02:26:50 +0000436 media_constraints = make_media_stream_constraints(audio, video)
437 template_values = {'error_messages': error_messages,
438 'token': token,
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000439 'me': user,
440 'room_key': room_key,
441 'room_link': room_link,
442 'initiator': initiator,
443 'pc_config': json.dumps(pc_config),
444 'pc_constraints': json.dumps(pc_constraints),
445 'offer_constraints': json.dumps(offer_constraints),
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000446 'media_constraints': json.dumps(media_constraints),
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000447 'turn_url': turn_url,
wu@webrtc.orgbc189fb2013-09-13 20:11:47 +0000448 'stereo': stereo,
449 'audio_send_codec': audio_send_codec,
450 'audio_receive_codec': audio_receive_codec
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000451 }
452 if unittest:
453 target_page = 'test/test_' + unittest + '.html'
454 else:
455 target_page = 'index.html'
456
457 template = jinja_environment.get_template(target_page)
458 self.response.out.write(template.render(template_values))
459 logging.info('User ' + user + ' added to room ' + room_key)
460 logging.info('Room ' + room_key + ' has state ' + str(room))
461
462
463app = webapp2.WSGIApplication([
464 ('/', MainPage),
465 ('/message', MessagePage),
466 ('/_ah/channel/connected/', ConnectPage),
467 ('/_ah/channel/disconnected/', DisconnectPage)
468 ], debug=True)