electric_eye 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b2affb0dc95af7bf5563442e3d984bad0011a6e2
4
- data.tar.gz: 71cb8b06d07557218df843b86121cd3e3685a124
3
+ metadata.gz: 33c71e7271287c8eefaed7c21b11e5dbfa8f2e8e
4
+ data.tar.gz: d95da73de1f27d44012389bc7d6b18516ef0f15a
5
5
  SHA512:
6
- metadata.gz: b6eef4c40c21ddfcb1f66428e60a9cba2388718d65bbd25d09fb579aebe64500266d795c26cb5dd196fa186372838215d754d9d737f219e631c45d20ea312851
7
- data.tar.gz: 258b468cef8ce9839000b1be4b38dab26f4f8345230b76cac884b714ba41ffd3f12c02233b8ef8c1e371e1c34018cb81bc969679f7aa4c91bcbf88a0f47b6bd9
6
+ metadata.gz: da2bfd5d2e5366b3e8a3929613207aa2ae489577baea1d99abc85a2626e6843b2b4ee4d8e85f69ba53a36e2147c1649f1ccde2054fed9ecad4c851d80c86dc86
7
+ data.tar.gz: 7c9e5f484dfc4e6dd4d179fa2982a2c2dced25645bc2e51a418ffebcd08415bb0c94aff473e4ca92702550fabd47d62f9b6cb1f71120ad795c7c161efcf1aba3
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.4
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- electric_eye (0.0.4)
4
+ electric_eye (0.1.0)
5
5
  construct
6
+ filewatcher
6
7
  methadone (~> 1.9.0)
7
8
  open4
8
9
  table_print
@@ -30,12 +31,14 @@ GEM
30
31
  diff-lcs (1.2.5)
31
32
  fakefs (0.6.7)
32
33
  ffi (1.9.8)
34
+ filewatcher (0.5.3)
35
+ trollop (~> 2.0)
33
36
  gem-man (0.3.0)
34
37
  gherkin (2.12.2)
35
38
  multi_json (~> 1.3)
36
39
  hpricot (0.8.6)
37
40
  json (1.8.2)
38
- methadone (1.9.1)
41
+ methadone (1.9.2)
39
42
  bundler
40
43
  multi_json (1.11.0)
41
44
  multi_test (0.1.2)
@@ -57,8 +60,9 @@ GEM
57
60
  rspec-expectations (2.99.2)
58
61
  diff-lcs (>= 1.1.3, < 2.0)
59
62
  rspec-mocks (2.99.3)
60
- table_print (1.5.3)
63
+ table_print (1.5.6)
61
64
  timecop (0.3.5)
65
+ trollop (2.1.2)
62
66
 
63
67
  PLATFORMS
64
68
  ruby
@@ -74,3 +78,6 @@ DEPENDENCIES
74
78
  ronn
75
79
  rspec (~> 2.99)
76
80
  timecop
81
+
82
+ BUNDLED WITH
83
+ 1.11.2
data/README.org CHANGED
@@ -6,19 +6,22 @@ A network video recorder for multiple IP cameras using VLC.
6
6
 
7
7
  ** History
8
8
 
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.
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
10
 
11
- The problem was though VLC doesn't automate the recordings or handle the file structure nicely. This is where I started to think about creating an application which records from VLC and nicely sorts those recordings in directories by date & time.
11
+ I started with VLC doing the recording up to 0.1.0 where I changed over to using ffmpeg instead.
12
12
 
13
13
  ** Requirements
14
14
 
15
- - VLC - recording & motion detection
16
- - xvfb - Running virtual frame buffers (ie: desktops)
15
+ - ffmpeg - recording & motion detection
17
16
  - ruby
18
- - Linux (Tested on Debian 7)
17
+ - Linux (Tested on Debian 7, Xubuntu 14.04)
19
18
 
20
19
  ** Installation
21
20
 
21
+ Under linux install vlc xvfb & ruby
22
+
23
+ : sudo apt-get install ffmpeg ruby
24
+
22
25
  Add this line to your application's Gemfile:
23
26
 
24
27
  : gem 'electric_eye'
@@ -37,7 +40,8 @@ Enter your cameras into the JSON config file like so
37
40
 
38
41
  : ---
39
42
  : duration: 60
40
- : path: "/media/data/recordings/temp"
43
+ : path: "/media/data/recordings"
44
+ : threshold: 2
41
45
  : cameras:
42
46
  : - :name: Reception
43
47
  : :url: rtsp://<user>:<passwd>@<camera's ip>/live2.sdp
@@ -50,14 +54,15 @@ You should be able to view the URL through vlc before using this program.
50
54
 
51
55
  The recordings directory will end up with these directories
52
56
 
53
- : /media/data/recordings/reception/20150527
54
- : /media/data/recordings/kitchen/20150527
57
+ : /media/data/recordings/reception
58
+ : /media/data/recordings/kitchen
55
59
 
56
- Notice the date at the end of these paths, there will be one for each day. The contents within will be recordings which are done by default every 10minutes, example;
60
+ Files will be numbered up to your wrap figure. The wrap figure determines when the recording program should start from zero again. EG: If you select you duration as 3600 seconds and a wrap figure of 168 then you get a rolling recording over a 1 week period which would be divided up into 1hr files.
57
61
 
58
- : 20150527-1020-reception.mjpeg
59
- : 20150527-1030-reception.mjpeg
60
- : 20150527-1040-reception.mjpeg
62
+ : reception000.mjpeg
63
+ : motion-reception000.mjpeg
64
+ : reception001.mjpeg
65
+ : motion-reception001.mjpeg
61
66
 
62
67
  The default is going to be 10 minute blocks, this can be overridden with the duration variable above in minutes.
63
68
 
@@ -83,6 +88,9 @@ Usage in development mode
83
88
 
84
89
  : bundle exec bin/electric_eye -h
85
90
 
91
+ Debug mode
92
+
93
+ : bundle exec bin/electric_eye -s --log-level=debug
86
94
 
87
95
  ** Start on boot
88
96
 
@@ -133,6 +141,8 @@ Replace johnsmith with your user where you have setup your camera profiles. NOTE
133
141
 
134
142
  ** Cleanup
135
143
 
144
+ Optional - This was needed for versions prior to 0.1.0, now it is only a precaution as ffmpeg does clean up after itself.
145
+
136
146
  Cleaning up recordings. Put the following into your /etc/crontab per recording directory.
137
147
 
138
148
  : 00 19 * * * root /usr/bin/find <directory to recordings> -type f -mtime +<days> -exec rm {} \;
@@ -162,11 +172,17 @@ Example for cleaning up reception after 60days at 7pm everynight.
162
172
 
163
173
  - [X] Create threshold as a variable
164
174
 
165
- - [ ] Add a feature to clean up old recordings using a "period" setting
175
+ - [X] Swap over to using ffmpeg
176
+
177
+ - [X] Do post motion detection (using fmmpeg)
178
+
179
+ - [X] Add a feature to clean up old recordings using a "period" setting (ffmpeg handles this)
166
180
  EG: 60 day period which could be set in the config file how many days you want to keep
167
181
  Then just call 'electric_eye --remove-recordings' within crontab
168
182
  This would iterate over all my cameras and remove old recordings to keep a rolling set of days.
169
183
 
170
- - [ ] Allow different recording programs like raspicam
184
+ - [ ] Allow motion detection to be turned on/off (default: off)
171
185
 
172
- - [ ] Do inline motion detection (using activevlc)
186
+ - [ ] Threshold should be per camera or have inside & outside thresholds
187
+ There is a large difference in movement between indoor office cameras
188
+ and outdoor cameras. With wind and rain comes a lot of motion!
data/bin/electric_eye CHANGED
@@ -22,12 +22,18 @@ class App
22
22
  @configEye.list_cameras
23
23
  elsif options[:d] # Set duration
24
24
  @configEye.set_duration(options[:duration])
25
+ elsif options[:w] # Set wrap
26
+ @configEye.set_wrap(options[:wrap])
25
27
  elsif options[:p] # Set path
26
28
  @configEye.set_path(options[:path])
27
29
  elsif options[:s] # Start recording
28
30
  @record.start
29
31
  elsif options[:k] # Stop recordings
30
32
  @record.stop
33
+ # elsif options[:m] # Perform post motion detection
34
+ # # NOTE: This happens automatically during recording so this option is only here for admins
35
+ # # to perform the operation manually.
36
+ # @record.start_motion_detection(options[:listfile])
31
37
  elsif options[:t]
32
38
  @configEye.set_threshold(options[:threshold])
33
39
  else
@@ -46,9 +52,11 @@ class App
46
52
  on("-r", "--remove", "Remove a camera")
47
53
  on("-l", "--list", "List cameras")
48
54
  on("-d", "--duration SECONDS", "Set recording duration in seconds (default: 600)")
55
+ on("-w", "--wrap FILES", "Set how many files to keep before wrapping (default: 168 at 1hr = 1week)")
49
56
  on("-p", "--path DIR", "Set recordings path")
50
57
  on("-s", "--start", "Start recordings")
51
58
  on("-k", "--stop", "Stop recordings")
59
+ # on("-m", "--motion LISTFILE", "Post motion detection, pass in a list of video and it will process")
52
60
  on("-t", "--threshold LEVEL", "Set threshold for motion detection (default: 2)")
53
61
 
54
62
  # Arguments
data/electric_eye.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.add_runtime_dependency "construct"
22
22
  spec.add_runtime_dependency "table_print"
23
23
  spec.add_runtime_dependency "open4"
24
+ spec.add_runtime_dependency "filewatcher"
24
25
 
25
26
  spec.add_development_dependency "bundler", "~> 1.7"
26
27
  spec.add_development_dependency "rake", "~> 10.0"
@@ -2,7 +2,7 @@ require 'construct'
2
2
  require 'fileutils'
3
3
 
4
4
  Given(/^I have a camera called "([^"]*)"$/) do |arg1|
5
- @config = Construct.new
5
+ @config = Construct.new({path: "/tmp/temp", duration: 600, wrap: 168})
6
6
  @config.cameras = [{name: "Reception", url: "http://thecamera.org"}]
7
7
  dir="#{ENV['HOME']}/.electric_eye"
8
8
  FileUtils.rm_r(dir) if Dir.exist?(dir)
@@ -19,7 +19,8 @@ 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', threshold: 2, cameras: []})
22
+ # Create a new file with defaults
23
+ Construct.new({duration: 600, wrap: 168, path: '~/recordings', threshold: 2, cameras: []})
23
24
  end
24
25
  end
25
26
 
@@ -57,6 +58,13 @@ module ElectricEye
57
58
  tp @config.cameras, :name, :url => {width: 120}
58
59
  end
59
60
 
61
+ # Set wrap
62
+ def set_wrap(wrap)
63
+ @config.wrap = wrap.to_i
64
+ save
65
+ info "Wrap set to #{wrap} files"
66
+ end
67
+
60
68
  # Set duration
61
69
  def set_duration(seconds)
62
70
  @config.duration = seconds.to_i
@@ -1,6 +1,7 @@
1
1
  require 'methadone'
2
2
  require 'open4'
3
3
  require 'fileutils'
4
+ require 'filewatcher'
4
5
 
5
6
  module ElectricEye
6
7
  class Record
@@ -15,52 +16,69 @@ module ElectricEye
15
16
  @motion = Motion.new # Create a new instance method to our motion library
16
17
 
17
18
  pids = []
19
+ # Start the recording for each camera
18
20
  # Step through each camera
19
21
  @configEye.config.cameras.each do |camera|
20
- # Let's first record two 1minute videos of each camera.
21
- # This will turn into a never ending loop until we stop the process.
22
- pids << fork do
23
- stop_recording = false
24
- Signal.trap('INT') { stop_recording = true }
25
- until stop_recording
26
- path = "#{path(camera)}"
27
- debug "Recording #{camera[:name]} to #{path}.mjpeg..."
28
-
29
- # Set a recording going using vlc, hold onto the process till it's finished.
30
- cmd="cvlc #{camera[:url]} --sout file/ts:#{path}.mjpeg"
31
- pid,stdin,stdout,stderr=Open4::popen4(cmd)
32
-
33
- # Wait for a defined duration from the config file.
34
- seconds = @configEye.config.duration
35
- while(seconds > 0)
36
- sleep 1
37
- seconds -= 1
38
- break if stop_recording
39
- end
40
-
41
- Process.kill 9, pid # Stop current recording.
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
55
- end
56
- end
22
+
23
+ # until stop_recording
24
+ path = "#{path(camera)}"
25
+ listfile = "#{path}.list"
26
+ debug "Recording #{camera[:name]} to #{path}.mjpeg..."
57
27
 
28
+ # Set a recording going using vlc, hold onto the process till it's finished.
29
+ # segment_time = how much time to record in each segment in seconds, ie: 3600 = 1hr
30
+ # sgement_wrap = how many copies
31
+ loglevel = "-loglevel panic" if logger.level >= 1
32
+ cmd="ffmpeg -f mjpeg -i #{camera[:url]} #{loglevel} -acodec copy -vcodec copy -y -f segment -segment_list #{listfile} -segment_time #{@configEye.config.duration} -segment_wrap #{@configEye.config.wrap} #{path}%03d.mjpeg"
33
+
34
+ # Run command and add to our pids to make it easy for electric_eye to clean up.
35
+ info "Starting to record #{camera[:name]}"
36
+ pids << Process.spawn(cmd)
37
+
38
+ # Start the motion detection for this camera
39
+ puts "before thread: #{path}"
40
+
41
+ pids << fork do
42
+ `echo "path: #{dir(camera)}" >> #{listfile}.log`
43
+ start_motion_detection(camera)
44
+ end
58
45
  end
59
46
 
60
47
  store_pids(pids)
61
48
  info "Cameras recording"
62
49
  end
63
50
 
51
+ # Start motion detection
52
+ def start_motion_detection(camera)
53
+ # Watch the ffmpeg segment list output file which will trigger the block within
54
+ # where we can look at the last line in the file and perform post motion detection.
55
+
56
+ dir =dir(camera)
57
+ path = path(camera)
58
+
59
+ # Watch the directory & read from the list file
60
+ filewatcher = FileWatcher.new("#{path}*.mjpeg")
61
+ filewatcher.watch do |f|
62
+ file = read_listfile("#{path}.list")
63
+ if file
64
+ debug "Processing #{file}"
65
+ loglevel = "-loglevel panic" if logger.level >= 1
66
+
67
+ # Run motion detection on the file, make sure that we output to a different file.
68
+ cmd="ffmpeg -i #{dir}/#{file} #{loglevel} -y -vf \"select=gt(scene\\,0.003),setpts=N/(25*TB)\" #{dir}/motion-#{file}"
69
+
70
+ # Run command and add to our pids to make it easy for electric_eye to clean up.
71
+ Process.spawn(cmd)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Read the last line from the list file.
77
+ def read_listfile(listfile)
78
+ lines = File.open(listfile).readlines
79
+ lines.last.chomp! unless lines.length == 0
80
+ end
81
+
64
82
  # Remove a recording
65
83
  def remove(path)
66
84
  debug "REMOVE #{path}.mjpeg (no motion)"
@@ -82,10 +100,14 @@ module ElectricEye
82
100
  File.open(PID_FILE, "r").gets # Get pids
83
101
  end
84
102
 
85
- def path(camera)
103
+ def dir(camera)
86
104
  dir = "#{@configEye.config.path}/#{camera[:name]}"
87
105
  FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
88
- "#{dir}/#{Time.now.strftime('%Y%m%d-%H%M')}-#{camera[:name]}"
106
+ dir
107
+ end
108
+
109
+ def path(camera)
110
+ "#{dir(camera)}/#{camera[:name]}"
89
111
  end
90
112
 
91
113
  def initialize(configEye)
@@ -1,3 +1,3 @@
1
1
  module ElectricEye
2
- VERSION = "0.0.5"
2
+ VERSION = "0.1.0"
3
3
  end
data/spec/config_spec.rb CHANGED
@@ -148,6 +148,32 @@ describe "remove camera" do
148
148
  end
149
149
  end
150
150
 
151
+ describe "set_wrap" do
152
+ include FakeFS::SpecHelpers
153
+
154
+ before do
155
+ @configEye = ConfigEye.new
156
+ end
157
+
158
+ context "when no wrap has been set" do
159
+ it "returns the default of 168 times" do
160
+ expect(@configEye.config.wrap).to equal(168)
161
+ end
162
+ end
163
+
164
+ context "when calling with -w 24" do
165
+ it "returns 24" do
166
+ @configEye.set_wrap(24)
167
+ expect(@configEye.config.wrap).to equal(24)
168
+ end
169
+
170
+ it "calls save" do
171
+ expect(@configEye).to receive(:save).once
172
+ @configEye.set_wrap(24)
173
+ end
174
+ end
175
+ end
176
+
151
177
  describe "set_duration" do
152
178
  include FakeFS::SpecHelpers
153
179
 
data/spec/record_spec.rb CHANGED
@@ -27,6 +27,21 @@ describe "record" do
27
27
  end
28
28
  end
29
29
 
30
+ describe "#dir" do
31
+ before do
32
+ Timecop.freeze(Time.local(2015,06,30,10,05,0))
33
+ end
34
+
35
+ after do
36
+ Timecop.return
37
+ end
38
+
39
+ it "returns a full dir" do
40
+ dir = @record.dir(@configEye.config.cameras.first)
41
+ expect(dir).to eq("~/recordings/Reception")
42
+ end
43
+ end
44
+
30
45
  describe "record_path" do
31
46
  before do
32
47
  Timecop.freeze(Time.local(2015,06,30,10,05,0))
@@ -36,9 +51,9 @@ describe "record" do
36
51
  Timecop.return
37
52
  end
38
53
 
39
- it "returns a full path with todays date" do
54
+ it "returns a full path" do
40
55
  path = @record.path(@configEye.config.cameras.first)
41
- expect(path).to include("~/recordings/Reception/20150630-1005-Reception")
56
+ expect(path).to eq("~/recordings/Reception/Reception")
42
57
  end
43
58
  end
44
59
 
@@ -60,8 +75,8 @@ describe "record" do
60
75
  end
61
76
 
62
77
  it "calls kill" do
63
- open4 = mock(Open4)
64
- open4.stub!(:exitstatus).and_return(0)
78
+ open4 = double(Open4)
79
+ open4.stub(:exitstatus).and_return(0)
65
80
 
66
81
  Open4.should_receive(:popen4).with('kill -INT 10000 10001 10002').and_return(open4)
67
82
 
@@ -75,6 +90,40 @@ describe "record" do
75
90
  end
76
91
  end
77
92
 
93
+ describe "#read_listfile" do
94
+ before do
95
+ FakeFS.activate!
96
+ end
97
+
98
+ after do
99
+ FakeFS.deactivate!
100
+ end
101
+
102
+ context "with a full file" do
103
+ before do
104
+ File.open("output.list", "w") do |file|
105
+ file.puts "file1.mjpeg"
106
+ file.puts "file2.mjpeg"
107
+ end
108
+ end
109
+
110
+ it "returns the last filename" do
111
+ @record.read_listfile("output.list").should eq("file2.mjpeg")
112
+ end
113
+ end
114
+
115
+ context "with a empty file" do
116
+ before do
117
+ File.open("output.list", "w") do |file|
118
+ end
119
+ end
120
+
121
+ it "returns the last filename" do
122
+ @record.read_listfile("output.list").should eq(nil)
123
+ end
124
+ end
125
+ end
126
+
78
127
  describe "#remove" do
79
128
  before do
80
129
  @path = "/tmp/electric_eye"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: electric_eye
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Pope
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-01 00:00:00.000000000 Z
11
+ date: 2016-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: construct
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: filewatcher
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -202,6 +216,7 @@ extensions: []
202
216
  extra_rdoc_files: []
203
217
  files:
204
218
  - ".gitignore"
219
+ - ".ruby-version"
205
220
  - Gemfile
206
221
  - Gemfile.lock
207
222
  - LICENSE.txt
@@ -251,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
251
266
  version: '0'
252
267
  requirements: []
253
268
  rubyforge_project:
254
- rubygems_version: 2.2.2
269
+ rubygems_version: 2.4.5.1
255
270
  signing_key:
256
271
  specification_version: 4
257
272
  summary: Network Video Recorder