blob: 19b270be273bbb5965e87e1357d871337e0a1f12 [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
42def make_pc_config(stun_server, turn_server, ts_pwd):
43 servers = []
44 if turn_server:
45 turn_config = 'turn:{}'.format(turn_server)
46 servers.append({'url':turn_config, 'credential':ts_pwd})
47 if stun_server:
48 stun_config = 'stun:{}'.format(stun_server)
49 else:
50 stun_config = 'stun:' + 'stun.l.google.com:19302'
51 servers.append({'url':stun_config})
52 return {'iceServers':servers}
53
54def create_channel(room, user, duration_minutes):
55 client_id = make_client_id(room, user)
56 return channel.create_channel(client_id, duration_minutes)
57
58def make_loopback_answer(message):
59 message = message.replace("\"offer\"", "\"answer\"")
60 message = message.replace("a=ice-options:google-ice\\r\\n", "")
61 return message
62
63def maybe_add_fake_crypto(message):
64 if message.find("a=crypto") == -1:
65 index = len(message)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000066 crypto_line = ("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:"
67 "BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\\r\\n")
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000068 # reverse find for multiple find and insert operations.
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000069 index = message.rfind("c=IN", 0, index)
70 while (index != -1):
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000071 message = message[:index] + crypto_line + message[index:]
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000072 index = message.rfind("c=IN", 0, index)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000073 return message
74
75def handle_message(room, user, message):
76 message_obj = json.loads(message)
77 other_user = room.get_other_user(user)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000078 room_key = room.key().id_or_name()
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000079 if message_obj['type'] == 'bye':
80 # This would remove the other_user in loopback test too.
81 # So check its availability before forwarding Bye message.
82 room.remove_user(user)
83 logging.info('User ' + user + ' quit from room ' + room_key)
84 logging.info('Room ' + room_key + ' has state ' + str(room))
85 if other_user and room.has_user(other_user):
86 if message_obj['type'] == 'offer':
87 # Special case the loopback scenario.
88 if other_user == user:
89 message = make_loopback_answer(message)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000090 # Workaround Chrome bug.
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000091 # Insert a=crypto line into offer from FireFox.
92 # TODO(juberti): Remove this call.
93 message = maybe_add_fake_crypto(message)
94 on_message(room, other_user, message)
95
96def get_saved_messages(client_id):
97 return Message.gql("WHERE client_id = :id", id=client_id)
98
99def delete_saved_messages(client_id):
100 messages = get_saved_messages(client_id)
101 for message in messages:
102 message.delete()
103 logging.info('Deleted the saved message for ' + client_id)
104
105def send_saved_messages(client_id):
106 messages = get_saved_messages(client_id)
107 for message in messages:
108 channel.send_message(client_id, message.msg)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000109 logging.info('Delivered saved message to ' + client_id)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000110 message.delete()
111
112def on_message(room, user, message):
113 client_id = make_client_id(room, user)
114 if room.is_connected(user):
115 channel.send_message(client_id, message)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000116 logging.info('Delivered message to user ' + user)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000117 else:
118 new_message = Message(client_id = client_id, msg = message)
119 new_message.put()
120 logging.info('Saved message for user ' + user)
121
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000122def make_media_constraints_by_resolution(min_re, max_re):
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000123 constraints = { 'optional': [], 'mandatory': {} }
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000124 if min_re:
125 min_sizes = min_re.split('x')
126 if len(min_sizes) == 2:
127 constraints['mandatory']['minWidth'] = min_sizes[0]
128 constraints['mandatory']['minHeight'] = min_sizes[1]
129 else:
130 logging.info('Ignored invalid min_re: ' + min_re);
131
132 if max_re:
133 max_sizes = max_re.split('x')
134 if len(max_sizes) == 2:
135 constraints['mandatory']['maxWidth'] = max_sizes[0]
136 constraints['mandatory']['maxHeight'] = max_sizes[1]
137 else:
138 logging.info('Ignored invalid max_re: ' + max_re);
139
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000140 return constraints
141
142def make_pc_constraints(compat):
143 constraints = { 'optional': [] }
144 # For interop with FireFox. Enable DTLS in peerConnection ctor.
145 if compat.lower() == 'true':
146 constraints['optional'].append({'DtlsSrtpKeyAgreement': True})
147 return constraints
148
149def make_offer_constraints(compat):
150 constraints = { 'mandatory': {}, 'optional': [] }
151 # For interop with FireFox. Disable Data Channel in createOffer.
152 if compat.lower() == 'true':
153 constraints['mandatory']['MozDontOfferDataChannel'] = True
154 return constraints
155
156def append_url_arguments(request, link):
157 for argument in request.arguments():
158 if argument != 'r':
159 link += ('&' + cgi.escape(argument, True) + '=' +
160 cgi.escape(request.get(argument), True))
161 return link
162
163# This database is to store the messages from the sender client when the
164# receiver client is not ready to receive the messages.
165# Use TextProperty instead of StringProperty for msg because
166# the session description can be more than 500 characters.
167class Message(db.Model):
168 client_id = db.StringProperty()
169 msg = db.TextProperty()
170
171class Room(db.Model):
172 """All the data we store for a room"""
173 user1 = db.StringProperty()
174 user2 = db.StringProperty()
175 user1_connected = db.BooleanProperty(default=False)
176 user2_connected = db.BooleanProperty(default=False)
177
178 def __str__(self):
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000179 result = '['
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000180 if self.user1:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000181 result += "%s-%r" % (self.user1, self.user1_connected)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000182 if self.user2:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000183 result += ", %s-%r" % (self.user2, self.user2_connected)
184 result += ']'
185 return result
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000186
187 def get_occupancy(self):
188 occupancy = 0
189 if self.user1:
190 occupancy += 1
191 if self.user2:
192 occupancy += 1
193 return occupancy
194
195 def get_other_user(self, user):
196 if user == self.user1:
197 return self.user2
198 elif user == self.user2:
199 return self.user1
200 else:
201 return None
202
203 def has_user(self, user):
204 return (user and (user == self.user1 or user == self.user2))
205
206 def add_user(self, user):
207 if not self.user1:
208 self.user1 = user
209 elif not self.user2:
210 self.user2 = user
211 else:
212 raise RuntimeError('room is full')
213 self.put()
214
215 def remove_user(self, user):
216 delete_saved_messages(make_client_id(self, user))
217 if user == self.user2:
218 self.user2 = None
219 self.user2_connected = False
220 if user == self.user1:
221 if self.user2:
222 self.user1 = self.user2
223 self.user1_connected = self.user2_connected
224 self.user2 = None
225 self.user2_connected = False
226 else:
227 self.user1 = None
228 self.user1_connected = False
229 if self.get_occupancy() > 0:
230 self.put()
231 else:
232 self.delete()
233
234 def set_connected(self, user):
235 if user == self.user1:
236 self.user1_connected = True
237 if user == self.user2:
238 self.user2_connected = True
239 self.put()
240
241 def is_connected(self, user):
242 if user == self.user1:
243 return self.user1_connected
244 if user == self.user2:
245 return self.user2_connected
246
247class ConnectPage(webapp2.RequestHandler):
248 def post(self):
249 key = self.request.get('from')
250 room_key, user = key.split('/')
251 with LOCK:
252 room = Room.get_by_key_name(room_key)
253 # Check if room has user in case that disconnect message comes before
254 # connect message with unknown reason, observed with local AppEngine SDK.
255 if room and room.has_user(user):
256 room.set_connected(user)
257 send_saved_messages(make_client_id(room, user))
258 logging.info('User ' + user + ' connected to room ' + room_key)
259 logging.info('Room ' + room_key + ' has state ' + str(room))
260 else:
261 logging.warning('Unexpected Connect Message to room ' + room_key)
262
263
264class DisconnectPage(webapp2.RequestHandler):
265 def post(self):
266 key = self.request.get('from')
267 room_key, user = key.split('/')
268 with LOCK:
269 room = Room.get_by_key_name(room_key)
270 if room and room.has_user(user):
271 other_user = room.get_other_user(user)
272 room.remove_user(user)
273 logging.info('User ' + user + ' removed from room ' + room_key)
274 logging.info('Room ' + room_key + ' has state ' + str(room))
275 if other_user and other_user != user:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000276 channel.send_message(make_client_id(room, other_user),
277 '{"type":"bye"}')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000278 logging.info('Sent BYE to ' + other_user)
279 logging.warning('User ' + user + ' disconnected from room ' + room_key)
280
281
282class MessagePage(webapp2.RequestHandler):
283 def post(self):
284 message = self.request.body
285 room_key = self.request.get('r')
286 user = self.request.get('u')
287 with LOCK:
288 room = Room.get_by_key_name(room_key)
289 if room:
290 handle_message(room, user, message)
291 else:
292 logging.warning('Unknown room ' + room_key)
293
294class MainPage(webapp2.RequestHandler):
295 """The main UI page, renders the 'index.html' template."""
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000296 def get(self):
297 """Renders the main page. When this page is shown, we create a new
298 channel to push asynchronous updates to the client."""
299 # get the base url without arguments.
300 base_url = self.request.path_url
301 room_key = sanitize(self.request.get('r'))
302 debug = self.request.get('debug')
303 unittest = self.request.get('unittest')
304 stun_server = self.request.get('ss')
305 turn_server = self.request.get('ts')
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000306 min_re = self.request.get('minre')
307 max_re = self.request.get('maxre')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000308 hd_video = self.request.get('hd')
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000309 if hd_video.lower() == 'true':
310 min_re = '1280x720'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000311 ts_pwd = self.request.get('tp')
312 # set compat to true by default.
313 compat = 'true'
314 if self.request.get('compat'):
315 compat = self.request.get('compat')
316 if debug == 'loopback':
317 # set compat to false as DTLS does not work for loopback.
318 compat = 'false'
319
320
321 # token_timeout for channel creation, default 30min, max 2 days, min 3min.
322 token_timeout = self.request.get_range('tt',
323 min_value = 3,
324 max_value = 3000,
325 default = 30)
326
327 if unittest:
328 # Always create a new room for the unit tests.
329 room_key = generate_random(8)
330
331 if not room_key:
332 room_key = generate_random(8)
333 redirect = '/?r=' + room_key
334 redirect = append_url_arguments(self.request, redirect)
335 self.redirect(redirect)
336 logging.info('Redirecting visitor to base URL to ' + redirect)
337 return
338
339 user = None
340 initiator = 0
341 with LOCK:
342 room = Room.get_by_key_name(room_key)
343 if not room and debug != "full":
344 # New room.
345 user = generate_random(8)
346 room = Room(key_name = room_key)
347 room.add_user(user)
348 if debug != 'loopback':
349 initiator = 0
350 else:
351 room.add_user(user)
352 initiator = 1
353 elif room and room.get_occupancy() == 1 and debug != 'full':
354 # 1 occupant.
355 user = generate_random(8)
356 room.add_user(user)
357 initiator = 1
358 else:
359 # 2 occupants (full).
360 template = jinja_environment.get_template('full.html')
361 self.response.out.write(template.render({ 'room_key': room_key }))
362 logging.info('Room ' + room_key + ' is full')
363 return
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000364
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000365 room_link = base_url + '/?r=' + room_key
366 room_link = append_url_arguments(self.request, room_link)
367 token = create_channel(room, user, token_timeout)
368 pc_config = make_pc_config(stun_server, turn_server, ts_pwd)
369 pc_constraints = make_pc_constraints(compat)
370 offer_constraints = make_offer_constraints(compat)
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000371 media_constraints = make_media_constraints_by_resolution(min_re, max_re)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000372 template_values = {'token': token,
373 'me': user,
374 'room_key': room_key,
375 'room_link': room_link,
376 'initiator': initiator,
377 'pc_config': json.dumps(pc_config),
378 'pc_constraints': json.dumps(pc_constraints),
379 'offer_constraints': json.dumps(offer_constraints),
380 'media_constraints': json.dumps(media_constraints)
381 }
382 if unittest:
383 target_page = 'test/test_' + unittest + '.html'
384 else:
385 target_page = 'index.html'
386
387 template = jinja_environment.get_template(target_page)
388 self.response.out.write(template.render(template_values))
389 logging.info('User ' + user + ' added to room ' + room_key)
390 logging.info('Room ' + room_key + ' has state ' + str(room))
391
392
393app = webapp2.WSGIApplication([
394 ('/', MainPage),
395 ('/message', MessagePage),
396 ('/_ah/channel/connected/', ConnectPage),
397 ('/_ah/channel/disconnected/', DisconnectPage)
398 ], debug=True)