├── .gitattributes
├── .gitignore
├── README.md
├── beep-7.mp3
├── easyrtc
└── easyrtc.css
├── index.html
├── js
├── beep.js
├── easyRTCapp.js
└── motion.js
└── styles
└── main.css
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 |
19 | # External tool builders
20 | .externalToolBuilders/
21 |
22 | # Locally stored "Eclipse launch configurations"
23 | *.launch
24 |
25 | # CDT-specific
26 | .cproject
27 |
28 | # PDT-specific
29 | .buildpath
30 |
31 |
32 | #################
33 | ## Visual Studio
34 | #################
35 |
36 | ## Ignore Visual Studio temporary files, build results, and
37 | ## files generated by popular Visual Studio add-ons.
38 |
39 | # User-specific files
40 | *.suo
41 | *.user
42 | *.sln.docstates
43 |
44 | # Build results
45 |
46 | [Dd]ebug/
47 | [Rr]elease/
48 | x64/
49 | build/
50 | [Bb]in/
51 | [Oo]bj/
52 |
53 | # MSTest test Results
54 | [Tt]est[Rr]esult*/
55 | [Bb]uild[Ll]og.*
56 |
57 | *_i.c
58 | *_p.c
59 | *.ilk
60 | *.meta
61 | *.obj
62 | *.pch
63 | *.pdb
64 | *.pgc
65 | *.pgd
66 | *.rsp
67 | *.sbr
68 | *.tlb
69 | *.tli
70 | *.tlh
71 | *.tmp
72 | *.tmp_proj
73 | *.log
74 | *.vspscc
75 | *.vssscc
76 | .builds
77 | *.pidb
78 | *.log
79 | *.scc
80 |
81 | # Visual C++ cache files
82 | ipch/
83 | *.aps
84 | *.ncb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 |
89 | # Visual Studio profiler
90 | *.psess
91 | *.vsp
92 | *.vspx
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 |
101 | # TeamCity is a build add-in
102 | _TeamCity*
103 |
104 | # DotCover is a Code Coverage Tool
105 | *.dotCover
106 |
107 | # NCrunch
108 | *.ncrunch*
109 | .*crunch*.local.xml
110 |
111 | # Installshield output folder
112 | [Ee]xpress/
113 |
114 | # DocProject is a documentation generator add-in
115 | DocProject/buildhelp/
116 | DocProject/Help/*.HxT
117 | DocProject/Help/*.HxC
118 | DocProject/Help/*.hhc
119 | DocProject/Help/*.hhk
120 | DocProject/Help/*.hhp
121 | DocProject/Help/Html2
122 | DocProject/Help/html
123 |
124 | # Click-Once directory
125 | publish/
126 |
127 | # Publish Web Output
128 | *.Publish.xml
129 | *.pubxml
130 |
131 | # NuGet Packages Directory
132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
133 | #packages/
134 |
135 | # Windows Azure Build Output
136 | csx
137 | *.build.csdef
138 |
139 | # Windows Store app package directory
140 | AppPackages/
141 |
142 | # Others
143 | sql/
144 | *.Cache
145 | ClientBin/
146 | [Ss]tyle[Cc]op.*
147 | ~$*
148 | *~
149 | *.dbmdl
150 | *.[Pp]ublish.xml
151 | *.pfx
152 | *.publishsettings
153 |
154 | # RIA/Silverlight projects
155 | Generated_Code/
156 |
157 | # Backup & report files from converting an old project file to a newer
158 | # Visual Studio version. Backup files are not needed, because we have git ;-)
159 | _UpgradeReport_Files/
160 | Backup*/
161 | UpgradeLog*.XML
162 | UpgradeLog*.htm
163 |
164 | # SQL Server files
165 | App_Data/*.mdf
166 | App_Data/*.ldf
167 |
168 | #############
169 | ## Windows detritus
170 | #############
171 |
172 | # Windows image file caches
173 | Thumbs.db
174 | ehthumbs.db
175 |
176 | # Folder config file
177 | Desktop.ini
178 |
179 | # Recycle Bin used on file shares
180 | $RECYCLE.BIN/
181 |
182 | # Mac crap
183 | .DS_Store
184 |
185 |
186 | #############
187 | ## Python
188 | #############
189 |
190 | *.py[co]
191 |
192 | # Packages
193 | *.egg
194 | *.egg-info
195 | dist/
196 | build/
197 | eggs/
198 | parts/
199 | var/
200 | sdist/
201 | develop-eggs/
202 | .installed.cfg
203 |
204 | # Installer logs
205 | pip-log.txt
206 |
207 | # Unit test / coverage reports
208 | .coverage
209 | .tox
210 |
211 | #Translations
212 | *.mo
213 |
214 | #Mr Developer
215 | .mr.developer.cfg
216 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WebRTC Motion Detecting Baby Monitor
2 | ====================================
3 |
4 | WebRTC-based baby monitor with motion detection.
5 |
6 | WebRTC elements utilize on Priologic's EasyRTC Enterprise - https://github.com/priologic/easyrtc
7 | Motion detection elements based on Really Good's js motion detection - https://github.com/ReallyGood/js-motion-detection
8 |
9 | This is currently a very crude proof-of-concept for demonstration purposes. Call is established using easyRTC and the video stream is redirected to motion.js. Motion.js detects motion by comparing frames in the video stream. If the motion level is very low (currently set to 1 - very sensitive) - a 'motion' event is fired. beep.js plays a beep when it sees this this event.
10 |
11 | Please see webrtchacks.com for details until I get a chance to write more.
12 |
13 | Tested with Chrome 29.0.1547.62 and Firefox 23.0.1.
14 |
15 | Please see http://webrtchacks.com/baby-motion-detector/ for more details.
--------------------------------------------------------------------------------
/beep-7.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webrtcHacks/webrtc_baby_monitor/b2549dfcbeb1663b19df7859624e1c1449d9cd09/beep-7.mp3
--------------------------------------------------------------------------------
/easyrtc/easyrtc.css:
--------------------------------------------------------------------------------
1 | /*
2 | * These styles define the appearance of the default error dialog box.
3 | */
4 | #easyRTCErrorDialog {
5 | background-color: #ffe0e0;
6 |
7 | position:fixed;
8 | right: 10px;
9 | top:20px;
10 | z-index: 30;
11 | opacity: 0.95;
12 | padding: 0.5em;
13 | border-radius:10px;
14 | border-color: red;
15 | border-style: solid;
16 | border-width: 1px;
17 | -webkit-box-shadow: 2px 2px 8px 1px rgba(0,0,0,0.9);
18 | box-shadow: 2px 2px 8px 1px rgba(0,0,0,0.9);
19 | }
20 |
21 | .easyRTCErrorDialog_title {
22 | position:static;
23 | text-align:center;
24 | font-size: 18px;
25 | font-weight: bold;
26 | margin-bottom: 0.5em;
27 | clear:both;
28 | }
29 |
30 | #easyRTCErrorDialog_body{
31 | position:static;
32 | height:150px;
33 | overflow-y:auto;
34 | }
35 |
36 | .easyRTCErrorDialog_element {
37 | position:static;
38 | font-style: italic;
39 | font-size: 12px;
40 | width:300px;
41 | margin-bottom: 0.5em;
42 | clear: both;
43 | float:left;
44 | }
45 |
46 | .easyRTCErrorDialog_okayButton {
47 | position:static;
48 | clear:both;
49 | float:right;
50 | }
51 |
52 | .easyRTCMirror {
53 | -webkit-transform: scaleX(-1);
54 | -moz-transform: scaleX(-1);
55 | -ms-transform: scaleX(-1);
56 | -o-transform: scaleX(-1);
57 | transform: scaleX(-1);
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Motion Detection Hack
5 |
6 |
7 |
8 |
9 |
10 |
WebRTC Motion Detecting Baby Monitor
11 |
12 |
13 |
Local
14 |
15 |
16 |
17 |
18 |
Remote
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/js/beep.js:
--------------------------------------------------------------------------------
1 | //Modified from js motion sample.js
2 | // consider using a debounce utility if you get too many consecutive events
3 | (function () {
4 | $(window).on('motion', function (ev, data) {
5 | console.log('detected motion at', new Date(), 'with data:', data);
6 |
7 | //Play a beep
8 | var snd = new Audio("beep-7.mp3"); // buffers automatically when created
9 | snd.play();
10 |
11 | //navigator.notification.vibrate(200);
12 | console.log("Beep!");
13 |
14 | var spot = $(data.spot.el);
15 | spot.addClass('active');
16 | setTimeout(function () {
17 | spot.removeClass('active');
18 | }, 230);
19 |
20 | });
21 | })();
--------------------------------------------------------------------------------
/js/easyRTCapp.js:
--------------------------------------------------------------------------------
1 | //source: https://github.com/priologic/easyrtc/blob/master/docs/easyrtc_client_tutorial.md
2 |
3 | easyRTC.setStreamAcceptor( function(callerEasyrtcid, stream) {
4 | var video = document.getElementById('remote');
5 | easyRTC.setVideoObjectSrc(video, stream);
6 | });
7 |
8 | easyRTC.setOnStreamClosed( function (callerEasyrtcid) {
9 | easyRTC.setVideoObjectSrc(document.getElementById('remote'), "");
10 | });
11 |
12 |
13 | function my_init() {
14 |
15 | easyRTC.setLoggedInListener( loggedInListener);
16 | var connectSuccess = function(myId) {
17 | console.log("My easyrtcid is " + myId);
18 | }
19 | var connectFailure = function(errmesg) {
20 | console.log(errmesg);
21 | }
22 |
23 | //ADDED THIS
24 | easyRTC.setApiKey("4lbjgglag51i4thm");
25 | easyRTC.setSocketUrl("https://lb1.easyrtc.com/");
26 |
27 |
28 | easyRTC.enableDebug(false);
29 | console.log("EasyRTC Initializing.");
30 |
31 | //Prevent audio feedback when self-testing
32 | easyRTC.enableAudio(false);
33 | //easyRTC.enableDebug(true);
34 | console.log("EasyRTC Initializing.");
35 |
36 | easyRTC.initMediaSource(
37 | function () { // success callback
38 | var selfVideo = document.getElementById("local");
39 | easyRTC.setVideoObjectSrc(selfVideo, easyRTC.getLocalStream());
40 | easyRTC.connect("webrtcHacks Baby Monitor", connectSuccess, connectFailure);
41 | },
42 | connectFailure
43 | );
44 | }
45 |
46 |
47 | function loggedInListener(connected) {
48 | var otherClientDiv = document.getElementById('otherClients');
49 | while (otherClientDiv.hasChildNodes()) {
50 | otherClientDiv.removeChild(otherClientDiv.lastChild);
51 | }
52 | for(var i in connected) {
53 | var button = document.createElement('button');
54 | button.onclick = function(easyrtcid) {
55 | return function() {
56 | performCall(easyrtcid);
57 | }
58 | }(i);
59 |
60 | label = document.createTextNode(i);
61 | button.appendChild(label);
62 | otherClientDiv.appendChild(button);
63 | }
64 | }
65 |
66 |
67 | function performCall(easyrtcid) {
68 | easyRTC.call(
69 | easyrtcid,
70 | function(easyrtcid) { console.log("completed call to " + easyrtcid);},
71 | function(errorMessage) { console.log("err:" + errorMessage);},
72 | function(accepted, bywho) {
73 | console.log((accepted?"accepted":"rejected")+ " by " + bywho);
74 | }
75 | );
76 | }
--------------------------------------------------------------------------------
/js/motion.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | // config start
4 | var OUTLINES = false;
5 | // config end
6 |
7 | window.hotSpots = [];
8 |
9 | var content = $('#dmotion');
10 | var canvases = $('canvas');
11 |
12 | var lastImageData;
13 | var canvasSource = $("#canvas-source")[0];
14 | var canvasBlended = $("#canvas-blended")[0];
15 |
16 | var contextSource = canvasSource.getContext('2d');
17 | var contextBlended = canvasBlended.getContext('2d');
18 |
19 |
20 | var video;
21 | //var sensitivity = 3;
22 |
23 | function startMotion() {
24 |
25 | console.log("Motion detection started.");
26 |
27 | //change this to remote
28 | video = $('#remote')[0];
29 |
30 |
31 | var resize = function () {
32 | var ratio = video.width / video.height;
33 | var w = $(this).width();
34 | var h = $(this).height() - 110;
35 |
36 | if (content.width() > w) {
37 | content.width(w);
38 | content.height(w / ratio);
39 | } else {
40 | content.height(h);
41 | content.width(h * ratio);
42 | }
43 | canvases.width(content.width());
44 | canvases.height(content.height());
45 | }
46 | $(window).resize(resize);
47 | $(window).ready(function () {
48 | resize();
49 |
50 | });
51 |
52 | // mirror video
53 | contextSource.translate(canvasSource.width, 0);
54 | contextSource.scale(-1, 1);
55 |
56 | var c = 5;
57 |
58 | start();
59 | } //end function
60 |
61 | function start() {
62 | //$('.loading').fadeOut();
63 | $('#hotSpots').fadeIn();
64 | // $('body').addClass('black-background');
65 | // $(".instructions").delay(600).fadeIn();
66 | $(canvasSource).delay(600).fadeIn();
67 | $(canvasBlended).delay(600).fadeIn();
68 | $('#canvas-highlights').delay(600).fadeIn();
69 | $(window).trigger('start');
70 | update();
71 | }
72 |
73 | window.requestAnimFrame = (function () {
74 | return window.requestAnimationFrame ||
75 | window.webkitRequestAnimationFrame ||
76 | window.mozRequestAnimationFrame ||
77 | window.oRequestAnimationFrame ||
78 | window.msRequestAnimationFrame ||
79 | function (callback) {
80 | window.setTimeout(callback, 1000 / 60);
81 | };
82 | })();
83 |
84 | function update() {
85 | drawVideo();
86 | blend();
87 | checkAreas();
88 | requestAnimFrame(update);
89 | }
90 |
91 | function drawVideo() {
92 | contextSource.drawImage(video, 0, 0, video.width, video.height);
93 | }
94 |
95 | function blend() {
96 | var width = canvasSource.width;
97 | var height = canvasSource.height;
98 | // get webcam image data
99 | var sourceData = contextSource.getImageData(0, 0, width, height);
100 | // create an image if the previous image doesn’t exist
101 | if (!lastImageData) lastImageData = contextSource.getImageData(0, 0, width, height);
102 | // create a ImageData instance to receive the blended result
103 | var blendedData = contextSource.createImageData(width, height);
104 | // blend the 2 images
105 | differenceAccuracy(blendedData.data, sourceData.data, lastImageData.data);
106 | // draw the result in a canvas
107 | contextBlended.putImageData(blendedData, 0, 0);
108 | // store the current webcam image
109 | lastImageData = sourceData;
110 | }
111 |
112 | function fastAbs(value) {
113 | // funky bitwise, equal Math.abs
114 | return (value ^ (value >> 31)) - (value >> 31);
115 | }
116 |
117 | function threshold(value) {
118 | return (value > 0x15) ? 0xFF : 0;
119 | }
120 |
121 | function difference(target, data1, data2) {
122 | // blend mode difference
123 | if (data1.length != data2.length) return null;
124 | var i = 0;
125 | while (i < (data1.length * 0.25)) {
126 | target[4 * i] = data1[4 * i] == 0 ? 0 : fastAbs(data1[4 * i] - data2[4 * i]);
127 | target[4 * i + 1] = data1[4 * i + 1] == 0 ? 0 : fastAbs(data1[4 * i + 1] - data2[4 * i + 1]);
128 | target[4 * i + 2] = data1[4 * i + 2] == 0 ? 0 : fastAbs(data1[4 * i + 2] - data2[4 * i + 2]);
129 | target[4 * i + 3] = 0xFF;
130 | ++i;
131 | }
132 | }
133 |
134 | function differenceAccuracy(target, data1, data2) {
135 | if (data1.length != data2.length) return null;
136 | var i = 0;
137 | while (i < (data1.length * 0.25)) {
138 | var average1 = (data1[4 * i] + data1[4 * i + 1] + data1[4 * i + 2]) / 3;
139 | var average2 = (data2[4 * i] + data2[4 * i + 1] + data2[4 * i + 2]) / 3;
140 | var diff = threshold(fastAbs(average1 - average2));
141 | target[4 * i] = diff;
142 | target[4 * i + 1] = diff;
143 | target[4 * i + 2] = diff;
144 | target[4 * i + 3] = 0xFF;
145 | ++i;
146 | }
147 | }
148 |
149 | //This is still setup for looking at individual hotspots. Could be simplified to con
150 | function checkAreas() {
151 | var data;
152 |
153 | for (var h = 0; h < hotSpots.length; h++) {
154 | var blendedData = contextBlended.getImageData(hotSpots[h].x, hotSpots[h].y, hotSpots[h].width, hotSpots[h].height);
155 | var i = 0;
156 | var average = 0;
157 | while (i < (blendedData.data.length * 0.25)) {
158 | // make an average between the color channel
159 | average += (blendedData.data[i * 4] + blendedData.data[i * 4 + 1] + blendedData.data[i * 4 + 2]) / 3;
160 | ++i;
161 | }
162 | // calculate an average between the color values of the spot area
163 | average = Math.round(average / (blendedData.data.length * 0.25));
164 |
165 | //CHAD: Value below was originaly 10 - made very sensisitve
166 | if (average > 1) {
167 | // over a small limit, consider that a movement is detected
168 | data = { confidence: average, spot: hotSpots[h] };
169 | $(data.spot.el).trigger('motion', data);
170 | }
171 | } }
172 |
173 |
174 | function getCoords() {
175 | $('#hotSpots').children().each(function (i, el) {
176 | var ratio = $("#canvas-highlights").width() / $('video').width();
177 | hotSpots[i] = {
178 | x: this.offsetLeft / ratio,
179 | y: this.offsetTop / ratio,
180 | width: this.scrollWidth / ratio,
181 | height: this.scrollHeight / ratio,
182 | el: el
183 | };
184 | });
185 | if (OUTLINES) highlightHotSpots();
186 | }
187 |
188 | $(window).on('start resize', getCoords);
189 |
--------------------------------------------------------------------------------
/styles/main.css:
--------------------------------------------------------------------------------
1 |
2 | /* Clean these up at some point */
3 | /*
4 |
5 | #container {
6 | position: absolute;
7 | float: left;
8 | left: 0;
9 | right: 0;
10 | top: 0;
11 | bottom: 0;
12 | }
13 |
14 | #content {
15 | position: absolute;
16 | float: left;
17 | left: 0;
18 | right: 0;
19 | top: 0;
20 | bottom: 0;
21 | overflow: hidden;
22 | z-index: 0;
23 | }
24 |
25 | .magic {
26 | float: left;
27 | padding-left: 20px;
28 | font-size: 20px;
29 | color: #AAA;
30 | line-height: 55px;
31 | }
32 |
33 |
34 | /*added this
35 | #iframe {
36 | width: 100%;
37 | height: 100%;
38 | position: absolute;
39 | left: 0;
40 | top: 0;
41 | }
42 | */
43 |
44 |
45 | /*These are critical for js motion detection*/
46 | #canvas-source, #canvas-highlights {
47 | display: none;
48 | position: absolute;
49 | left: 0;
50 | top: 0;
51 | opacity: 0;
52 | }
53 |
54 | #canvas-blended {
55 | display: none;
56 | position: absolute;
57 | bottom: 0;
58 | right: 0;
59 | opacity: 0;
60 | }
61 |
62 | #hotSpots {
63 | position: relative;
64 | display: none;
65 | height: 100%;
66 | }
67 |
68 | /*
69 | #hotSpots > * {
70 | position: absolute;
71 | -moz-transition: all 80ms linear;
72 | -webkit-transition: all 80ms linear;
73 | transition: all 80ms linear;
74 | }
75 | */
76 |
77 | /*added this to make the target the entire area*/
78 | #target {
79 | width: 100%;
80 | height: 100%;
81 | top: 0;
82 | left: 0;
83 | margin-top: 0;
84 | }
85 |
--------------------------------------------------------------------------------