├── .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 | Symple WebRTC Live Video Chat Demo 4 | 5 | 6 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |
52 |
53 |
54 | 55 |
56 | 57 | 62 | 63 |
You have been disconnected from the server
64 | 65 |
66 |
67 | 68 |
69 |
70 |

Login

71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 |

People

88 |
89 |
    90 |
  • Please login...
  • 91 |
  • 92 |
    93 | 94 | 95 | 96 | 97 | 98 | 99 | 106 |
    107 | @{{peer.user}} 108 | 112 |
  • 113 |
114 |
115 |
116 | 117 |
118 |
119 |
120 |

Recordings

121 |
122 |
    123 |
  • Please login...
  • 124 |
  • 125 | {{file}} 126 |
    127 | 128 | 129 | 136 |
    137 | 142 |
  • 143 |
144 |
145 |
146 |
147 | 148 |
149 | 150 |
151 |
152 |
153 |

Video

154 |
155 |
156 |
157 |
158 |
159 | 160 |
161 |
162 |
163 |
164 |
165 |
166 | 167 | 187 |
188 | 189 |
190 |
191 |
192 |

Conversation

193 |
194 |
    195 |
  • 196 |
    197 |
    198 | 199 | 200 | 207 | 208 | 209 | 210 | 211 | 212 |
    213 |
    214 |
  • 215 |
  • 216 | {{message.time}} 217 | @{{message.user}} 218 | {{message.data}} 219 |
  • 220 |
221 |
222 |
223 |
224 |
225 | 226 | 233 |
234 | 235 | 236 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symple-webrtc-video-chat-demo", 3 | "title": "Symple WebRTC Video Chat Demo", 4 | "version": "0.6.1", 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "dependencies": { 9 | "symple" : "*", 10 | "symple-client" : "*", 11 | "symple-client-player" : "*", 12 | "express" : "~4.13.3", 13 | "ejs" : "~2.3.4", 14 | "redis" : "~2.4.2" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/sourcey/symple-webrtc-video-chat-demo.git" 19 | }, 20 | "keywords": [ 21 | "symple", 22 | "server", 23 | "realtime", 24 | "messaging", 25 | "websocket", 26 | "streaming", 27 | "webrtc", 28 | "chat" 29 | ], 30 | "author": "Kam Low (http://sourcey.com)", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/sourcey/symple-webrtc-video-chat-demo/issues" 34 | }, 35 | "homepage": "http://sourcey.com/symple" 36 | } 37 | -------------------------------------------------------------------------------- /dashboard/symple.json: -------------------------------------------------------------------------------- 1 | { 2 | /* The port to listen on */ 3 | "port" : 4551, 4 | 5 | /* Allow anonymous connections */ 6 | "anonymous" : true, 7 | 8 | /* Session ttl in minutes 9 | "sessionTtl" : 15, */ 10 | 11 | /* Redis configuration 12 | "redis" : { 13 | "host" : "localhost", 14 | "port" : 6379 15 | } */ 16 | } 17 | -------------------------------------------------------------------------------- /server/main.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | // 3 | // LibSourcey 4 | // Copyright (c) 2005, Sourcey 5 | // 6 | // SPDX-License-Identifier: LGPL-2.1+ 7 | // 8 | /// 9 | 10 | 11 | #include "scy/idler.h" 12 | #include "scy/logger.h" 13 | #include "signaler.h" 14 | 15 | #include "webrtc/base/ssladapter.h" 16 | #include "webrtc/base/thread.h" 17 | 18 | 19 | using std::endl; 20 | using namespace scy; 21 | 22 | 23 | // Test this demo with the code in the `client` directory 24 | 25 | 26 | #define SERVER_HOST "localhost" 27 | #define USE_SSL 0 // 1 28 | #if USE_SSL 29 | #define SERVER_PORT 443 30 | #else 31 | #define SERVER_PORT 4551 32 | #endif 33 | 34 | 35 | int main(int argc, char** argv) 36 | { 37 | Logger::instance().add(new ConsoleChannel("debug", LTrace)); // LTrace 38 | 39 | #if USE_SSL 40 | SSLManager::initNoVerifyClient(); 41 | #endif 42 | 43 | // Setup WebRTC environment 44 | rtc::LogMessage::LogToDebug(rtc::LERROR); 45 | rtc::LogMessage::LogTimestamps(); 46 | rtc::LogMessage::LogThreads(); 47 | 48 | rtc::InitializeSSL(); 49 | 50 | { 51 | smpl::Client::Options options; 52 | options.host = SERVER_HOST; 53 | options.port = SERVER_PORT; 54 | options.name = "Media Server"; 55 | options.user = "mediaserver"; 56 | options.type = "mediaserver"; 57 | 58 | // NOTE: The server must enable anonymous authentication for this demo. 59 | // options.token = ""; token based authentication 60 | 61 | Signaler app(options); 62 | 63 | auto rtcthread = rtc::Thread::Current(); 64 | Idler rtc([=]() { 65 | // TraceL << "Running WebRTC loop" << endl; 66 | rtcthread->ProcessMessages(10); 67 | }); 68 | 69 | app.waitForShutdown(); 70 | } 71 | 72 | #if USE_SSL 73 | net::SSLManager::destroy(); 74 | #endif 75 | rtc::CleanupSSL(); 76 | Logger::destroy(); 77 | 78 | return 0; 79 | } 80 | -------------------------------------------------------------------------------- /server/signaler.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | // 3 | // LibSourcey 4 | // Copyright (c) 2005, Sourcey 5 | // 6 | // SPDX-License-Identifier: LGPL-2.1+ 7 | // 8 | /// 9 | 10 | 11 | #include "signaler.h" 12 | 13 | #include "scy/av/codec.h" 14 | #include "scy/av/format.h" 15 | #include "scy/filesystem.h" 16 | #include "scy/util.h" 17 | 18 | #include 19 | #include 20 | 21 | 22 | #define OUTPUT_FILENAME "webrtcrecorder.mp4" 23 | #define OUTPUT_FORMAT \ 24 | av::Format("MP4", "mp4", \ 25 | av::VideoCodec("H.264", "libx264", 400, 300, 25, 48000, 128000, "yuv420p"), \ 26 | av::AudioCodec("AAC", "libfdk_aac", 2, 44100, 64000, "s16")); 27 | 28 | 29 | using std::endl; 30 | 31 | 32 | namespace scy { 33 | 34 | 35 | Signaler::Signaler(const smpl::Client::Options& options) 36 | : _client(options) 37 | { 38 | _client.StateChange += slot(this, &Signaler::onClientStateChange); 39 | _client.roster().ItemAdded += slot(this, &Signaler::onPeerConnected); 40 | _client.roster().ItemRemoved += slot(this, &Signaler::onPeerDiconnected); 41 | _client += packetSlot(this, &Signaler::onPeerCommand); 42 | _client += packetSlot(this, &Signaler::onPeerEvent); 43 | _client += packetSlot(this, &Signaler::onPeerMessage); 44 | _client.connect(); 45 | } 46 | 47 | 48 | Signaler::~Signaler() 49 | { 50 | } 51 | 52 | 53 | void Signaler::sendSDP(PeerConnection* conn, const std::string& type, 54 | const std::string& sdp) 55 | { 56 | assert(type == "offer" || type == "answer"); 57 | smpl::Event e; 58 | e.setName("ice:sdp"); 59 | auto& desc = e["sdp"]; 60 | desc[kSessionDescriptionTypeName] = type; 61 | desc[kSessionDescriptionSdpName] = sdp; 62 | 63 | postMessage(e); 64 | } 65 | 66 | 67 | void Signaler::sendCandidate(PeerConnection* conn, const std::string& mid, 68 | int mlineindex, const std::string& sdp) 69 | { 70 | smpl::Event e; 71 | e.setName("ice:candidate"); 72 | auto& desc = e["candidate"]; 73 | desc[kCandidateSdpMidName] = mid; 74 | desc[kCandidateSdpMlineIndexName] = mlineindex; 75 | desc[kCandidateSdpName] = sdp; 76 | 77 | postMessage(e); 78 | } 79 | 80 | 81 | void Signaler::onPeerConnected(smpl::Peer& peer) 82 | { 83 | if (peer.id() == _client.ourID()) 84 | return; 85 | DebugL << "Peer connected: " << peer.id() << endl; 86 | 87 | if (PeerConnectionManager::exists(peer.id())) { 88 | DebugL << "Peer already has a session: " << peer.id() << endl; 89 | } 90 | } 91 | 92 | 93 | void Signaler::onPeerCommand(smpl::Command& c) 94 | { 95 | DebugL << "Peer command: " << c.from().toString() << endl; 96 | 97 | // List available files for streaming 98 | if (c.node() == "streaming:files") { 99 | json::Value files; 100 | StringVec nodes; 101 | fs::readdir(getDataDirectory(), nodes); 102 | for (auto node : nodes) { 103 | files.append(node); 104 | } 105 | 106 | c.setData("files", files); 107 | c.setStatus(200); 108 | _client.respond(c); 109 | } 110 | 111 | // Start a streaming session 112 | else if (c.node() == "streaming:start") { 113 | std::string file = c.data("file").asString(); 114 | std::string filePath(getDataDirectory()); 115 | fs::addnode(filePath, file); 116 | auto conn = new StreamingPeerConnection(this, c.from().id, c.id(), filePath); 117 | auto stream = conn->createMediaStream(); 118 | conn->createConnection(); 119 | conn->createOffer(); 120 | PeerConnectionManager::add(c.id(), conn); 121 | 122 | c.setStatus(200); 123 | _client.respond(c); 124 | // _client.persistence().add(c.id(), reinterpret_cast(c.clone()), 0); 125 | } 126 | 127 | // Start a recording session 128 | else if (c.node() == "recording:start") { 129 | 130 | av::EncoderOptions options; 131 | options.ofile = OUTPUT_FILENAME; 132 | options.oformat = OUTPUT_FORMAT; 133 | 134 | auto conn = new RecordingPeerConnection(this, c.from().id, c.id(), options); 135 | conn->constraints().SetMandatoryReceiveVideo(true); 136 | conn->constraints().SetMandatoryReceiveAudio(true); 137 | conn->createConnection(); 138 | PeerConnectionManager::add(c.id(), conn); 139 | 140 | c.setStatus(200); 141 | _client.respond(c); 142 | // _client.persistence().add(c.id(), reinterpret_cast(c.clone()), 0); 143 | } 144 | } 145 | 146 | 147 | void Signaler::onPeerEvent(smpl::Event& e) 148 | { 149 | DebugL << "Peer event: " << e.from().toString() << endl; 150 | 151 | if (e.name() == "ice:sdp") { 152 | recvSDP(e.from().id, e["sdp"]); 153 | } 154 | else if (e.name() == "ice:candidate") { 155 | recvCandidate(e.from().id, e["candidate"]); 156 | } 157 | } 158 | 159 | 160 | void Signaler::onPeerMessage(smpl::Message& m) 161 | { 162 | DebugL << "Peer message: " << m.from().toString() << endl; 163 | } 164 | 165 | 166 | void Signaler::onPeerDiconnected(const smpl::Peer& peer) 167 | { 168 | DebugL << "Peer disconnected" << endl; 169 | 170 | // TODO: Loop all and close for peer 171 | 172 | // auto conn = get(peer.id()); 173 | // if (conn) { 174 | // DebugL << "Closing peer connection: " << peer.id() << endl; 175 | // conn->closeConnection(); // will be deleted via callback 176 | // } 177 | } 178 | 179 | 180 | void Signaler::onClientStateChange(void* sender, sockio::ClientState& state, 181 | const sockio::ClientState& oldState) 182 | { 183 | DebugL << "Client state changed from " << oldState << " to " << state << endl; 184 | 185 | switch (state.id()) { 186 | case sockio::ClientState::Connecting: 187 | break; 188 | case sockio::ClientState::Connected: 189 | break; 190 | case sockio::ClientState::Online: 191 | break; 192 | case sockio::ClientState::Error: 193 | throw std::runtime_error("Cannot connect to Symple server. " 194 | "Did you start the demo app and the " 195 | "Symple server is running on port 4551?"); 196 | } 197 | } 198 | 199 | 200 | void Signaler::onAddRemoteStream(PeerConnection* conn, webrtc::MediaStreamInterface* stream) 201 | { 202 | } 203 | 204 | 205 | void Signaler::onRemoveRemoteStream(PeerConnection* conn, webrtc::MediaStreamInterface* stream) 206 | { 207 | assert(0 && "free streams"); 208 | } 209 | 210 | 211 | void Signaler::onStable(PeerConnection* conn) 212 | { 213 | } 214 | 215 | 216 | void Signaler::onClosed(PeerConnection* conn) 217 | { 218 | // _recorder.reset(); // shutdown the recorder 219 | PeerConnectionManager::onClosed(conn); 220 | } 221 | 222 | 223 | void Signaler::onFailure(PeerConnection* conn, const std::string& error) 224 | { 225 | // _recorder.reset(); // shutdown the recorder 226 | PeerConnectionManager::onFailure(conn, error); 227 | } 228 | 229 | 230 | void Signaler::postMessage(const smpl::Message& m) 231 | { 232 | _ipc.push(new ipc::Action( 233 | std::bind(&Signaler::syncMessage, this, std::placeholders::_1), 234 | m.clone())); 235 | } 236 | 237 | 238 | void Signaler::syncMessage(const ipc::Action& action) 239 | { 240 | auto m = reinterpret_cast(action.arg); 241 | _client.send(*m); 242 | delete m; 243 | } 244 | 245 | 246 | std::string Signaler::getDataDirectory() const 247 | { 248 | // TODO: Make configurable 249 | std::string dir(getCwd()); 250 | fs::addnode(dir, "data"); 251 | return dir; 252 | } 253 | 254 | 255 | } // namespace scy 256 | -------------------------------------------------------------------------------- /server/signaler.h: -------------------------------------------------------------------------------- 1 | /// 2 | // 3 | // LibSourcey 4 | // Copyright (c) 2005, Sourcey 5 | // 6 | // SPDX-License-Identifier: LGPL-2.1+ 7 | // 8 | /// 9 | 10 | 11 | #ifndef SCY_WebRTC_Signaler_H 12 | #define SCY_WebRTC_Signaler_H 13 | 14 | 15 | #include "scy/application.h" 16 | #include "scy/ipc.h" 17 | #include "scy/net/sslmanager.h" 18 | #include "scy/net/sslsocket.h" 19 | #include "scy/symple/client.h" 20 | #include "scy/webrtc/peerconnectionmanager.h" 21 | #include "scy/webrtc/streamingpeerconnection.h" 22 | #include "scy/webrtc/recordingpeerconnection.h" 23 | 24 | 25 | namespace scy { 26 | 27 | 28 | class Signaler : public PeerConnectionManager, public Application 29 | { 30 | public: 31 | Signaler(const smpl::Client::Options& options); 32 | ~Signaler(); 33 | 34 | protected: 35 | 36 | /// PeerConnectionManager interface 37 | void sendSDP(PeerConnection* conn, const std::string& type, const std::string& sdp); 38 | void sendCandidate(PeerConnection* conn, const std::string& mid, int mlineindex, const std::string& sdp); 39 | void onAddRemoteStream(PeerConnection* conn, webrtc::MediaStreamInterface* stream); 40 | void onRemoveRemoteStream(PeerConnection* conn, webrtc::MediaStreamInterface* stream); 41 | void onStable(PeerConnection* conn); 42 | void onClosed(PeerConnection* conn); 43 | void onFailure(PeerConnection* conn, const std::string& error); 44 | 45 | void postMessage(const smpl::Message& m); 46 | void syncMessage(const ipc::Action& action); 47 | 48 | void onPeerConnected(smpl::Peer& peer); 49 | void onPeerCommand(smpl::Command& m); 50 | void onPeerEvent(smpl::Event& m); 51 | void onPeerMessage(smpl::Message& m); 52 | void onPeerDiconnected(const smpl::Peer& peer); 53 | 54 | void onClientStateChange(void* sender, sockio::ClientState& state, const sockio::ClientState& oldState); 55 | 56 | std::string getDataDirectory() const; 57 | 58 | protected: 59 | #if USE_SSL 60 | smpl::SSLClient _client; 61 | #else 62 | smpl::TCPClient _client; 63 | #endif 64 | ipc::SyncQueue<> _ipc; 65 | }; 66 | 67 | 68 | } // namespace scy 69 | 70 | 71 | #endif 72 | --------------------------------------------------------------------------------