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 |
Prepare to broadcast
71 |
72 |
73 | Start room
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 | Join room
125 | Leave room
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 |
Prepare to broadcast
15 |
16 |
24 |
25 |
26 |
Start my own room
27 |
28 |
Start room
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 Send
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: '',
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 |
Join room
27 |
28 |
Leave room
30 |
31 |
Message to peers
32 | in the room Send
33 |
34 |
35 |
36 |
37 |
38 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------