blob: 952adcb8d6e664aa579d8cfcad12c72df23a7342 [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
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000063def handle_message(room, user, message):
64 message_obj = json.loads(message)
65 other_user = room.get_other_user(user)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000066 room_key = room.key().id_or_name()
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000067 if message_obj['type'] == 'bye':
68 # This would remove the other_user in loopback test too.
69 # So check its availability before forwarding Bye message.
70 room.remove_user(user)
71 logging.info('User ' + user + ' quit from room ' + room_key)
72 logging.info('Room ' + room_key + ' has state ' + str(room))
73 if other_user and room.has_user(other_user):
74 if message_obj['type'] == 'offer':
75 # Special case the loopback scenario.
76 if other_user == user:
77 message = make_loopback_answer(message)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000078 on_message(room, other_user, message)
79
80def get_saved_messages(client_id):
81 return Message.gql("WHERE client_id = :id", id=client_id)
82
83def delete_saved_messages(client_id):
84 messages = get_saved_messages(client_id)
85 for message in messages:
86 message.delete()
87 logging.info('Deleted the saved message for ' + client_id)
88
89def send_saved_messages(client_id):
90 messages = get_saved_messages(client_id)
91 for message in messages:
92 channel.send_message(client_id, message.msg)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +000093 logging.info('Delivered saved message to ' + client_id)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +000094 message.delete()
95
96def on_message(room, user, message):
97 client_id = make_client_id(room, user)
98 if room.is_connected(user):
99 channel.send_message(client_id, message)
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000100 logging.info('Delivered message to user ' + user)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000101 else:
102 new_message = Message(client_id = client_id, msg = message)
103 new_message.put()
104 logging.info('Saved message for user ' + user)
105
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000106def make_media_constraints(media, min_re, max_re):
107 video_constraints = { 'optional': [], 'mandatory': {} }
108 media_constraints = { 'video':video_constraints, 'audio':True }
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000109
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000110 # Media: audio:audio only; video:video only; (default):both.
111 if media.lower() == 'audio':
112 media_constraints['video'] = False
113 elif media.lower() == 'video':
114 media_constraints['audio'] = False
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000115
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000116 if media.lower() != 'audio' :
117 if min_re:
118 min_sizes = min_re.split('x')
119 if len(min_sizes) == 2:
120 video_constraints['mandatory']['minWidth'] = min_sizes[0]
121 video_constraints['mandatory']['minHeight'] = min_sizes[1]
122 else:
pbos@webrtc.orgb4a06232013-04-08 15:59:24 +0000123 logging.info('Ignored invalid min_re: ' + min_re)
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000124 if max_re:
125 max_sizes = max_re.split('x')
126 if len(max_sizes) == 2:
127 video_constraints['mandatory']['maxWidth'] = max_sizes[0]
128 video_constraints['mandatory']['maxHeight'] = max_sizes[1]
129 else:
pbos@webrtc.orgb4a06232013-04-08 15:59:24 +0000130 logging.info('Ignored invalid max_re: ' + max_re)
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000131 media_constraints['video'] = video_constraints
132
133 return media_constraints
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000134
135def make_pc_constraints(compat):
136 constraints = { 'optional': [] }
137 # For interop with FireFox. Enable DTLS in peerConnection ctor.
138 if compat.lower() == 'true':
139 constraints['optional'].append({'DtlsSrtpKeyAgreement': True})
140 return constraints
141
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000142def make_offer_constraints():
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000143 constraints = { 'mandatory': {}, 'optional': [] }
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000144 return constraints
145
146def append_url_arguments(request, link):
147 for argument in request.arguments():
148 if argument != 'r':
149 link += ('&' + cgi.escape(argument, True) + '=' +
150 cgi.escape(request.get(argument), True))
151 return link
152
153# This database is to store the messages from the sender client when the
154# receiver client is not ready to receive the messages.
155# Use TextProperty instead of StringProperty for msg because
156# the session description can be more than 500 characters.
157class Message(db.Model):
158 client_id = db.StringProperty()
159 msg = db.TextProperty()
160
161class Room(db.Model):
162 """All the data we store for a room"""
163 user1 = db.StringProperty()
164 user2 = db.StringProperty()
165 user1_connected = db.BooleanProperty(default=False)
166 user2_connected = db.BooleanProperty(default=False)
167
168 def __str__(self):
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000169 result = '['
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000170 if self.user1:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000171 result += "%s-%r" % (self.user1, self.user1_connected)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000172 if self.user2:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000173 result += ", %s-%r" % (self.user2, self.user2_connected)
174 result += ']'
175 return result
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000176
177 def get_occupancy(self):
178 occupancy = 0
179 if self.user1:
180 occupancy += 1
181 if self.user2:
182 occupancy += 1
183 return occupancy
184
185 def get_other_user(self, user):
186 if user == self.user1:
187 return self.user2
188 elif user == self.user2:
189 return self.user1
190 else:
191 return None
192
193 def has_user(self, user):
194 return (user and (user == self.user1 or user == self.user2))
195
196 def add_user(self, user):
197 if not self.user1:
198 self.user1 = user
199 elif not self.user2:
200 self.user2 = user
201 else:
202 raise RuntimeError('room is full')
203 self.put()
204
205 def remove_user(self, user):
206 delete_saved_messages(make_client_id(self, user))
207 if user == self.user2:
208 self.user2 = None
209 self.user2_connected = False
210 if user == self.user1:
211 if self.user2:
212 self.user1 = self.user2
213 self.user1_connected = self.user2_connected
214 self.user2 = None
215 self.user2_connected = False
216 else:
217 self.user1 = None
218 self.user1_connected = False
219 if self.get_occupancy() > 0:
220 self.put()
221 else:
222 self.delete()
223
224 def set_connected(self, user):
225 if user == self.user1:
226 self.user1_connected = True
227 if user == self.user2:
228 self.user2_connected = True
229 self.put()
230
231 def is_connected(self, user):
232 if user == self.user1:
233 return self.user1_connected
234 if user == self.user2:
235 return self.user2_connected
236
237class ConnectPage(webapp2.RequestHandler):
238 def post(self):
239 key = self.request.get('from')
240 room_key, user = key.split('/')
241 with LOCK:
242 room = Room.get_by_key_name(room_key)
243 # Check if room has user in case that disconnect message comes before
244 # connect message with unknown reason, observed with local AppEngine SDK.
245 if room and room.has_user(user):
246 room.set_connected(user)
247 send_saved_messages(make_client_id(room, user))
248 logging.info('User ' + user + ' connected to room ' + room_key)
249 logging.info('Room ' + room_key + ' has state ' + str(room))
250 else:
251 logging.warning('Unexpected Connect Message to room ' + room_key)
252
253
254class DisconnectPage(webapp2.RequestHandler):
255 def post(self):
256 key = self.request.get('from')
257 room_key, user = key.split('/')
258 with LOCK:
259 room = Room.get_by_key_name(room_key)
260 if room and room.has_user(user):
261 other_user = room.get_other_user(user)
262 room.remove_user(user)
263 logging.info('User ' + user + ' removed from room ' + room_key)
264 logging.info('Room ' + room_key + ' has state ' + str(room))
265 if other_user and other_user != user:
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000266 channel.send_message(make_client_id(room, other_user),
267 '{"type":"bye"}')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000268 logging.info('Sent BYE to ' + other_user)
269 logging.warning('User ' + user + ' disconnected from room ' + room_key)
270
271
272class MessagePage(webapp2.RequestHandler):
273 def post(self):
274 message = self.request.body
275 room_key = self.request.get('r')
276 user = self.request.get('u')
277 with LOCK:
278 room = Room.get_by_key_name(room_key)
279 if room:
280 handle_message(room, user, message)
281 else:
282 logging.warning('Unknown room ' + room_key)
283
284class MainPage(webapp2.RequestHandler):
285 """The main UI page, renders the 'index.html' template."""
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000286 def get(self):
287 """Renders the main page. When this page is shown, we create a new
288 channel to push asynchronous updates to the client."""
289 # get the base url without arguments.
290 base_url = self.request.path_url
291 room_key = sanitize(self.request.get('r'))
292 debug = self.request.get('debug')
293 unittest = self.request.get('unittest')
294 stun_server = self.request.get('ss')
295 turn_server = self.request.get('ts')
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000296 min_re = self.request.get('minre')
297 max_re = self.request.get('maxre')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000298 hd_video = self.request.get('hd')
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000299 turn_url = 'https://computeengineondemand.appspot.com/'
vikasmarwaha@webrtc.orgebf49da2013-03-19 22:15:55 +0000300 if hd_video.lower() == 'true':
301 min_re = '1280x720'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000302 ts_pwd = self.request.get('tp')
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000303 media = self.request.get('media')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000304 # set compat to true by default.
305 compat = 'true'
306 if self.request.get('compat'):
307 compat = self.request.get('compat')
308 if debug == 'loopback':
309 # set compat to false as DTLS does not work for loopback.
310 compat = 'false'
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000311 # set stereo to false by default
312 stereo = 'false'
313 if self.request.get('stereo'):
314 stereo = self.request.get('stereo')
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000315
316
317 # token_timeout for channel creation, default 30min, max 2 days, min 3min.
318 token_timeout = self.request.get_range('tt',
319 min_value = 3,
320 max_value = 3000,
321 default = 30)
322
323 if unittest:
324 # Always create a new room for the unit tests.
325 room_key = generate_random(8)
326
327 if not room_key:
328 room_key = generate_random(8)
329 redirect = '/?r=' + room_key
330 redirect = append_url_arguments(self.request, redirect)
331 self.redirect(redirect)
332 logging.info('Redirecting visitor to base URL to ' + redirect)
333 return
334
335 user = None
336 initiator = 0
337 with LOCK:
338 room = Room.get_by_key_name(room_key)
339 if not room and debug != "full":
340 # New room.
341 user = generate_random(8)
342 room = Room(key_name = room_key)
343 room.add_user(user)
344 if debug != 'loopback':
345 initiator = 0
346 else:
347 room.add_user(user)
348 initiator = 1
349 elif room and room.get_occupancy() == 1 and debug != 'full':
350 # 1 occupant.
351 user = generate_random(8)
352 room.add_user(user)
353 initiator = 1
354 else:
355 # 2 occupants (full).
356 template = jinja_environment.get_template('full.html')
357 self.response.out.write(template.render({ 'room_key': room_key }))
358 logging.info('Room ' + room_key + ' is full')
359 return
phoglund@webrtc.org5d3713932013-03-07 09:59:43 +0000360
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000361 room_link = base_url + '?r=' + room_key
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000362 room_link = append_url_arguments(self.request, room_link)
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000363 turn_url = turn_url + 'turn?' + 'username=' + user + '&key=4080218913'
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000364 token = create_channel(room, user, token_timeout)
365 pc_config = make_pc_config(stun_server, turn_server, ts_pwd)
366 pc_constraints = make_pc_constraints(compat)
vikasmarwaha@webrtc.org59a06672013-05-16 01:05:19 +0000367 offer_constraints = make_offer_constraints()
braveyao@webrtc.orgf354e1f2013-03-20 00:23:55 +0000368 media_constraints = make_media_constraints(media, min_re, max_re)
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000369 template_values = {'token': token,
370 'me': user,
371 'room_key': room_key,
372 'room_link': room_link,
373 'initiator': initiator,
374 'pc_config': json.dumps(pc_config),
375 'pc_constraints': json.dumps(pc_constraints),
376 'offer_constraints': json.dumps(offer_constraints),
vikasmarwaha@webrtc.org222e9942013-04-06 05:58:15 +0000377 'media_constraints': json.dumps(media_constraints),
vikasmarwaha@webrtc.org1993a552013-05-13 18:48:09 +0000378 'turn_url': turn_url,
379 'stereo': stereo
vikasmarwaha@webrtc.org98fce152013-02-27 23:22:10 +0000380 }
381 if unittest:
382 target_page = 'test/test_' + unittest + '.html'
383 else:
384 target_page = 'index.html'
385
386 template = jinja_environment.get_template(target_page)
387 self.response.out.write(template.render(template_values))
388 logging.info('User ' + user + ' added to room ' + room_key)
389 logging.info('Room ' + room_key + ' has state ' + str(room))
390
391
392app = webapp2.WSGIApplication([
393 ('/', MainPage),
394 ('/message', MessagePage),
395 ('/_ah/channel/connected/', ConnectPage),
396 ('/_ah/channel/disconnected/', DisconnectPage)
397 ], debug=True)