├── .gitignore ├── .npmrc ├── README.md ├── broadcast.html ├── images └── watch.jpg ├── index.html ├── lib └── simple-webrtc.js ├── ng-simple-webrtc.js ├── package.json ├── styles.css └── watch.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng-simple-webrtc 2 | 3 | > AngularJS wrapper for SimpleWebRTC client from https://simplewebrtc.com/ 4 | 5 | [![NPM][npm-icon] ][npm-url] 6 | [![Circle CI][circle-ci-icon] ][circle-ci-url] 7 | 8 | AngularJS client for starting video / broadcasting to multiple peers via WebRTC, built 9 | on top of the [SimpleWebRTC](https://simplewebrtc.com/) code. 10 | 11 | This example wraps the calls to the WebRTC library into 2 simple Angular directives: a broadcaster 12 | and a room watcher. A single broadcasted stream can be viewed by multiple watchers in a single room. 13 | 14 | ## Demo 15 | 16 | To run the demo locally, clone the repo or install from NPM, then 17 | 18 | npm start 19 | 20 | Open the broadcast page `localhost:3400/broadcast.html` and create a room. Open `localhost:3400/watch.html` 21 | in a separate browser tab and enter the same room name. You should see the broadcasted picture. 22 | 23 | ![watch screenshot](images/watch.jpg) 24 | 25 | ## Install 26 | 27 | npm install ng-simple-webrtc --save 28 | 29 | Include the script tags, at least Angular and SimpleWebRTC before this module 30 | 31 | ```html 32 | 33 | 34 | 35 | ``` 36 | 37 | Add `SimpleWebRTC` to the list of your application's module dependencies 38 | 39 | ```js 40 | angular.module('WatchApp', ['SimpleWebRTC']) 41 | // adds custom directive 42 | ``` 43 | 44 | ## Broadcast a room 45 | 46 | Use directive `broadcaster` to connect to the local camera and broadcast the picture. 47 | Communicate the room name, and see the status properties (`hasStream`, `isBroadcasting`) via 48 | isolate scope's properties. 49 | 50 | ```html 51 | 57 | ``` 58 | 59 | You can control camera mirror display by setting "true" or "false" value of the `mirror` attribute. 60 | 61 | To connect to the camera and start a room, broadcast events `prepare` and `start` 62 | 63 | ```html 64 |
65 | 69 | 70 | 71 |
72 | 73 | 74 |
75 |
76 | ``` 77 | ```js 78 | angular.module('BroadcastApp', ['SimpleWebRTC']) 79 | .controller('BroadcastAppController', function ($scope) { 80 | $scope.hasStream = false; 81 | $scope.roomName = ''; 82 | $scope.isBroadcasting = ''; 83 | $scope.prepare = function prepare() { 84 | $scope.$broadcast('prepare'); 85 | }; 86 | $scope.start = function start() { 87 | $scope.$broadcast('start'); 88 | }; 89 | }); 90 | ``` 91 | 92 | See file [broadcast.html](broadcast.html) for the full demo 93 | 94 | When local video starts, the directive broadcasts 'video-resolution' event with width and height 95 | of the captured video stream. 96 | 97 | ## Watch a room 98 | 99 | To join and watch a room (without broadcasting anything yourself) use `watch-room` directive. 100 | You can pass the room name and see the status via isolate scope attributes 101 | 102 | ```html 103 | 104 | 110 | ``` 111 | 112 | `maxAllowedWatchers` property controls how many people can be in the room when joining, 113 | default 10. If more than that, the watcher will leave the room, emitting a message `room-full`. 114 | 115 | `nick` is an optional property sent to the remote group on disconnect. 116 | 117 | You can start watching (join a room) and stop watching (leave a room) by broadcasting 118 | an event 119 | 120 | ```html 121 |
122 | 123 | 124 | 125 | 126 |
127 | ``` 128 | 129 | ```js 130 | angular.module('WatchApp', ['SimpleWebRTC']) 131 | .controller('WatchAppController', function ($scope) { 132 | $scope.roomName = ''; 133 | $scope.joinedRoom = false; 134 | $scope.joinRoom = function () { 135 | $scope.$broadcast('joinRoom'); 136 | }; 137 | $scope.leaveRoom = function () { 138 | $scope.$broadcast('leaveRoom'); 139 | }; 140 | }); 141 | ``` 142 | 143 | See the included file [watch.html](watch.html) as an example 144 | 145 | ## Custom video list 146 | 147 | You can supply an array to hold all videos and handle the layout by your self instead of appended by the library. 148 | You must provide an empty array to initialize it and pass it to the `video-list` attribute to `broadcaster` or `watch-room` directives (or both). 149 | 150 | ```html 151 | 152 | ``` 153 | 154 | ```js 155 | angular.module('WatchApp', ['SimpleWebRTC']) 156 | .controller('WatchAppController', function ($scope) { 157 | $scope.roomName = ''; 158 | $scope.joinedRoom = false; 159 | $scope.videoList = []; // initialize videoList variable to hold all videos coming to watch-room directive 160 | $scope.joinRoom = function () { 161 | $scope.$broadcast('joinRoom'); 162 | }; 163 | $scope.leaveRoom = function () { 164 | $scope.$broadcast('leaveRoom'); 165 | }; 166 | }); 167 | ``` 168 | 169 | ## Details 170 | 171 | The `webrtc` object created by the `SimpleWebRTC` library is attached to the `$rootScope`. 172 | 173 | To broadcast a message to all peers in the room via RTC data channel, use `messageAll` event. 174 | 175 | ```js 176 | $scope.sendMessage = function sendMessage() { 177 | $scope.$broadcast('messageAll', { 178 | from: 'username', 179 | text: 'hi there' 180 | }); 181 | }; 182 | ``` 183 | 184 | Each peer will receive the message via 'channelMessage' event. The event will have 2 arguments: `peer` and `message`. 185 | 186 | ```js 187 | $scope.$on('channelMessage', function (event, peer, message) { 188 | console.log('message', message); 189 | }); 190 | ``` 191 | 192 | The `message` is automatically JSON stringified and parsed when sent. 193 | 194 | If the page has global variable `ngSimpleWebRTC`, certain options will be added to the simple webrtc options during creation. ngSimpleWebRTC.peerConnectionConfig is Useful for paid ICE/STUN/TURN services, 195 | see for example [xirsys.com](http://xirsys.com/simplewebrtc/) documentation. You may also set `debug` and `socketio` configuration this way. 196 | 197 | ### Small print 198 | 199 | Author: Gleb Bahmutov © 2015 200 | 201 | * [@bahmutov](https://twitter.com/bahmutov) 202 | * [glebbahmutov.com](http://glebbahmutov.com) 203 | * [blog](http://glebbahmutov.com/blog/) 204 | 205 | License: MIT - do anything with the code, but don't blame me if it does not work. 206 | 207 | Spread the word: tweet, star on github, etc. 208 | 209 | Support: if you find any problems with this module, email / tweet / 210 | [open issue](https://github.com/bahmutov/ng-simple-webrtc/issues) on Github 211 | 212 | ## MIT License 213 | 214 | Copyright (c) 2015 Gleb Bahmutov 215 | 216 | Permission is hereby granted, free of charge, to any person 217 | obtaining a copy of this software and associated documentation 218 | files (the "Software"), to deal in the Software without 219 | restriction, including without limitation the rights to use, 220 | copy, modify, merge, publish, distribute, sublicense, and/or sell 221 | copies of the Software, and to permit persons to whom the 222 | Software is furnished to do so, subject to the following 223 | conditions: 224 | 225 | The above copyright notice and this permission notice shall be 226 | included in all copies or substantial portions of the Software. 227 | 228 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 229 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 230 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 231 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 232 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 233 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 234 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 235 | OTHER DEALINGS IN THE SOFTWARE. 236 | 237 | [npm-icon]: https://nodei.co/npm/ng-simple-webrtc.png?downloads=true 238 | [npm-url]: https://npmjs.org/package/ng-simple-webrtc 239 | [circle-ci-icon]: https://circleci.com/gh/bahmutov/ng-simple-webrtc.svg?style=svg 240 | [circle-ci-url]: https://circleci.com/gh/bahmutov/ng-simple-webrtc 241 | -------------------------------------------------------------------------------- /broadcast.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Simple Angular broadcaster via WebRTC

12 | 13 |
14 | 15 | 16 | 24 | 25 |
26 |

Start my own room

27 | 28 | 29 | 30 |

Broadcasting. To watch connect to this server and open 31 | watch.html page. 32 | Enter the same room "{{ roomName }}" and watch this video stream.

33 | 34 |

Message to peers 35 | in the room 36 |

37 | 38 |
39 | 40 | 41 | 42 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /images/watch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/ng-simple-webrtc/1343a04b7165b51af718eeca35ba043296b648ba/images/watch.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Angular wrapper around the simple example from simplewebrtc.com/. 9 | First start broadcasting from a new room using broadcast.html. Then 10 | connect as many watchers as needed from watch.html page. 11 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /ng-simple-webrtc.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | if (!angular) { 4 | throw new Error('Missing Angular library'); 5 | } 6 | 7 | angular.module('SimpleWebRTC', []) 8 | .run(function () { 9 | if (typeof SimpleWebRTC === 'undefined') { 10 | throw new Error('Cannot find SimpleWebRTC code'); 11 | } 12 | }) 13 | .directive('watchRoom', function () { 14 | return { 15 | template: '
' + 16 | '
' + 17 | '
', 18 | scope: { 19 | roomName: '=', 20 | joinedRoom: '=', 21 | videoList: '=', 22 | maxNumPeers: '=', 23 | nick: '=' 24 | }, 25 | link: function (scope, element, attr) { 26 | scope.muted = attr.muted === 'true'; 27 | }, 28 | controller: function ($scope, $rootScope) { 29 | var webrtc, watchingVideo; 30 | 31 | $scope.maxNumPeers = typeof $scope.maxNumPeers === 'number' ? 32 | $scope.maxNumPeers : 10; 33 | 34 | function formRTCOptions() { 35 | var webrtcOptions = { 36 | autoRequestMedia: false, 37 | debug: false, 38 | nick: $scope.nick, 39 | receiveMedia: { // FIXME: remove old chrome <= 37 constraints format 40 | mandatory: { 41 | OfferToReceiveAudio: false, 42 | OfferToReceiveVideo: true 43 | } 44 | } 45 | }; 46 | grabExtraWebRTCOptions(webrtcOptions); 47 | return webrtcOptions; 48 | } 49 | 50 | function postCreationRTCOptions(webrtc) { 51 | } 52 | 53 | function rtcEventResponses(webrtc) { 54 | webrtc.on('readyToCall', function () { 55 | console.log('webrtc ready to call'); 56 | }); 57 | 58 | webrtc.on('joinedRoom', function (name) { 59 | console.log('joined room "%s"', name); 60 | 61 | var peers = webrtc.getPeers(); 62 | if (peers && Array.isArray(peers) && 63 | peers.length > $scope.maxNumPeers) { 64 | console.error('Too many people in the room, leaving'); 65 | webrtc.leaveRoom(); 66 | $scope.$emit('room-full'); 67 | return; 68 | } 69 | 70 | $scope.$emit('joinedRoom', name); 71 | 72 | webrtc.on('channelMessage', function (peer, message) { 73 | console.log('received channel message "%s" from peer "%s"', 74 | message, peer.nick || peer.id); 75 | $scope.$emit('channelMessage', peer, JSON.parse(message)); 76 | $scope.$apply(); 77 | }); 78 | }); 79 | $scope.$on('messageAll', function (event, message) { 80 | if (message && webrtc) { 81 | webrtc.sendDirectlyToAll(JSON.stringify(message)); 82 | } 83 | }); 84 | webrtc.on('videoRemoved', function (video, peer) { 85 | if (Array.isArray($scope.videoList)) { 86 | for (var i = 0; i < $scope.videoList.length; i++) { 87 | if (video.id === $scope.videoList[i].id) { 88 | $scope.videoList.splice(i, 1); 89 | $scope.$apply(); 90 | return; 91 | } 92 | } 93 | } 94 | }); 95 | webrtc.on('videoAdded', function (video, peer) { 96 | console.log('video added from peer nickname', peer.nick); 97 | if ($scope.muted) { 98 | video.setAttribute('muted', true); 99 | video.setAttribute('hidden', true); 100 | } 101 | 102 | // videoList is an array, it means the user wants to append the video in it 103 | // so, skip manual addition to dom 104 | if (Array.isArray($scope.videoList)) { 105 | video.isRemote = true; 106 | $scope.videoList.push(video); 107 | $scope.joinedRoom = true; 108 | $scope.$apply(); 109 | return; 110 | } 111 | 112 | var remotes = document.getElementById('remotes'); 113 | remotes.appendChild(video); 114 | watchingVideo = video; 115 | 116 | $scope.$emit('videoAdded', video); 117 | $scope.joinedRoom = true; 118 | $scope.$apply(); 119 | }); 120 | 121 | webrtc.on('iceFailed', function (peer) { 122 | console.error('ice failed', peer); 123 | $scope.$emit('iceFailed', peer); 124 | }); 125 | 126 | webrtc.on('connectivityError', function (peer) { 127 | console.error('connectivity error', peer); 128 | $scope.$emit('connectivityError', peer); 129 | }); 130 | 131 | $scope.$on('leaveRoom', function leaveRoom() { 132 | console.log('leaving room', $scope.roomName); 133 | if (!$scope.roomName) { 134 | return; 135 | } 136 | 137 | webrtc.leaveRoom($scope.roomName); 138 | 139 | if (watchingVideo) { 140 | var remotes = document.getElementById('remotes'); 141 | remotes.removeChild(watchingVideo); 142 | } 143 | $scope.joinedRoom = false; 144 | }); 145 | } 146 | 147 | // emit this event, and we join the room. 148 | $scope.$on('joinRoom', function joinRoom() { 149 | console.log('joining room', $scope.roomName); 150 | if (!$scope.roomName) { 151 | return; 152 | } 153 | 154 | var webrtcOptions = formRTCOptions(); 155 | webrtc = new SimpleWebRTC(webrtcOptions); 156 | postCreationRTCOptions(webrtc); 157 | $rootScope.webrtc = webrtc; 158 | $scope.$emit('haveWebRTC'); 159 | rtcEventResponses(webrtc); 160 | 161 | 162 | // Post WebRTC Options 163 | // And, a joinRoom command. 164 | 165 | webrtc.mute(); 166 | webrtc.joinRoom($scope.roomName); 167 | }); 168 | } 169 | } 170 | }) 171 | 172 | // ==================================================================================================================================== 173 | 174 | .directive('broadcaster', function () { 175 | return { 176 | template: '

My video

' + 177 | '
' + 178 | '' + 179 | '
', 180 | scope: { 181 | hasStream: '=', 182 | roomName: '=', 183 | isBroadcasting: '=', 184 | sourceId: '=', 185 | minWidth: '=', 186 | minHeight: '=', 187 | videoList: '=', 188 | nick: '=', 189 | doNotHandleLocalStream: '=' 190 | }, 191 | link: function (scope, element, attr) { 192 | scope.mirror = attr.mirror === 'true'; 193 | scope.muted = attr.muted === 'true'; 194 | }, 195 | controller: function ($scope, $rootScope) { 196 | var webrtc; 197 | 198 | function formRTCOptions() { 199 | var webrtcOptions = { 200 | // the id/element dom element that will hold "our" video 201 | localVideoEl: 'localVideo', 202 | autoRequestMedia: false, 203 | debug: false, 204 | nick: $scope.nick, 205 | media: { 206 | audio: false, 207 | video: true 208 | }, 209 | receiveMedia: { // FIXME: remove old chrome <= 37 constraints format 210 | mandatory: { 211 | OfferToReceiveAudio: false, 212 | OfferToReceiveVideo: false 213 | } 214 | } 215 | }; 216 | grabExtraWebRTCOptions(webrtcOptions); 217 | 218 | if ($scope.muted) { 219 | webrtcOptions.media = { 220 | audio: false, 221 | video: true 222 | }; 223 | } 224 | // source id returned from navigator.getUserMedia (optional) 225 | var sourceId = $scope.sourceId; 226 | if (sourceId) { 227 | console.log('requesting video camera with id ' + sourceId); 228 | webrtcOptions.media.video = { 229 | optional: [{ sourceId: sourceId }] 230 | }; 231 | } 232 | if ($scope.minWidth) { 233 | var minWidth = parseInt($scope.minWidth); 234 | if (typeof webrtcOptions.media.video !== 'object') { 235 | webrtcOptions.media.video = {}; 236 | } 237 | webrtcOptions.media.video.mandatory = { 238 | minWidth: minWidth, 239 | maxWidth: minWidth 240 | }; 241 | } 242 | return webrtcOptions; 243 | } 244 | 245 | // options to make after the webrtc object is created. 246 | function postCreationRTCOptions(webrtc) 247 | { 248 | webrtc.config.localVideo.mirror = Boolean($scope.mirror); 249 | if ($scope.muted) { 250 | webrtc.mute(); 251 | } 252 | } 253 | 254 | // event Responses to make after the webrtc object is created. 255 | function rtcEventResponses(webrtc) 256 | { 257 | 258 | if( ! $scope.doNotHandleLocalStream) { 259 | webrtc.off('localStream'); 260 | webrtc.on('localStream', function (stream) { 261 | console.log('got video stream', stream, 'from the local camera'); 262 | var videoTracks = stream.getVideoTracks(); 263 | console.log('how many video tracks?', videoTracks.length); 264 | if (videoTracks.length) { 265 | var first = videoTracks[0]; 266 | console.log('video track label', first.label); 267 | } 268 | // videoList is an array, it means the user wants to append the video in it 269 | if (Array.isArray($scope.videoList)) { 270 | var video = document.createElement("video"); 271 | video.id = stream.id; 272 | // TODO use $window service 273 | video.src = window.URL.createObjectURL(stream); 274 | video.play(); 275 | video.isRemote = false; 276 | $scope.videoList.push(video); 277 | } 278 | 279 | $scope.hasStream = true; 280 | $scope.$apply(); 281 | }); 282 | } 283 | webrtc.on('localMediaError', function (err) { 284 | console.error('local camera error', err, 285 | 'media constraints', webrtc.config.media); 286 | $scope.$emit('localMediaError', { 287 | error: err, 288 | config: webrtc.config.media 289 | }); 290 | }); 291 | } 292 | 293 | $scope.$on('prepare', function prepareToBroadcast() { 294 | if (webrtc) { 295 | console.log('already has prepared'); 296 | return; 297 | } 298 | 299 | var webrtcOptions = formRTCOptions(); 300 | webrtc = new SimpleWebRTC(webrtcOptions); 301 | postCreationRTCOptions(webrtc); 302 | $rootScope.webrtc = webrtc; 303 | $scope.$emit('haveWebRTC'); 304 | rtcEventResponses(webrtc); 305 | if (!$scope.doNotHandleLocalStream) webrtc.startLocalVideo(); //otherwise webrtc.startLocalVideo(); 306 | }); 307 | 308 | function isTakenError(err) { 309 | return err === 'taken'; 310 | } 311 | 312 | function onStartedRoom(name) { 313 | console.log('joining as broadcaster to room ', name); 314 | $scope.isBroadcasting = true; 315 | $scope.$emit('created-room', name); 316 | $scope.$apply(); 317 | } 318 | 319 | function joinRoomAsBroadcaster() { 320 | console.log('Trying to join existing room "%s" as broadcaster', $scope.roomName); 321 | webrtc.joinRoom($scope.roomName); 322 | $scope.isBroadcasting = true; 323 | $scope.$emit('created-room', name); 324 | } 325 | 326 | // 327 | $scope.$on('start', function start() { 328 | console.log('starting room', $scope.roomName); 329 | if (!$scope.roomName) { 330 | return; 331 | } 332 | 333 | webrtc.createRoom($scope.roomName, function (err) { 334 | if (err) { 335 | if (isTakenError(err)) { 336 | console.log('Room "%s" is taken', $scope.roomName); 337 | joinRoomAsBroadcaster(); 338 | } else { 339 | $scope.$emit('createRoomError', err); 340 | throw new Error(err); 341 | } 342 | } else { 343 | onStartedRoom($scope.roomName); 344 | } 345 | }); 346 | 347 | // a peer can send message to everyone in the room using 348 | // webrtc.sendDirectlyToAll('hi there') or webrtc.sendToAll('hi there') 349 | webrtc.on('channelMessage', function (peer, message) { 350 | console.log('received channel message "%s" from peer "%s"', 351 | message, peer.nick || peer.id); 352 | var value = JSON.parse(message); 353 | $scope.$emit('channelMessage', peer, value); 354 | $scope.$apply(); 355 | }); 356 | 357 | }); 358 | 359 | $scope.$on('messageAll', function (event, message) { 360 | if (message && webrtc) { 361 | var str = JSON.stringify(message); 362 | webrtc.sendDirectlyToAll(str); 363 | } 364 | }); 365 | } 366 | }; 367 | }); 368 | 369 | function grabExtraWebRTCOptions(webrtcOptions) { 370 | var ngSimpleWebRTC = window.ngSimpleWebRTC || {}; 371 | // This is the turn/stun servers. 372 | if (typeof ngSimpleWebRTC.peerConnectionConfig !== 'undefined') { 373 | webrtcOptions.peerConnectionConfig = ngSimpleWebRTC.peerConnectionConfig; 374 | } 375 | if (typeof ngSimpleWebRTC.debug !== 'undefined') { 376 | webrtcOptions.debug = ngSimpleWebRTC.debug; 377 | } 378 | if (typeof ngSimpleWebRTC.socketio === 'object') { 379 | webrtcOptions.socketio = ngSimpleWebRTC.socketio; 380 | } 381 | } 382 | 383 | }(window.angular)); 384 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-simple-webrtc", 3 | "version": "0.22.1", 4 | "description": "AngularJS wrapper for SimpleWebRTC client from https://simplewebrtc.com/", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "clean-console broadcast.html && clean-console watch.html", 8 | "start": "http-server -p 3400" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/bahmutov/ng-simple-webrtc.git" 13 | }, 14 | "keywords": [ 15 | "webrtc", 16 | "video", 17 | "peer", 18 | "communication", 19 | "simple", 20 | "simplewebrtc", 21 | "angular", 22 | "ng", 23 | "angularjs" 24 | ], 25 | "author": "Gleb Bahmutov ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/bahmutov/ng-simple-webrtc/issues" 29 | }, 30 | "homepage": "https://github.com/bahmutov/ng-simple-webrtc", 31 | "dependencies": { 32 | "angular": "1.4.1", 33 | "http-server": "0.8.0", 34 | "simplewebrtc": "1.19.0" 35 | }, 36 | "devDependencies": { 37 | "clean-console": "0.3.0", 38 | "console-log-div": "0.6.2", 39 | "es5-shim": "4.1.7", 40 | "pre-git": "0.6.1" 41 | }, 42 | "pre-commit": "npm test", 43 | "post-commit": "npm version" 44 | } 45 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Menlo,Monaco,Consolas,"Courier New",monospace; 3 | } 4 | 5 | video { 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /watch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Simple Angular watcher via WebRTC

12 | 13 |
14 | 15 |

Join and watch a room

16 | 17 | 18 | 19 | 24 | 25 | 27 | 28 | 30 | 31 |

Message to peers 32 | in the room 33 | 34 |

35 | 36 | 37 | 38 | 62 | 63 | 64 | 65 | --------------------------------------------------------------------------------