Download:
child 1:4fde67f78a8e
0:be29d4a1df6d
Anton Shestakov <engored@ya.ru>, Sat, 09 Mar 2013 13:51:40 +0900
Sort of works. This is as fragile as it sounds actually, because it works only in perfect cases. Ice candidates from Chrome produce errors in Firefox; also Firefox cannot create answer to Chrome's offer; sound is missing in Firefox(?).

7 файлов изменено, 286 вставок(+), 0 удалений(-) [+]
.hgignore file | annotate | diff | comparison | revisions
README.rst file | annotate | diff | comparison | revisions
REQS file | annotate | diff | comparison | revisions
app.py file | annotate | diff | comparison | revisions
static/js/main.js file | annotate | diff | comparison | revisions
static/js/webrtc-adapter.js file | annotate | diff | comparison | revisions
templates/index.html file | annotate | diff | comparison | revisions
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Sat Mar 09 13:51:40 2013 +0900
@@ -0,0 +1,3 @@
+syntax: glob
+*.pyc
+venv/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/README.rst Sat Mar 09 13:51:40 2013 +0900
@@ -0,0 +1,14 @@
+Tornado-WebRTC
+==============
+
+Tornado app that can establish WebRTC sessions. Incidentally, that's what
+is says on the tin.
+
+So yeah, for people who have no idea what WebRTC is; it's a realtime
+communications API for browsers. Open website, dial your friend, have fun.
+
+And for people who roughly know how WebRTC works; this app:
+
+#. provides a page with all required elements and scripts and
+#. lets browsers (or even separate tabs) talk to each other via WebSockets
+ in order to connect peer-to-peer
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/REQS Sat Mar 09 13:51:40 2013 +0900
@@ -0,0 +1,1 @@
+tornado
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app.py Sat Mar 09 13:51:40 2013 +0900
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import os
+import logging
+
+from tornado.ioloop import IOLoop
+from tornado.options import define, options
+from tornado.web import Application, RequestHandler
+from tornado.websocket import WebSocketHandler
+
+
+rel = lambda *x: os.path.abspath(os.path.join(os.path.dirname(__file__), *x))
+
+
+class MainHandler(RequestHandler):
+ def get(self):
+ self.render('index.html')
+
+
+class EchoWebSocket(WebSocketHandler):
+ clients = []
+
+ def open(self):
+ logging.info('WebSocket opened from %s', self.request.remote_ip)
+ EchoWebSocket.clients.append(self)
+
+ def on_message(self, message):
+ logging.info('got message from %s: %s', self.request.remote_ip, message)
+ for client in EchoWebSocket.clients:
+ if client is self:
+ continue
+ client.write_message(message)
+
+ def on_close(self):
+ logging.info('WebSocket closed')
+ EchoWebSocket.clients.remove(self)
+
+
+def main():
+ define('listen', metavar='IP', default='127.0.0.1', help='listen on IP address (default 127.0.0.1)')
+ define('port', metavar='PORT', default=8888, type=int, help='listen on PORT (default 8888)')
+ define('debug', metavar='True|False', default=False, type=bool,
+ help='enable Tornado debug mode: templates will not be cached '
+ 'and the app will watch for changes to its source files '
+ 'and reload itself when anything changes')
+
+ options.parse_command_line()
+
+ settings = dict(
+ template_path=rel('templates'),
+ static_path=rel('static'),
+ debug=options.debug
+ )
+
+ application = Application([
+ (r'/', MainHandler),
+ (r'/ws', EchoWebSocket),
+ ], **settings)
+
+ application.listen(address=options.listen, port=options.port)
+ IOLoop.instance().start()
+
+
+if __name__ == '__main__':
+ main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/main.js Sat Mar 09 13:51:40 2013 +0900
@@ -0,0 +1,84 @@
+var ws = new WebSocket('ws://' + location.host + '/ws');
+var pc;
+
+
+function call() {
+ init({audio: true, video: true}, true);
+}
+
+
+function receive() {
+ init({audio: true, video: true}, false);
+}
+
+
+function init(constraints, initiator) {
+ getUserMedia(constraints, function(stream) {
+ pc = new RTCPeerConnection(null);
+
+ pc.addStream(stream);
+ attachMediaStream(document.getElementById('local'), stream);
+ pc.onaddstream = function(event) {
+ attachMediaStream(document.getElementById('remote'), event.stream);
+ };
+ pc.onicecandidate = function(event) {
+ if (event.candidate) {
+ ws.send(JSON.stringify(event.candidate));
+ }
+ };
+ ws.onmessage = function (event) {
+ var signal = JSON.parse(event.data);
+ if (signal.sdp) {
+ if (initiator) {
+ receiveAnswer(signal);
+ } else {
+ receiveOffer(signal);
+ }
+ } else if (signal.candidate) {
+ pc.addIceCandidate(new RTCIceCandidate(signal));
+ }
+ };
+
+ if (initiator) {
+ createOffer();
+ }
+ }, fail);
+}
+
+
+function createOffer() {
+ console.log('creating offer');
+ pc.createOffer(function(offer) {
+ console.log('created offer');
+ pc.setLocalDescription(offer, function() {
+ console.log('setLocalDescription done, sending to remote');
+ ws.send(JSON.stringify(offer));
+ }, fail);
+ }, fail);
+}
+
+
+function receiveOffer(offer) {
+ console.log('received offer');
+ pc.setRemoteDescription(new RTCSessionDescription(offer), function() {
+ console.log('creating answer');
+ pc.createAnswer(function(answer) {
+ console.log('created answer');
+ pc.setLocalDescription(answer, function() {
+ console.log('setLocalDescription done, sending to remote');
+ ws.send(JSON.stringify(answer));
+ }, fail);
+ }, fail);
+ }, fail);
+}
+
+
+function receiveAnswer(answer) {
+ console.log('received answer');
+ pc.setRemoteDescription(new RTCSessionDescription(answer));
+}
+
+
+function fail(error) {
+ console.log(error);
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/js/webrtc-adapter.js Sat Mar 09 13:51:40 2013 +0900
@@ -0,0 +1,89 @@
+var RTCPeerConnection = null;
+var getUserMedia = null;
+var attachMediaStream = null;
+var reattachMediaStream = null;
+var webrtcDetectedBrowser = null;
+
+if (navigator.mozGetUserMedia) {
+ console.log("This appears to be Firefox");
+
+ webrtcDetectedBrowser = "firefox";
+
+ // The RTCPeerConnection object.
+ RTCPeerConnection = mozRTCPeerConnection;
+
+ // The RTCSessionDescription object.
+ RTCSessionDescription = Object;
+
+ // The RTCIceCandidate object.
+ RTCIceCandidate = mozRTCIceCandidate;
+
+ // Get UserMedia (only difference is the prefix).
+ // Code from Adam Barth.
+ getUserMedia = navigator.mozGetUserMedia.bind(navigator);
+
+ // Attach a media stream to an element.
+ attachMediaStream = function(element, stream) {
+ console.log("Attaching media stream");
+ element.src = URL.createObjectURL(stream);;
+ element.play();
+ };
+
+ reattachMediaStream = function(to, from) {
+ console.log("Reattaching media stream");
+ to.mozSrcObject = from.mozSrcObject;
+ to.play();
+ };
+
+ // Fake get{Video,Audio}Tracks
+ MediaStream.prototype.getVideoTracks = function() {
+ return [];
+ };
+
+ MediaStream.prototype.getAudioTracks = function() {
+ return [];
+ };
+} else if (navigator.webkitGetUserMedia) {
+ console.log("This appears to be Chrome");
+
+ webrtcDetectedBrowser = "chrome";
+
+ // The RTCPeerConnection object.
+ RTCPeerConnection = webkitRTCPeerConnection;
+
+ // Get UserMedia (only difference is the prefix).
+ // Code from Adam Barth.
+ getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+
+ // Attach a media stream to an element.
+ attachMediaStream = function(element, stream) {
+ element.src = webkitURL.createObjectURL(stream);
+ };
+
+ reattachMediaStream = function(to, from) {
+ to.src = from.src;
+ };
+
+ // The representation of tracks in a stream is changed in M26.
+ // Unify them for earlier Chrome versions in the coexisting period.
+ if (!webkitMediaStream.prototype.getVideoTracks) {
+ webkitMediaStream.prototype.getVideoTracks = function() {
+ return this.videoTracks;
+ };
+ webkitMediaStream.prototype.getAudioTracks = function() {
+ return this.audioTracks;
+ };
+ }
+
+ // New syntax of getXXXStreams method in M26.
+ if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
+ webkitRTCPeerConnection.prototype.getLocalStreams = function() {
+ return this.localStreams;
+ };
+ webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
+ return this.remoteStreams;
+ };
+ }
+} else {
+ console.log("Browser does not appear to be WebRTC-capable");
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/index.html Sat Mar 09 13:51:40 2013 +0900
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Tornado WebRTC</title>
+ <script src="{{ static_url('js/webrtc-adapter.js') }}"></script>
+ <script src="{{ static_url('js/main.js') }}"></script>
+ <style>
+ video#remote {
+ width: 480px;
+ height: 360px;
+ background: #777;
+ }
+ video#local {
+ position: absolute;
+ width: 160px;
+ height: 120px;
+ background: #555;
+ margin-top: 240px;
+ margin-left: -165px;
+ }
+ </style>
+ </head>
+ <body>
+ <video id="remote" autoplay></video>
+ <video id="local" autoplay muted></video>
+ <button onclick="call()">I want to call</button>
+ <button onclick="receive()">I want to receive a call</button>
+ </body>
+</html>