electric_eye 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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