electric_eye 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.org +20 -8
- data/bin/electric_eye +6 -1
- data/features/electric_eye.feature +8 -7
- data/lib/electric_eye/config_eye.rb +17 -4
- data/lib/electric_eye/motion.rb +44 -0
- data/lib/electric_eye/record.rb +27 -4
- data/lib/electric_eye/version.rb +1 -1
- data/lib/electric_eye.rb +1 -0
- data/man/electric_eye.1 +2 -2
- data/man/electric_eye.1.html +2 -2
- data/man/electric_eye.1.ronn +2 -2
- data/spec/config_spec.rb +46 -7
- data/spec/fixtures/movement.log +1219 -0
- data/spec/fixtures/no_movement.log +1219 -0
- data/spec/motion_spec.rb +79 -0
- data/spec/{electric_eye_spec.rb → record_spec.rb} +21 -1
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2affb0dc95af7bf5563442e3d984bad0011a6e2
|
4
|
+
data.tar.gz: 71cb8b06d07557218df843b86121cd3e3685a124
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6eef4c40c21ddfcb1f66428e60a9cba2388718d65bbd25d09fb579aebe64500266d795c26cb5dd196fa186372838215d754d9d737f219e631c45d20ea312851
|
7
|
+
data.tar.gz: 258b468cef8ce9839000b1be4b38dab26f4f8345230b76cac884b714ba41ffd3f12c02233b8ef8c1e371e1c34018cb81bc969679f7aa4c91bcbf88a0f47b6bd9
|
data/Gemfile.lock
CHANGED
data/README.org
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
A network video recorder for multiple IP cameras using VLC.
|
4
4
|
|
5
|
+
[[http://mlug-au.org/doku.php/workshops/electric_eye_mpd][MLUG presentation slides on electric_eye]]
|
6
|
+
|
5
7
|
** History
|
6
8
|
|
7
9
|
I've been using Zoneminder & motion and these programs are either too large for my requirements (zoneminder) or don't work with the cameras I own (motion). What I did notice is all my cameras work through VLC with high resolution and VLC can record.
|
@@ -10,7 +12,8 @@ The problem was though VLC doesn't automate the recordings or handle the file st
|
|
10
12
|
|
11
13
|
** Requirements
|
12
14
|
|
13
|
-
- VLC
|
15
|
+
- VLC - recording & motion detection
|
16
|
+
- xvfb - Running virtual frame buffers (ie: desktops)
|
14
17
|
- ruby
|
15
18
|
- Linux (Tested on Debian 7)
|
16
19
|
|
@@ -62,15 +65,19 @@ The default is going to be 10 minute blocks, this can be overridden with the dur
|
|
62
65
|
|
63
66
|
First make sure you add your cameras
|
64
67
|
|
65
|
-
: electric_eye
|
68
|
+
: electric_eye -a Reception <url>
|
66
69
|
|
67
70
|
Now start the daemon to start the recording process
|
68
71
|
|
69
|
-
: electric_eye
|
72
|
+
: electric_eye -s
|
73
|
+
|
74
|
+
Start with debug messages
|
75
|
+
|
76
|
+
: electric_eye -s --log-level=debug
|
70
77
|
|
71
78
|
Stop all recordings
|
72
79
|
|
73
|
-
: electric_eye
|
80
|
+
: electric_eye -k
|
74
81
|
|
75
82
|
Usage in development mode
|
76
83
|
|
@@ -143,18 +150,23 @@ Example for cleaning up reception after 60days at 7pm everynight.
|
|
143
150
|
5. Create a new Pull Request
|
144
151
|
|
145
152
|
** TODO
|
153
|
+
:PROPERTIES:
|
154
|
+
:CREATED: [2015-07-01 Wed 16:37]
|
155
|
+
:END:
|
146
156
|
|
147
|
-
- [
|
157
|
+
- [X] Add more testing
|
148
158
|
|
149
|
-
- [
|
159
|
+
- [X] Add post recording motion detection (use vlc)
|
160
|
+
|
161
|
+
- [X] Make sure we cannot add blank cameras
|
162
|
+
|
163
|
+
- [X] Create threshold as a variable
|
150
164
|
|
151
165
|
- [ ] Add a feature to clean up old recordings using a "period" setting
|
152
166
|
EG: 60 day period which could be set in the config file how many days you want to keep
|
153
167
|
Then just call 'electric_eye --remove-recordings' within crontab
|
154
168
|
This would iterate over all my cameras and remove old recordings to keep a rolling set of days.
|
155
169
|
|
156
|
-
- [ ] Make sure we cannot add blank cameras
|
157
|
-
|
158
170
|
- [ ] Allow different recording programs like raspicam
|
159
171
|
|
160
172
|
- [ ] Do inline motion detection (using activevlc)
|
data/bin/electric_eye
CHANGED
@@ -28,6 +28,10 @@ class App
|
|
28
28
|
@record.start
|
29
29
|
elsif options[:k] # Stop recordings
|
30
30
|
@record.stop
|
31
|
+
elsif options[:t]
|
32
|
+
@configEye.set_threshold(options[:threshold])
|
33
|
+
else
|
34
|
+
puts opts.help
|
31
35
|
end
|
32
36
|
|
33
37
|
exit 0
|
@@ -41,10 +45,11 @@ class App
|
|
41
45
|
on("-a", "--add", "Add a camera")
|
42
46
|
on("-r", "--remove", "Remove a camera")
|
43
47
|
on("-l", "--list", "List cameras")
|
44
|
-
on("-d", "--duration SECONDS", "Set recording duration in seconds")
|
48
|
+
on("-d", "--duration SECONDS", "Set recording duration in seconds (default: 600)")
|
45
49
|
on("-p", "--path DIR", "Set recordings path")
|
46
50
|
on("-s", "--start", "Start recordings")
|
47
51
|
on("-k", "--stop", "Stop recordings")
|
52
|
+
on("-t", "--threshold LEVEL", "Set threshold for motion detection (default: 2)")
|
48
53
|
|
49
54
|
# Arguments
|
50
55
|
arg :camera, :optional
|
@@ -13,11 +13,12 @@ Feature: Electric Eye
|
|
13
13
|
| camera | which is optional |
|
14
14
|
| url | which is optional |
|
15
15
|
And the following options should be documented:
|
16
|
-
| --add
|
17
|
-
| --remove
|
18
|
-
| --duration
|
19
|
-
| --path
|
20
|
-
| --list
|
21
|
-
| --start
|
22
|
-
| --stop
|
16
|
+
| --add |
|
17
|
+
| --remove |
|
18
|
+
| --duration |
|
19
|
+
| --path |
|
20
|
+
| --list |
|
21
|
+
| --start |
|
22
|
+
| --stop |
|
23
|
+
| --threshold |
|
23
24
|
|
@@ -19,7 +19,7 @@ module ElectricEye
|
|
19
19
|
if File.exist?(CONFIG_FILE)
|
20
20
|
Construct.load File.read(CONFIG_FILE)
|
21
21
|
else
|
22
|
-
Construct.new({duration: 600, path: '~/recordings', cameras: []})
|
22
|
+
Construct.new({duration: 600, path: '~/recordings', threshold: 2, cameras: []})
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -30,9 +30,15 @@ module ElectricEye
|
|
30
30
|
|
31
31
|
# Add camera
|
32
32
|
def add_camera(camera, url)
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
if camera.nil?
|
34
|
+
warn "NO camera given"
|
35
|
+
elsif url.nil?
|
36
|
+
warn "NO url given"
|
37
|
+
else
|
38
|
+
@config.cameras.push({name: camera, url: url})
|
39
|
+
save
|
40
|
+
info "Camera added"
|
41
|
+
end
|
36
42
|
end
|
37
43
|
|
38
44
|
# Remove camera
|
@@ -58,6 +64,13 @@ module ElectricEye
|
|
58
64
|
info "Duration set to #{seconds} seconds"
|
59
65
|
end
|
60
66
|
|
67
|
+
# Set threshold
|
68
|
+
def set_threshold(level)
|
69
|
+
@config.threshold = level.to_i
|
70
|
+
save
|
71
|
+
info "Threshold set to #{level} objects"
|
72
|
+
end
|
73
|
+
|
61
74
|
# Set path
|
62
75
|
def set_path(dir)
|
63
76
|
@config.path = dir
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'methadone'
|
2
|
+
require 'open4'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module ElectricEye
|
6
|
+
class Motion
|
7
|
+
include Methadone::CLILogging
|
8
|
+
|
9
|
+
# Detect if there is motion given the results
|
10
|
+
#
|
11
|
+
# path = the log file which is created by vlc with the movementdetect lines in.
|
12
|
+
# threshold is how many objects are moving at once expressed by vlc.
|
13
|
+
#
|
14
|
+
def detect(path, threshold = 2)
|
15
|
+
results = read_log(path)
|
16
|
+
results.each {|line| return true if movement(line) >= threshold}
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create the log file using vlc
|
21
|
+
def create_log(path)
|
22
|
+
# Use Xvfb which opens up a virtual desktop to dump a GUI screen we don't want to see.
|
23
|
+
# -a = select the next available display
|
24
|
+
`xvfb-run -a cvlc --no-loop --play-and-exit --video-filter=motiondetect -vvv #{path}.mjpeg > #{path}.log 2>&1`
|
25
|
+
end
|
26
|
+
|
27
|
+
# Read in the log file and return the motiondetect lines
|
28
|
+
def read_log(path)
|
29
|
+
results = []
|
30
|
+
if File.exists?(path)
|
31
|
+
File.readlines(path).each do |line|
|
32
|
+
results.push line.chomp if line =~ /motiondetect filter/
|
33
|
+
end
|
34
|
+
end
|
35
|
+
results
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get the movement amount from the string
|
39
|
+
def movement(line)
|
40
|
+
line.slice!(/\[.*\]/) # Remove the number in brackets at the start of the string
|
41
|
+
line.slice(/\d+/).to_i # Get the movement
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/electric_eye/record.rb
CHANGED
@@ -12,6 +12,8 @@ module ElectricEye
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def start
|
15
|
+
@motion = Motion.new # Create a new instance method to our motion library
|
16
|
+
|
15
17
|
pids = []
|
16
18
|
# Step through each camera
|
17
19
|
@configEye.config.cameras.each do |camera|
|
@@ -21,10 +23,11 @@ module ElectricEye
|
|
21
23
|
stop_recording = false
|
22
24
|
Signal.trap('INT') { stop_recording = true }
|
23
25
|
until stop_recording
|
24
|
-
|
25
|
-
|
26
|
+
path = "#{path(camera)}"
|
27
|
+
debug "Recording #{camera[:name]} to #{path}.mjpeg..."
|
28
|
+
|
26
29
|
# Set a recording going using vlc, hold onto the process till it's finished.
|
27
|
-
cmd="cvlc
|
30
|
+
cmd="cvlc #{camera[:url]} --sout file/ts:#{path}.mjpeg"
|
28
31
|
pid,stdin,stdout,stderr=Open4::popen4(cmd)
|
29
32
|
|
30
33
|
# Wait for a defined duration from the config file.
|
@@ -37,14 +40,34 @@ module ElectricEye
|
|
37
40
|
|
38
41
|
Process.kill 9, pid # Stop current recording.
|
39
42
|
Process.wait pid # Wait around so we don't get Zombies
|
43
|
+
|
44
|
+
# Look for any motion
|
45
|
+
Thread.new(path) do |threadPath|
|
46
|
+
@motion.create_log(threadPath) # Create the motion detection log file.
|
47
|
+
|
48
|
+
# Remove the log & recording if there is no motion
|
49
|
+
if @motion.detect("#{threadPath}.log", @configEye.config.threshold)
|
50
|
+
debug "KEEP #{threadPath}.mjpeg (motion)"
|
51
|
+
else
|
52
|
+
remove(threadPath)
|
53
|
+
end
|
54
|
+
end
|
40
55
|
end
|
41
56
|
end
|
57
|
+
|
42
58
|
end
|
43
59
|
|
44
60
|
store_pids(pids)
|
45
61
|
info "Cameras recording"
|
46
62
|
end
|
47
63
|
|
64
|
+
# Remove a recording
|
65
|
+
def remove(path)
|
66
|
+
debug "REMOVE #{path}.mjpeg (no motion)"
|
67
|
+
File.delete("#{path}.log")
|
68
|
+
File.delete("#{path}.mjpeg")
|
69
|
+
end
|
70
|
+
|
48
71
|
def stop
|
49
72
|
stop_recordings(get_pids) if File.exist?(PID_FILE)
|
50
73
|
end
|
@@ -62,7 +85,7 @@ module ElectricEye
|
|
62
85
|
def path(camera)
|
63
86
|
dir = "#{@configEye.config.path}/#{camera[:name]}"
|
64
87
|
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
65
|
-
"#{dir}/#{Time.now.strftime('%Y%m%d-%H%M')}-#{camera[:name]}
|
88
|
+
"#{dir}/#{Time.now.strftime('%Y%m%d-%H%M')}-#{camera[:name]}"
|
66
89
|
end
|
67
90
|
|
68
91
|
def initialize(configEye)
|
data/lib/electric_eye/version.rb
CHANGED
data/lib/electric_eye.rb
CHANGED
data/man/electric_eye.1
CHANGED
data/man/electric_eye.1.html
CHANGED
@@ -120,11 +120,11 @@ duration of the recordings is set by the user (default is 10minutes).</p>
|
|
120
120
|
|
121
121
|
<h2 id="AUTHOR">AUTHOR</h2>
|
122
122
|
|
123
|
-
<p>Michael Pope
|
123
|
+
<p>Michael Pope <a href="mailto:map7777@gmail.com" data-bare-link="true">map7777@gmail.com</a></p>
|
124
124
|
|
125
125
|
<h2 id="SEE-ALSO">SEE ALSO</h2>
|
126
126
|
|
127
|
-
<p
|
127
|
+
<p><a href="https://www.videolan.org/vlc/">VLC</a></p>
|
128
128
|
|
129
129
|
|
130
130
|
<ol class='man-decor man-foot man foot'>
|
data/man/electric_eye.1.ronn
CHANGED
data/spec/config_spec.rb
CHANGED
@@ -89,15 +89,27 @@ describe "add camera" do
|
|
89
89
|
before do
|
90
90
|
@configEye = ConfigEye.new
|
91
91
|
end
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
92
|
+
|
93
|
+
context "when both name & url provided" do
|
94
|
+
it "adds camera to array" do
|
95
|
+
@configEye.add_camera("Reception", "http://user:pass@my.camera.org/live2.sdp")
|
96
|
+
expect(@configEye.config.cameras.length).to equal(1)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "calls save" do
|
100
|
+
expect(@configEye).to receive(:save).once
|
101
|
+
@configEye.add_camera("Reception", "http://user:pass@my.camera.org/live2.sdp")
|
102
|
+
end
|
96
103
|
end
|
97
104
|
|
98
|
-
|
99
|
-
|
100
|
-
|
105
|
+
context "when only name provided" do
|
106
|
+
it "returns an error" do
|
107
|
+
end
|
108
|
+
|
109
|
+
it "doesn't call save" do
|
110
|
+
expect(@configEye).to receive(:save).never
|
111
|
+
@configEye.add_camera("Reception", nil)
|
112
|
+
end
|
101
113
|
end
|
102
114
|
end
|
103
115
|
|
@@ -188,3 +200,30 @@ describe "set_path" do
|
|
188
200
|
end
|
189
201
|
end
|
190
202
|
|
203
|
+
describe "set_threshold" do
|
204
|
+
include FakeFS::SpecHelpers
|
205
|
+
|
206
|
+
before do
|
207
|
+
@configEye = ConfigEye.new
|
208
|
+
end
|
209
|
+
|
210
|
+
context "when no threshold has been set" do
|
211
|
+
it "returns the default of 2 objects" do
|
212
|
+
expect(@configEye.config.threshold).to equal(2)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
context "when calling with -d 3" do
|
217
|
+
it "returns 3" do
|
218
|
+
@configEye.set_threshold(3)
|
219
|
+
expect(@configEye.config.threshold).to equal(3)
|
220
|
+
end
|
221
|
+
|
222
|
+
it "calls save" do
|
223
|
+
expect(@configEye).to receive(:save).once
|
224
|
+
@configEye.set_threshold(3)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
|