├── .gitignore ├── CMakeLists.txt ├── README.md ├── dashboard ├── README.md ├── app.js ├── assets │ ├── app.css │ ├── app.js │ ├── client.js │ └── helpers.js ├── index.ejs ├── package.json └── symple.json └── server ├── main.cpp ├── signaler.cpp └── signaler.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | bak 5 | build* 6 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.10) 2 | project(webrtcnativelabs) 3 | 4 | # Set the source directory and search locations as required for your project 5 | set(sourcedir server) 6 | set(sourcefiles ${sourcedir}/*.cpp) 7 | set(headerfiles ${sourcedir}/*.h) 8 | 9 | # Set some default options for LibSourcey 10 | set(WITH_FFMPEG ON) 11 | set(WITH_WEBRTC ON) # ON CACHE BOOL "Enable WebRTC dependency" FORCE) 12 | set(BUILD_APPLICATIONS OFF CACHE BOOL "Disable applications" FORCE) 13 | set(BUILD_SAMPLES OFF CACHE BOOL "Disable samples" FORCE) 14 | set(BUILD_TESTS OFF CACHE BOOL "Disable tests" FORCE) 15 | set(BUILD_SHARED_LIBS OFF) 16 | 17 | # Include LibSourcey 18 | include("../libsourcey/LibSourcey.cmake") 19 | #print_module_variables(LibSourcey) 20 | 21 | include_directories(${sourcedir} ${LibSourcey_INCLUDE_DIRS}) 22 | link_directories(${LibSourcey_LIBRARY_DIRS}) 23 | link_libraries(${LibSourcey_INCLUDE_LIBRARIES}) 24 | 25 | # Glob your sources and headers 26 | # Be sure to modify the search paths according to your project structure 27 | file(GLOB_RECURSE sources ${sourcefiles}) 28 | file(GLOB_RECURSE headers ${headerfiles}) 29 | 30 | # Create and install the executable 31 | add_executable(webrtcnativelabs ${sources} ${headers}) 32 | add_dependencies(webrtcnativelabs libuv jsoncpp) 33 | add_dependencies(webrtcnativelabs uv base crypto av net http util json socketio symple webrtc) 34 | install(TARGETS webrtcnativelabs RUNTIME DESTINATION bin) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTC Native Media Streamer 2 | 3 | This sample app showcases how to record and stream live WebRTC video streams on the server side. Audio and video streams are recorded and multiplexed in MP4 format (H.264/ACC) by default, but any format supported by LibSourcey/FFmpeg can be used. 4 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Symple WebRTC Video Chat Demo 2 | 3 | The Symple video chat demo is an example of how to use Symple to build an instant messaging and WebRTC video chat application in about 100 lines of JavaScript. External projects used are AngularJS, Bootstrap, Node.js and Express. 4 | 5 | See this blog post for more information about the demo: http://sourcey.com/symple-webrtc-video-chat-demo 6 | 7 | ## What is Symple? 8 | 9 | Symple is a unrestrictive real time messaging and presence protocol that implements the minimum number of features required to build full fledged messaging applications with security, flexibility, performance and scalability in mind. These features include: 10 | 11 | * Session sharing with any backend (via Redis) 12 | * User rostering and presence 13 | * Media streaming (via WebRTC, [see demo](http://symple.sourcey.com)) 14 | * Scoped messaging ie. direct, user and group scope 15 | * Real-time commands and events 16 | * Real-time forms 17 | 18 | Symple currently has client implementations in [JavaScript](https://github.com/sourcey/symple-client), [Ruby](https://github.com/sourcey/symple-client-ruby) and [C++](https://github.com/sourcey/libsourcey/tree/master/src/symple), which make it ideal for a wide range of messaging requirements, such as building real-time games and applications that run in the web browser, desktop, and mobile phone. 19 | 20 | ## Usage 21 | 22 | 1. Install dependencies: `npm install` 23 | 2. Fire up the server: `node app` 24 | 3. And point your browser to: `http://localhost:4550` 25 | 26 | ## Hacking 27 | 28 | Some key options are specified in the main HTML file located at `index.ejs` 29 | 30 | **CLIENT_OPTIONS** This is the options hash for the Symple client. This is where you specify the server URL and Peer object. Note that we have disabled 'websocket' transport by default, but you will probably want to re-enable it in production. 31 | 32 | **WEBRTC_CONFIG** This is the PeerConnection options hash. In production you will want to specify some TURN servers so as to ensure the p2p connection succeeds in all network topologies. 33 | 34 | Other than that all relevant JavaScript is located in `assets/app.js` and `assets/helpers.js`. Enjoy! 35 | 36 | ## Contact 37 | 38 | For more information please check out the Symple homepage: http://sourcey.com/symple/ 39 | If you have a bug or an issue then please use the Github issue tracker: https://github.com/sourcey/symple-webrtc-video-chat-demo/issues 40 | -------------------------------------------------------------------------------- /dashboard/app.js: -------------------------------------------------------------------------------- 1 | // 2 | /// Setup the Symple server 3 | 4 | var Symple = require('symple'); 5 | var sy = new Symple(); 6 | sy.loadConfig(__dirname + '/symple.json'); // see symple.json for options 7 | sy.init(); 8 | console.log('Symple server listening on port ' + sy.config.port); 9 | 10 | 11 | // 12 | /// Setup the client web server 13 | 14 | var express = require('express'), 15 | path = require('path'), 16 | redis = require('redis'), 17 | client = redis.createClient(), 18 | app = express(), 19 | serverPort = parseInt(sy.config.port) 20 | clientPort = serverPort - 1; 21 | 22 | app.set('port', clientPort); 23 | app.set('view engine', 'ejs'); 24 | app.set('views', __dirname + '/'); 25 | app.use(express.static(__dirname + '/assets')); 26 | app.use(express.static(__dirname + '/node_modules/symple-client/src')); 27 | app.use(express.static(__dirname + '/node_modules/symple-client-player/src')); 28 | 29 | app.get('/', function (req, res) { 30 | // Create a random token to identify this client 31 | // NOTE: This method of generating unique tokens is not secure, so don't use 32 | // it in production ;) 33 | var token = '' + Math.random(); 34 | 35 | // Create the arbitrary user session object here 36 | var session = { 37 | // user: 'demo', 38 | // name: 'Demo User', 39 | group: 'public' 40 | } 41 | 42 | // Store the user session on Redis 43 | // This will be sent to the Symple server to authenticate the session 44 | // client.set('symple:session:' + token, JSON.stringify(session), redis.print); 45 | 46 | // Render the response 47 | res.render('index', { 48 | port: serverPort, 49 | token: token, 50 | peer: session }); 51 | }); 52 | 53 | app.listen(app.get('port'), function () { 54 | console.log('Express server listening on port ' + app.get('port')); 55 | }); 56 | -------------------------------------------------------------------------------- /dashboard/assets/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #e0e0e0; 3 | } 4 | 5 | form { 6 | margin-bottom: 0; 7 | } 8 | 9 | 10 | 11 | 12 | #container { 13 | max-width: 1200px; 14 | margin: 0px auto; 15 | padding: 15px; 16 | } 17 | #nav { 18 | margin-top: 15px; 19 | margin-bottom: 15px; 20 | } 21 | #nav a { 22 | margin: 0 20px 0 0; 23 | } 24 | 25 | footer { 26 | clear: both; 27 | padding: 20px 0 0; 28 | text-align: center; 29 | color: #999; 30 | } 31 | 32 | #logo { 33 | display: inline-block; 34 | font-size: 22px; 35 | margin-top: 0; 36 | margin-bottom: 0; 37 | } 38 | #logo a { 39 | color: black; 40 | } 41 | 42 | .panel-heading { 43 | background: none !important; 44 | } 45 | .panel-title { 46 | font-size: 18px; 47 | color: #999; 48 | } 49 | 50 | .list-group .btn-group-sm > .btn { 51 | padding: 3px 5px; 52 | color: #999; 53 | } 54 | .list-group .btn-group-sm > .btn.active { 55 | color: #000; 56 | } 57 | .list-group .btn-group-sm > .btn.record.active { 58 | color: #c00; 59 | } 60 | 61 | 62 | 63 | #video .panel-body { 64 | padding: 0; 65 | border-radius: 0 0 4px 4px; 66 | background: black; 67 | } 68 | #video .row { 69 | margin: 0; 70 | } 71 | #video .col { 72 | padding: 0; 73 | margin: 0; 74 | } 75 | 76 | 77 | /* 78 | Video 79 | */ 80 | 81 | #video .panel-body { 82 | position: relative; 83 | } 84 | 85 | #video .local-video-wrap { 86 | position: absolute; 87 | bottom: 15px; 88 | right: 15px; 89 | height: 96px; 90 | width: 128px; 91 | border: 1px solid #333; 92 | z-index: 1; 93 | background: rgba(0, 0, 0, 0.5); 94 | } 95 | #video .local-video-wrap video { 96 | height: 96px; 97 | object-fit: cover; 98 | } 99 | #video .remote-video-wrap { 100 | height: 400px; 101 | } 102 | #video .remote-video-wrap video { 103 | height: 400px; 104 | max-width: 100%; 105 | /*object-fit: cover;*/ 106 | } 107 | 108 | #video #start-local-video { 109 | position: absolute; 110 | left: 50%; 111 | top: 50%; 112 | margin-top: -17px; 113 | margin-left: -47px; 114 | } 115 | 116 | #video .symple-player-status, 117 | #video .symple-player-controls { 118 | display: none; 119 | } 120 | 121 | 122 | #incoming-call-modal p { 123 | text-align: center; 124 | } 125 | 126 | 127 | 128 | /* 129 | Messages 130 | */ 131 | #messages .user { 132 | font-weight: bold; 133 | } 134 | /* 135 | #messages .user { 136 | color: #999; 137 | font-size: 12px; 138 | font-weight: bold; 139 | } 140 | #messages .data { 141 | display: block; 142 | margin: 3px 0 0; 143 | } 144 | font-weight: bold; 145 | #messages ul { 146 | list-style-type: none; 147 | margin: 0; 148 | padding: 0; 149 | } 150 | #messages ul li{ 151 | paddingtop: 8px 0; 152 | border-bottom: 1px solid #eee; 153 | } 154 | */ 155 | #messages .time { 156 | float: right; 157 | color: #999; 158 | font-size: 11px; 159 | } 160 | 161 | #messages #post-message { 162 | margin-bottom: 0; 163 | } 164 | 165 | /* 166 | Loading Overlay 167 | */ 168 | #loading-overlay { 169 | position: fixed; 170 | top: 0; 171 | left: 0; 172 | bottom: 0; 173 | right: 0; 174 | z-index: 1001; 175 | background: rgba(0, 0, 0, 0.5); 176 | } 177 | #loading-overlay .icon { 178 | position: absolute; 179 | top: 50%; 180 | left: 50%; 181 | width: 32px; 182 | height: 32px; 183 | margin: -16px 0 0 -16px; 184 | background-image: url(data:image/gif;base64,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); 185 | 186 | } 187 | -------------------------------------------------------------------------------- /dashboard/assets/app.js: -------------------------------------------------------------------------------- 1 | // Define the `sympleApp` module 2 | var sympleApp = angular.module('sympleApp', []); 3 | 4 | // Define the `DashboardController` controller on the `sympleApp` module 5 | sympleApp.controller('DashboardController', function DashboardController($scope) { 6 | 7 | 8 | // 9 | //= Variables 10 | 11 | // Client 12 | $scope.client; 13 | $scope.handle; 14 | $scope.peers = []; 15 | 16 | // Interface 17 | $scope.errorText = ""; 18 | $scope.isLoading = false; 19 | 20 | // Messaging 21 | $scope.directUser; 22 | $scope.messages = []; 23 | $scope.messageText = ""; 24 | 25 | // Video Chat 26 | $scope.localPlayer; 27 | $scope.remotePlayer; 28 | $scope.remoteVideoPeer; 29 | $scope.disableLocalAudio = false; // set true to prevent local feedback loop 30 | 31 | // Media Server 32 | $scope.mediaServerPeer; 33 | $scope.streamableFiles = []; // list of streamable files that can be played 34 | $scope.currentStreamingFile; // current file being streamed 35 | $scope.currentRecordingPeers = []; // list of user streams being recorded 36 | 37 | 38 | // 39 | //= Symple Client 40 | 41 | $scope.client = new Symple.Client(CLIENT_OPTIONS); 42 | 43 | $scope.client.on('announce', function(peer) { 44 | console.log('announce:', peer) 45 | 46 | $scope.client.join('public'); // join the public room 47 | $scope.isLoading = false; 48 | $scope.$apply(); 49 | }); 50 | 51 | $scope.client.on('presence', function(p) { 52 | console.log('presence:', p) 53 | }); 54 | 55 | $scope.client.on('message', function(m) { 56 | console.log('message:', m) 57 | 58 | // Normal Message 59 | if (!m.direct || m.direct == $scope.handle) { 60 | $scope.messages.push({ 61 | user: m.from.user, 62 | data: m.data, 63 | to: m.to, 64 | direct: m.direct, 65 | time: Symple.formatTime(new Date) 66 | }); 67 | $scope.$apply(); 68 | } 69 | else { 70 | console.log('dropping message:', m, m.direct) 71 | } 72 | }); 73 | 74 | $scope.client.on('command', function(c) { 75 | console.log('command:', c) 76 | 77 | if (c.node == 'call:init') { 78 | 79 | // Receive a call request 80 | if (!c.status) { 81 | 82 | // Show a dialog asking the user if they want to accept the call 83 | var e = $('#incoming-call-modal'); 84 | e.find('.caller').text('@' + c.from.user); 85 | e.find('.accept').unbind('click').click(function() { 86 | c.status = 200; 87 | $scope.remoteVideoPeer = c.from; 88 | $scope.client.respond(c); 89 | $scope.$apply(); 90 | e.modal('hide'); 91 | }) 92 | e.find('.reject').unbind('click').click(function() { 93 | c.status = 500; 94 | $scope.client.respond(c); 95 | e.modal('hide'); 96 | }) 97 | e.modal('show'); 98 | } 99 | 100 | // Handle call accepted 101 | else if (c.status == 200) { 102 | $scope.remoteVideoPeer = c.from; 103 | $scope.startLocalVideo(); 104 | $scope.$apply(); 105 | } 106 | 107 | // Handle call rejected or failure 108 | else if (c.status == 500) { 109 | alert('Call failed'); 110 | } 111 | else { 112 | alert('Unknown response status'); 113 | } 114 | } 115 | 116 | else if (c.node == 'streaming:start') { 117 | // Handle streaming start response 118 | if (c.status == 200) { 119 | $scope.remoteVideoPeer = c.from; 120 | $scope.$apply(); 121 | } 122 | } 123 | 124 | else if (c.node == 'streaming:files') { 125 | // Handle streaming filed response 126 | if (c.status == 200) { 127 | $scope.streamableFiles = c.data.files; 128 | $scope.$apply(); 129 | } 130 | } 131 | }); 132 | 133 | $scope.client.on('event', function(e) { 134 | console.log('event:', e) 135 | 136 | // Only handle events from the remoteVideoPeer 137 | if (!$scope.remoteVideoPeer || $scope.remoteVideoPeer.id != e.from.id) { 138 | console.log('mismatch event:', e.from, $scope.remoteVideoPeer) 139 | return 140 | } 141 | 142 | // ICE SDP 143 | if (e.name == 'ice:sdp') { 144 | if (e.sdp.type == 'offer') { 145 | 146 | // Create the remote player on offer 147 | if (!$scope.remotePlayer) { 148 | $scope.remotePlayer = createPlayer($scope, 'answerer', '#video .remote-video'); 149 | $scope.remotePlayer.play(); 150 | } 151 | $scope.remotePlayer.engine.recvRemoteSDP(e.sdp); 152 | } 153 | if (e.sdp.type == 'answer') { 154 | $scope.localPlayer.engine.recvRemoteSDP(e.sdp); 155 | } 156 | } 157 | 158 | // ICE Candidate 159 | else if (e.name == 'ice:candidate') { 160 | if (e.origin == 'answerer') 161 | $scope.localPlayer.engine.recvRemoteCandidate(e.candidate); 162 | else //if (e.origin == 'caller') 163 | $scope.remotePlayer.engine.recvRemoteCandidate(e.candidate); 164 | // else 165 | // alert('Unknown candidate origin'); 166 | } 167 | 168 | else { 169 | alert('Unknown event: ' + e.name); 170 | } 171 | }); 172 | 173 | $scope.client.on('disconnect', function() { 174 | console.log('disconnected') 175 | $scope.isLoading = false; 176 | $scope.errorText = 'Disconnected from the server'; 177 | $scope.peers = []; 178 | $scope.$apply(); 179 | }); 180 | 181 | $scope.client.on('error', function(error, message) { 182 | console.log('connection error:', error, message) 183 | $scope.isLoading = false; 184 | $scope.errorText = 'Cannot connect to the server.'; 185 | $scope.$apply(); 186 | }); 187 | 188 | $scope.client.on('addPeer', function(peer) { 189 | console.log('add peer:', peer) 190 | 191 | if (peer.type == 'mediaserver') { 192 | $scope.mediaServerPeer = peer; 193 | 194 | // get a list of available files 195 | $scope.client.sendCommand({ 196 | node: 'streaming:files', 197 | to: peer 198 | }); 199 | } else { 200 | $scope.peers.push(peer); 201 | } 202 | $scope.$apply(); 203 | }); 204 | 205 | $scope.client.on('removePeer', function(peer) { 206 | console.log('remove peer:', peer) 207 | for (var i = 0; i < $scope.peers.length; i++) { 208 | if ($scope.peers[i].id === peer.id) { 209 | $scope.peers.splice(i,1); 210 | $scope.$apply(); 211 | break; 212 | } 213 | } 214 | }); 215 | 216 | // Init handle from URL if specified 217 | var handle = getHandleFromURL(); 218 | if (handle && handle.length) { 219 | $scope.handle = handle; 220 | $scope.login(); 221 | } 222 | 223 | 224 | // 225 | //= Session 226 | 227 | $scope.login = function() { 228 | if (!$scope.handle || $scope.handle.length < 3) { 229 | alert('Please enter 3 or more alphanumeric characters.'); 230 | return; 231 | } 232 | 233 | $scope.client.options.peer.user = $scope.handle; 234 | $scope.client.connect(); 235 | $scope.isLoading = true; 236 | // $scope.$apply(); // apply already in progress 237 | } 238 | 239 | 240 | // 241 | //= Messaging 242 | 243 | $scope.setMessageTarget = function(user) { 244 | console.log('setMessageTarget', user); 245 | $scope.directUser = user ? user : ''; 246 | $('#post-message .direct-user').text('@' + $scope.directUser); 247 | $('#post-message .message-text')[0].focus(); 248 | } 249 | 250 | $scope.sendMessage = function() { 251 | console.log('sendMessage', $scope.messageText); 252 | $scope.client.sendMessage({ 253 | data: $scope.messageText, 254 | to: $scope.directUser, 255 | direct: $scope.directUser 256 | }); 257 | $scope.messages.push({ 258 | to: $scope.directUser, 259 | direct: $scope.directUser, 260 | user: $scope.handle, 261 | data: $scope.messageText, 262 | time: Symple.formatTime(new Date) 263 | }); 264 | $scope.messageText = ""; 265 | }; 266 | 267 | 268 | // 269 | //= Media Streaming (Media Server) 270 | 271 | $scope.refreshStreamingFiles = function() { 272 | $scope.client.sendCommand({ 273 | node: 'streaming:files', 274 | to: $scope.mediaServerPeer 275 | }); 276 | }; 277 | 278 | $scope.startStreamingFile = function(file) { 279 | destroyPlayers($scope); 280 | 281 | $scope.currentStreamingFile = file; 282 | $scope.client.sendCommand({ 283 | node: 'streaming:start', 284 | to: $scope.mediaServerPeer, 285 | data: { 286 | file: file 287 | } 288 | }); 289 | }; 290 | 291 | $scope.stopStreamingFile = function(file) { 292 | destroyPlayers($scope); 293 | 294 | $scope.currentStreamingFile = null; 295 | $scope.client.sendCommand({ 296 | node: 'streaming:stop', 297 | to: $scope.mediaServerPeer, 298 | data: { 299 | file: file 300 | } 301 | }); 302 | }; 303 | 304 | $scope.startRecordingPeer = function(peer) { 305 | console.log('start recording peer:', peer); 306 | $scope.currentRecordingPeers.push(peer); 307 | $scope.$apply(); 308 | 309 | $scope.client.sendCommand({ 310 | node: 'recording:start', 311 | to: $scope.mediaServerPeer, 312 | data: { 313 | peer: peer 314 | } 315 | }); 316 | } 317 | 318 | $scope.stopRecordingPeer = function(peer) { 319 | console.log('stop recording peer:', peer); 320 | for (var i = 0; i < $scope.currentRecordingPeers.length; i++) { 321 | if ($scope.currentRecordingPeers[i].id === peer.id) { 322 | $scope.currentRecordingPeers.splice(i,1); 323 | $scope.$apply(); 324 | break; 325 | } 326 | } 327 | 328 | $scope.client.sendCommand({ 329 | node: 'recording:stop', 330 | to: $scope.mediaServerPeer, 331 | data: { 332 | peer: peer 333 | } 334 | }); 335 | }; 336 | 337 | 338 | // 339 | //= Video Call 340 | 341 | $scope.startVideoCall = function(user) { 342 | if (assertGetUserMedia()) { 343 | console.log('startVideoCall', user) 344 | if (user == $scope.handle) { 345 | alert('Cannot video chat with yourself. Please open a new browser window and login with a different handle.'); 346 | return; 347 | } 348 | 349 | destroyPlayers($scope); 350 | $scope.client.sendCommand({ 351 | node: 'call:init', 352 | to: user 353 | }); 354 | } 355 | } 356 | 357 | $scope.startLocalVideo = function() { 358 | if (assertGetUserMedia()) { 359 | 360 | // Init local video player 361 | $scope.localPlayer = createPlayer($scope, 'caller', '#video .local-video'); 362 | $scope.localPlayer.play({ localMedia: true, disableAudio: $scope.disableLocalAudio }); 363 | $scope.localVideoPlaying = true; 364 | } 365 | } 366 | 367 | 368 | // 369 | //= Helpers 370 | 371 | $scope.isLoggedIn = function() { 372 | return $scope.handle != null && $scope.client.online(); 373 | } 374 | 375 | $scope.hasMediaServer = function() { 376 | return !!$scope.mediaServerPeer; 377 | } 378 | 379 | $scope.getMessageClass = function(m) { 380 | if (m.direct) 381 | return 'list-group-item-warning'; 382 | return ''; 383 | } 384 | // } 385 | }); 386 | -------------------------------------------------------------------------------- /dashboard/assets/client.js: -------------------------------------------------------------------------------- 1 | // Define the `sympleApp` module 2 | var sympleApp = angular.module('sympleApp', []); 3 | 4 | // Define the `DashboardController` controller on the `sympleApp` module 5 | sympleApp.controller('DashboardController', function DashboardController($scope) { 6 | 7 | // 8 | //= Variables 9 | 10 | // Client 11 | $scope.client; 12 | $scope.handle; 13 | $scope.peers = []; 14 | 15 | // Interface 16 | $scope.errorText = ""; 17 | $scope.isLoading = false; 18 | 19 | // Messaging 20 | $scope.directUser; 21 | $scope.messages = []; 22 | $scope.messageText = ""; 23 | 24 | // Video Chat 25 | $scope.localPlayer; 26 | $scope.remotePlayer; 27 | $scope.remoteVideoPeer; 28 | $scope.disableLocalAudio = false; // set true to prevent local feedback loop 29 | 30 | // Media Server 31 | $scope.mediaServerPeer; 32 | $scope.streamableFiles = []; // list of streamable files that can be played 33 | $scope.currentStreamingFile; // current file being streamed 34 | 35 | // $(document).ready(function() { 36 | 37 | // 38 | //= Symple Client 39 | 40 | $scope.client = new Symple.Client(CLIENT_OPTIONS); 41 | 42 | $scope.client.on('announce', function(peer) { 43 | console.log('announce:', peer) 44 | 45 | $scope.client.join('public'); // join the public room 46 | $scope.isLoading = false; 47 | $scope.$apply(); 48 | }); 49 | 50 | $scope.client.on('presence', function(p) { 51 | console.log('presence:', p) 52 | }); 53 | 54 | $scope.client.on('message', function(m) { 55 | console.log('message:', m) 56 | 57 | // Normal Message 58 | if (!m.direct || m.direct == $scope.handle) { 59 | $scope.messages.push({ 60 | user: m.from.user, 61 | data: m.data, 62 | to: m.to, 63 | direct: m.direct, 64 | time: Symple.formatTime(new Date) 65 | }); 66 | $scope.$apply(); 67 | } 68 | else { 69 | console.log('dropping message:', m, m.direct) 70 | } 71 | }); 72 | 73 | $scope.client.on('command', function(c) { 74 | console.log('command:', c) 75 | 76 | if (c.node == 'call:init') { 77 | 78 | // Receive a call request 79 | if (!c.status) { 80 | 81 | // Show a dialog asking the user if they want to accept the call 82 | var e = $('#incoming-call-modal'); 83 | e.find('.caller').text('@' + c.from.user); 84 | e.find('.accept').unbind('click').click(function() { 85 | c.status = 200; 86 | $scope.remoteVideoPeer = c.from; 87 | $scope.client.respond(c); 88 | $scope.$apply(); 89 | e.modal('hide'); 90 | }) 91 | e.find('.reject').unbind('click').click(function() { 92 | c.status = 500; 93 | $scope.client.respond(c); 94 | e.modal('hide'); 95 | }) 96 | e.modal('show'); 97 | } 98 | 99 | // Handle call accepted 100 | else if (c.status == 200) { 101 | $scope.remoteVideoPeer = c.from; 102 | $scope.startLocalVideo(); 103 | $scope.$apply(); 104 | } 105 | 106 | // Handle call rejected or failure 107 | else if (c.status == 500) { 108 | alert('Call failed'); 109 | } 110 | else { 111 | alert('Unknown response status'); 112 | } 113 | } 114 | 115 | else if (c.node == 'streaming:start') { 116 | // Handle streaming start response 117 | if (c.status == 200) { 118 | $scope.remoteVideoPeer = c.from; 119 | $scope.$apply(); 120 | } 121 | } 122 | 123 | else if (c.node == 'streaming:files') { 124 | // Handle streaming filed response 125 | if (c.status == 200) { 126 | $scope.streamableFiles = c.data.files; 127 | $scope.$apply(); 128 | } 129 | } 130 | }); 131 | 132 | $scope.client.on('event', function(e) { 133 | console.log('event:', e) 134 | 135 | // Only handle events from the remoteVideoPeer 136 | if (!$scope.remoteVideoPeer || $scope.remoteVideoPeer.id != e.from.id) { 137 | console.log('mismatch event:', e.from, $scope.remoteVideoPeer) 138 | return 139 | } 140 | 141 | // ICE SDP 142 | if (e.name == 'ice:sdp') { 143 | if (e.sdp.type == 'offer') { 144 | 145 | // Create the remote player on offer 146 | if (!$scope.remotePlayer) { 147 | $scope.remotePlayer = createPlayer($scope, 'answerer', '#video .remote-video'); 148 | $scope.remotePlayer.play(); 149 | } 150 | $scope.remotePlayer.engine.recvRemoteSDP(e.sdp); 151 | } 152 | if (e.sdp.type == 'answer') { 153 | $scope.localPlayer.engine.recvRemoteSDP(e.sdp); 154 | } 155 | } 156 | 157 | // ICE Candidate 158 | else if (e.name == 'ice:candidate') { 159 | if (e.origin == 'answerer') 160 | $scope.localPlayer.engine.recvRemoteCandidate(e.candidate); 161 | else //if (e.origin == 'caller') 162 | $scope.remotePlayer.engine.recvRemoteCandidate(e.candidate); 163 | // else 164 | // alert('Unknown candidate origin'); 165 | } 166 | 167 | else { 168 | alert('Unknown event: ' + e.name); 169 | } 170 | }); 171 | 172 | $scope.client.on('disconnect', function() { 173 | console.log('disconnected') 174 | $scope.isLoading = false; 175 | $scope.errorText = 'Disconnected from the server'; 176 | $scope.peers = []; 177 | $scope.$apply(); 178 | }); 179 | 180 | $scope.client.on('error', function(error, message) { 181 | console.log('connection error:', error, message) 182 | $scope.isLoading = false; 183 | $scope.errorText = 'Cannot connect to the server.'; 184 | $scope.$apply(); 185 | }); 186 | 187 | $scope.client.on('addPeer', function(peer) { 188 | console.log('add peer:', peer) 189 | 190 | if (peer.type == 'mediaserver') { 191 | $scope.mediaServerPeer = peer; 192 | 193 | // get a list of available files 194 | $scope.client.sendCommand({ 195 | node: 'streaming:files', 196 | to: peer 197 | }); 198 | } else { 199 | $scope.peers.push(peer); 200 | } 201 | $scope.$apply(); 202 | }); 203 | 204 | $scope.client.on('removePeer', function(peer) { 205 | console.log('remove peer:', peer) 206 | for (var i = 0; i < $scope.peers.length; i++) { 207 | if ($scope.peers[i].id === peer.id) { 208 | $scope.peers.splice(i,1); 209 | $scope.$apply(); 210 | break; 211 | } 212 | } 213 | }); 214 | 215 | // Init handle from URL if specified 216 | var handle = getHandleFromURL(); 217 | if (handle && handle.length) { 218 | $scope.handle = handle; 219 | $scope.login(); 220 | } 221 | // }); 222 | 223 | // 224 | // Session 225 | 226 | $scope.login = function() { 227 | if (!$scope.handle || $scope.handle.length < 3) { 228 | alert('Please enter 3 or more alphanumeric characters.'); 229 | return; 230 | } 231 | 232 | $scope.client.options.peer.user = $scope.handle; 233 | $scope.client.connect(); 234 | $scope.isLoading = true; 235 | // $scope.$apply(); // apply already in progress 236 | } 237 | 238 | 239 | // 240 | // Messaging 241 | 242 | $scope.setMessageTarget = function(user) { 243 | console.log('setMessageTarget', user); 244 | $scope.directUser = user ? user : ''; 245 | $('#post-message .direct-user').text('@' + $scope.directUser); 246 | $('#post-message .message-text')[0].focus(); 247 | } 248 | 249 | $scope.sendMessage = function() { 250 | console.log('sendMessage', $scope.messageText); 251 | $scope.client.sendMessage({ 252 | data: $scope.messageText, 253 | to: $scope.directUser, 254 | direct: $scope.directUser 255 | }); 256 | $scope.messages.push({ 257 | to: $scope.directUser, 258 | direct: $scope.directUser, 259 | user: $scope.handle, 260 | data: $scope.messageText, 261 | time: Symple.formatTime(new Date) 262 | }); 263 | $scope.messageText = ""; 264 | }; 265 | 266 | 267 | // 268 | // Media Streaming (Media Server) 269 | 270 | $scope.refreshStreamingFiles = function() { 271 | $scope.client.sendCommand({ 272 | node: 'streaming:files', 273 | to: $scope.mediaServerPeer 274 | }); 275 | }; 276 | 277 | $scope.startStreamingFile = function(file) { 278 | destroyPlayers($scope); 279 | 280 | $scope.currentStreamingFile = file; 281 | $scope.client.sendCommand({ 282 | node: 'streaming:start', 283 | to: $scope.mediaServerPeer, 284 | data: { 285 | file: file 286 | } 287 | }); 288 | }; 289 | 290 | $scope.stopStreamingFile = function(file) { 291 | destroyPlayers($scope); 292 | 293 | $scope.currentStreamingFile = null; 294 | $scope.client.sendCommand({ 295 | node: 'streaming:stop', 296 | to: $scope.mediaServerPeer, 297 | data: { 298 | file: file 299 | } 300 | }); 301 | }; 302 | 303 | 304 | // 305 | // Video Call 306 | 307 | $scope.startVideoCall = function(user) { 308 | if (assertGetUserMedia()) { 309 | console.log('startVideoCall', user) 310 | if (user == $scope.handle) { 311 | alert('Cannot video chat with yourself. Please open a new browser window and login with a different handle.'); 312 | return; 313 | } 314 | 315 | destroyPlayers($scope); 316 | $scope.client.sendCommand({ 317 | node: 'call:init', 318 | to: user 319 | }); 320 | } 321 | } 322 | 323 | $scope.startLocalVideo = function() { 324 | if (assertGetUserMedia()) { 325 | 326 | // Init local video player 327 | $scope.localPlayer = createPlayer($scope, 'caller', '#video .local-video'); 328 | $scope.localPlayer.play({ localMedia: true, disableAudio: $scope.disableLocalAudio }); 329 | $scope.localVideoPlaying = true; 330 | } 331 | } 332 | 333 | 334 | // 335 | // Helpers 336 | 337 | $scope.isLoggedIn = function() { 338 | return $scope.handle != null && $scope.client.online(); 339 | } 340 | 341 | $scope.hasMediaServer = function() { 342 | return !!$scope.mediaServerPeer; 343 | } 344 | 345 | $scope.getMessageClass = function(m) { 346 | if (m.direct) 347 | return 'list-group-item-warning'; 348 | return ''; 349 | } 350 | // } 351 | }); 352 | -------------------------------------------------------------------------------- /dashboard/assets/helpers.js: -------------------------------------------------------------------------------- 1 | function createPlayer($scope, origin, selector) { 2 | var player = new Symple.Player({ 3 | element: selector, 4 | engine: 'WebRTC', 5 | rtcConfig: WEBRTC_CONFIG, 6 | mediaConstraints: { 7 | 'mandatory': { 8 | 'OfferToReceiveAudio':true, 9 | 'OfferToReceiveVideo':true 10 | } 11 | }, 12 | onStateChange: function(player, state) { 13 | player.displayStatus(state); 14 | } 15 | }); 16 | player.setup(); 17 | player.engine.sendLocalSDP = function(desc) { 18 | $scope.client.send({ 19 | name: 'ice:sdp', 20 | to: $scope.remoteVideoPeer, 21 | origin: origin, 22 | type: 'event', 23 | sdp: desc 24 | }) 25 | } 26 | player.engine.sendLocalCandidate = function(cand) { 27 | $scope.client.send({ 28 | name: 'ice:candidate', 29 | to: $scope.remoteVideoPeer, 30 | origin: origin, 31 | type: 'event', 32 | candidate: cand 33 | }) 34 | } 35 | return player; 36 | } 37 | 38 | function destroyPlayers($scope) { 39 | if ($scope.remotePlayer) { 40 | $scope.remotePlayer.destroy() 41 | $scope.remotePlayer = null; 42 | } 43 | if ($scope.localPlayer) { 44 | $scope.localPlayer.destroy() 45 | $scope.localPlayer = null; 46 | $scope.localVideoPlaying = false; 47 | } 48 | $scope.remoteVideoPeer = null; 49 | $scope.$apply(); 50 | } 51 | 52 | function getHandleFromURL() { 53 | return location.search.split('handle=')[1] ? location.search.split('handle=')[1] : ''; 54 | } 55 | 56 | function assertGetUserMedia() { 57 | if (navigator.getUserMedia || navigator.webkitGetUserMedia || 58 | navigator.mozGetUserMedia || navigator.msGetUserMedia) { 59 | return true; 60 | } 61 | else { 62 | alert('getUserMedia() is not supported in your browser. Please upgrade to the latest Chrome or Firefox.'); 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /dashboard/index.ejs: -------------------------------------------------------------------------------- 1 | 2 |
3 |