vidibus-recording 0.0.7 → 1.0.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.
- data/README.rdoc +2 -1
- data/Rakefile +14 -0
- data/lib/generators/vidibus/recording_generator.rb +14 -0
- data/lib/generators/vidibus/templates/script +6 -0
- data/lib/vidibus/recording/backend/railtie.rb +1 -0
- data/lib/vidibus/recording/backend/rtmpdump.rb +29 -5
- data/lib/vidibus/recording/backend.rb +6 -3
- data/lib/vidibus/recording/capistrano/recipes.rb +40 -0
- data/lib/vidibus/recording/capistrano.rb +12 -0
- data/lib/vidibus/recording/daemon.rb +46 -0
- data/lib/vidibus/recording/helpers.rb +1 -1
- data/lib/vidibus/recording/mongoid.rb +163 -57
- data/lib/vidibus/recording/monitoring_job.rb +52 -0
- data/lib/vidibus/recording/part.rb +106 -0
- data/lib/vidibus/recording/railtie.rb +1 -0
- data/lib/vidibus/recording/version.rb +1 -1
- data/lib/vidibus/recording/worker.rb +135 -0
- data/lib/vidibus/recording.rb +3 -1
- metadata +26 -14
- data/lib/vidibus/recording/job.rb +0 -101
data/README.rdoc
CHANGED
data/Rakefile
CHANGED
@@ -1,2 +1,16 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib/', __FILE__)
|
2
|
+
|
1
3
|
require 'bundler'
|
4
|
+
require 'rdoc/task'
|
5
|
+
require 'rspec'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
|
2
8
|
Bundler::GemHelper.install_tasks
|
9
|
+
|
10
|
+
Rake::RDocTask.new do |rdoc|
|
11
|
+
rdoc.rdoc_dir = 'rdoc'
|
12
|
+
rdoc.title = 'Vidibus::Encoder'
|
13
|
+
rdoc.rdoc_files.include('README*')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
rdoc.options << '--charset=utf-8'
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/named_base'
|
3
|
+
|
4
|
+
module Vidibus
|
5
|
+
class RecorderGenerator < Rails::Generators::Base
|
6
|
+
|
7
|
+
self.source_paths << File.join(File.dirname(__FILE__), 'templates')
|
8
|
+
|
9
|
+
def create_script_file
|
10
|
+
template 'script', 'script/recording'
|
11
|
+
chmod 'script/recording', 0755
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
railtie.rb
|
@@ -5,6 +5,14 @@ module Vidibus::Recording::Backend
|
|
5
5
|
|
6
6
|
attr_accessor :stream, :file, :live, :metadata
|
7
7
|
|
8
|
+
def self.executable=(path)
|
9
|
+
@executable = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.executable
|
13
|
+
@executable || 'rtmpdump'
|
14
|
+
end
|
15
|
+
|
8
16
|
# Sets up a new dumper.
|
9
17
|
#
|
10
18
|
# Required attributes:
|
@@ -14,9 +22,11 @@ module Vidibus::Recording::Backend
|
|
14
22
|
# :live
|
15
23
|
#
|
16
24
|
def initialize(attributes)
|
17
|
-
self.stream = attributes[:stream]
|
18
|
-
self.file = attributes[:file]
|
25
|
+
self.stream = attributes[:stream]
|
26
|
+
self.file = attributes[:file]
|
19
27
|
self.live = attributes[:live]
|
28
|
+
raise ConfigurationError.new('No output file defined') unless file
|
29
|
+
raise ConfigurationError.new('No input stream given') unless stream
|
20
30
|
end
|
21
31
|
|
22
32
|
# Command for starting the recording.
|
@@ -26,16 +36,15 @@ module Vidibus::Recording::Backend
|
|
26
36
|
a << "-o #{file}"
|
27
37
|
a << "--live" if live
|
28
38
|
end
|
29
|
-
%(
|
39
|
+
%(#{self.class.executable} #{args.join(" ")} 2>&1)
|
30
40
|
end
|
31
41
|
|
32
42
|
# Extract metadata from stdout or stderr.
|
33
43
|
# Output delivered by rtmpdump looks like this:
|
34
44
|
#
|
35
45
|
# RTMPDump v2.2
|
36
|
-
# (c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team
|
46
|
+
# (c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team
|
37
47
|
# Connecting ...
|
38
|
-
# ERROR: rtmp server sent error
|
39
48
|
# Starting Live Stream
|
40
49
|
# Metadata:
|
41
50
|
# author
|
@@ -69,5 +78,20 @@ module Vidibus::Recording::Backend
|
|
69
78
|
self.metadata = Hash[tuples]
|
70
79
|
end
|
71
80
|
end
|
81
|
+
|
82
|
+
# Extract metadata from stdout or stderr.
|
83
|
+
# Output delivered by rtmpdump looks like this:
|
84
|
+
#
|
85
|
+
# RTMPDump v2.4
|
86
|
+
# (c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team
|
87
|
+
# Connecting ...
|
88
|
+
# ERROR: Problem accessing the DNS. (addr: whatever.domain)
|
89
|
+
#
|
90
|
+
def detect_error(string)
|
91
|
+
prefix = /(?:ERROR\:\ (.+))/ if string.match(/ERROR\:/)
|
92
|
+
if string.match(/(?:ERROR\:\ (.+))/)
|
93
|
+
raise RuntimeError.new($1)
|
94
|
+
end
|
95
|
+
end
|
72
96
|
end
|
73
97
|
end
|
@@ -1,5 +1,8 @@
|
|
1
|
+
require 'vidibus/recording/backend/rtmpdump'
|
2
|
+
|
1
3
|
module Vidibus::Recording
|
2
4
|
module Backend
|
5
|
+
class RuntimeError < StandardError; end
|
3
6
|
class ConfigurationError < StandardError; end
|
4
7
|
class ProtocolError < ConfigurationError; end
|
5
8
|
|
@@ -8,12 +11,12 @@ module Vidibus::Recording
|
|
8
11
|
# Returns an instance of a backend processor
|
9
12
|
# that is able to record the given stream.
|
10
13
|
def self.load(attributes)
|
11
|
-
stream = attributes[:stream]
|
14
|
+
stream = attributes[:stream]
|
15
|
+
raise ConfigurationError.new("No input stream given") unless stream
|
12
16
|
protocol = stream.match(/^[^:]+/).to_s
|
13
|
-
raise ProtocolError.new(%(No protocol could be derived stream "#{stream}")) if protocol ==
|
17
|
+
raise ProtocolError.new(%(No protocol could be derived stream "#{stream}")) if protocol == ''
|
14
18
|
|
15
19
|
for backend in BACKENDS
|
16
|
-
require "vidibus/recording/backend/#{backend}"
|
17
20
|
backend_class = "Vidibus::Recording::Backend::#{backend.classify}".constantize
|
18
21
|
if backend_class::PROTOCOLS.include?(protocol)
|
19
22
|
return backend_class.new(attributes)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Capistrano Recipes for watching folders.
|
2
|
+
#
|
3
|
+
# Load this file from your Capistrano config.rb:
|
4
|
+
# require 'vidibus/recording/capistrano/recipes'
|
5
|
+
#
|
6
|
+
# Add these callbacks to have the recording process restart when the server
|
7
|
+
# is restarted:
|
8
|
+
#
|
9
|
+
# after 'deploy:stop', 'vidibus:recording:stop'
|
10
|
+
# after 'deploy:start', 'vidibus:recording:start'
|
11
|
+
# after 'deploy:restart', 'vidibus:recording:restart'
|
12
|
+
#
|
13
|
+
Capistrano::Configuration.instance.load do
|
14
|
+
namespace :vidibus do
|
15
|
+
namespace :recording do
|
16
|
+
def rails_env
|
17
|
+
fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
|
18
|
+
end
|
19
|
+
|
20
|
+
def roles
|
21
|
+
fetch(:app)
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Stop the recording process'
|
25
|
+
task :stop, :roles => lambda { roles } do
|
26
|
+
run "cd #{current_path};#{rails_env} script/recording stop"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Start the recording process'
|
30
|
+
task :start, :roles => lambda { roles } do
|
31
|
+
run "cd #{current_path};#{rails_env} script/recording start"
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'Restart the recording process'
|
35
|
+
task :restart, :roles => lambda { roles } do
|
36
|
+
run "cd #{current_path};#{rails_env} script/recording restart"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'vidibus/recording/capistrano/recipes'
|
2
|
+
|
3
|
+
# Run Capistrano Recipes for watching folders.
|
4
|
+
#
|
5
|
+
# Load this file from your Capistrano config.rb:
|
6
|
+
# require 'vidibus/watch_folder/capistrano'
|
7
|
+
#
|
8
|
+
Capistrano::Configuration.instance.load do
|
9
|
+
after 'deploy:stop', 'vidibus:watch_folder:stop'
|
10
|
+
after 'deploy:start', 'vidibus:watch_folder:start'
|
11
|
+
after 'deploy:restart', 'vidibus:watch_folder:restart'
|
12
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
begin
|
2
|
+
require 'daemons'
|
3
|
+
rescue LoadError
|
4
|
+
raise %(Please add `gem 'daemons' gem to your Gemfile for this to work)
|
5
|
+
end
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
module Vidibus
|
9
|
+
module Recorder
|
10
|
+
class Daemon
|
11
|
+
|
12
|
+
def initialize(args)
|
13
|
+
@options = {:pid_dir => "#{Rails.root}/tmp/pids"}
|
14
|
+
options = OptionParser.new do |options|
|
15
|
+
options.banner = "Usage: #{File.basename($0)} start|stop|restart"
|
16
|
+
options.on('-h', '--help', 'Show this message') do
|
17
|
+
puts options
|
18
|
+
exit 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
@args = options.parse!(args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def daemonize
|
25
|
+
dir = @options[:pid_dir]
|
26
|
+
Dir.mkdir(dir) unless File.exists?(dir)
|
27
|
+
run_process('recording', dir)
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_process(name, dir)
|
31
|
+
Daemons.run_proc(name, :dir => dir, :dir_mode => :normal) { run }
|
32
|
+
end
|
33
|
+
|
34
|
+
def run
|
35
|
+
Dir.chdir(Rails.root)
|
36
|
+
log = File.join(Rails.root, 'log', 'recording.log')
|
37
|
+
Vidibus::Recorder.logger = ActiveSupport::BufferedLogger.new(log)
|
38
|
+
Vidibus::Recorder.monitor
|
39
|
+
rescue => e
|
40
|
+
Vidibus::Recorder.logger.fatal(e)
|
41
|
+
STDERR.puts(e.message)
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -2,17 +2,15 @@ module Vidibus::Recording
|
|
2
2
|
module Mongoid
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
class ProcessError < StandardError; end
|
6
|
-
class StreamError < StandardError; end
|
7
|
-
|
8
5
|
included do
|
9
6
|
include ::Mongoid::Timestamps
|
10
|
-
include Vidibus::Recording::Helpers
|
11
7
|
include Vidibus::Uuid::Mongoid
|
12
8
|
|
9
|
+
embeds_many :parts, :as => :recording, :class_name => 'Vidibus::Recording::Part'
|
10
|
+
|
13
11
|
field :name
|
14
12
|
field :stream
|
15
|
-
field :live, :type => Boolean
|
13
|
+
# field :live, :type => Boolean
|
16
14
|
field :pid, :type => Integer
|
17
15
|
field :info, :type => Hash
|
18
16
|
field :size, :type => Integer
|
@@ -22,6 +20,8 @@ module Vidibus::Recording
|
|
22
20
|
field :started_at, :type => DateTime
|
23
21
|
field :stopped_at, :type => DateTime
|
24
22
|
field :failed_at, :type => DateTime
|
23
|
+
field :running, :type => Boolean, :default => false
|
24
|
+
field :monitoring_job_identifier, :type => String
|
25
25
|
|
26
26
|
validates :name, :presence => true
|
27
27
|
validates :stream, :format => {:with => /^rtmp.*?:\/\/.+$/}
|
@@ -29,67 +29,107 @@ module Vidibus::Recording
|
|
29
29
|
before_destroy :cleanup
|
30
30
|
end
|
31
31
|
|
32
|
-
# Starts a recording
|
32
|
+
# Starts a recording worker now, unless it has been done already.
|
33
33
|
# Provide a Time object to schedule start.
|
34
34
|
def start(time = :now)
|
35
|
-
return false if done?
|
35
|
+
return false if done? || started?
|
36
36
|
if time == :now
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
self.started_at = Time.now
|
38
|
+
start_worker
|
39
|
+
start_monitoring_job
|
40
|
+
save!
|
40
41
|
else
|
41
42
|
schedule(time)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
45
|
-
#
|
46
|
-
def
|
46
|
+
# Continue recording that is not running anymore.
|
47
|
+
def resume
|
48
|
+
return false if running? || !started?
|
49
|
+
self.stopped_at = nil
|
50
|
+
self.failed_at = nil
|
51
|
+
start_worker
|
52
|
+
start_monitoring_job
|
53
|
+
save!
|
54
|
+
end
|
55
|
+
|
56
|
+
# Resets data and starts anew.
|
57
|
+
def restart
|
47
58
|
stop
|
48
59
|
reset
|
49
|
-
start
|
60
|
+
start
|
50
61
|
end
|
51
62
|
|
52
|
-
# Stops the recording
|
63
|
+
# Stops the recording worker and starts postprocessing.
|
53
64
|
def stop
|
54
|
-
return false if
|
55
|
-
|
65
|
+
return false if done? || !started?
|
66
|
+
worker.stop
|
56
67
|
self.pid = nil
|
57
68
|
self.stopped_at = Time.now
|
69
|
+
self.running = false
|
70
|
+
self.monitoring_job_identifier = nil
|
71
|
+
postprocess
|
72
|
+
end
|
73
|
+
|
74
|
+
# Gets called from recording worker if it receives no more data.
|
75
|
+
def halt(msg = nil)
|
76
|
+
return false unless running?
|
77
|
+
worker.stop
|
78
|
+
self.pid = nil
|
79
|
+
self.running = false
|
58
80
|
postprocess
|
59
81
|
end
|
60
82
|
|
61
|
-
# Receives an error from recording
|
62
|
-
# The
|
83
|
+
# Receives an error from recording worker and stores it.
|
84
|
+
# The worker gets stopped and postprocessing is started.
|
63
85
|
def fail(msg)
|
64
|
-
return
|
65
|
-
|
86
|
+
return false unless running?
|
87
|
+
worker.stop
|
66
88
|
self.pid = nil
|
67
89
|
self.error = msg
|
68
90
|
self.failed_at = Time.now
|
91
|
+
self.running = false
|
69
92
|
postprocess
|
70
93
|
end
|
71
94
|
|
72
|
-
#
|
95
|
+
# TODO: really a public method?
|
96
|
+
# Removes all acquired data!
|
73
97
|
def reset
|
74
98
|
remove_files
|
75
99
|
blank = {}
|
76
|
-
[
|
77
|
-
|
100
|
+
[
|
101
|
+
:started_at,
|
102
|
+
:stopped_at,
|
103
|
+
:failed_at,
|
104
|
+
:info,
|
105
|
+
:error,
|
106
|
+
:size,
|
107
|
+
:duration,
|
108
|
+
:monitoring_job_identifier
|
109
|
+
].map {|a| blank[a] = nil }
|
110
|
+
update_attributes!(blank)
|
111
|
+
destroy_all_parts
|
78
112
|
end
|
79
113
|
|
80
|
-
#
|
81
|
-
|
82
|
-
|
114
|
+
# TODO: really a public method?
|
115
|
+
# Returns an instance of the recording worker.
|
116
|
+
def worker
|
117
|
+
@worker ||= Vidibus::Recording::Worker.new(self)
|
83
118
|
end
|
84
119
|
|
120
|
+
# TODO: really a public method?
|
85
121
|
# Returns an instance of a fitting recording backend.
|
86
122
|
def backend
|
87
|
-
@backend ||= Vidibus::Recording::Backend.load(
|
123
|
+
@backend ||= Vidibus::Recording::Backend.load({
|
124
|
+
:stream => stream,
|
125
|
+
:file => current_part.data_file,
|
126
|
+
:live => true
|
127
|
+
})
|
88
128
|
end
|
89
129
|
|
90
130
|
# Returns true if recording has either been stopped or failed.
|
91
131
|
def done?
|
92
|
-
|
132
|
+
stopped? || failed?
|
93
133
|
end
|
94
134
|
|
95
135
|
# Returns true if recording has failed.
|
@@ -97,20 +137,35 @@ module Vidibus::Recording
|
|
97
137
|
!!failed_at
|
98
138
|
end
|
99
139
|
|
100
|
-
# Returns true if
|
140
|
+
# Returns true if recording has been started.
|
101
141
|
def started?
|
102
142
|
!!started_at
|
103
143
|
end
|
104
144
|
|
105
|
-
|
106
|
-
|
107
|
-
|
145
|
+
def stopped?
|
146
|
+
!!stopped_at
|
147
|
+
end
|
148
|
+
|
149
|
+
def has_data?
|
150
|
+
size.to_i > 0
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns true if recording worker is still running.
|
154
|
+
# Persists attributes accordingly.
|
155
|
+
def worker_running?
|
156
|
+
if worker.running?
|
157
|
+
update_attributes(:running => true) unless running?
|
158
|
+
true
|
159
|
+
else
|
160
|
+
update_attributes(:pid => nil, :running => false)
|
161
|
+
false
|
162
|
+
end
|
108
163
|
end
|
109
164
|
|
110
165
|
# Return folder to store recordings in.
|
111
166
|
def folder
|
112
167
|
@folder ||= begin
|
113
|
-
f = [
|
168
|
+
f = ['recordings']
|
114
169
|
f.unshift(Rails.root) if defined?(Rails)
|
115
170
|
path = File.join(f)
|
116
171
|
FileUtils.mkdir_p(path) unless File.exist?(path)
|
@@ -118,64 +173,115 @@ module Vidibus::Recording
|
|
118
173
|
end
|
119
174
|
end
|
120
175
|
|
121
|
-
|
122
|
-
|
123
|
-
@file ||= "#{folder}/#{uuid}.rec"
|
176
|
+
def basename
|
177
|
+
"#{folder}/#{uuid}"
|
124
178
|
end
|
125
179
|
|
126
180
|
# Returns the log file name for this recording.
|
127
181
|
def log_file
|
128
|
-
@log_file ||=
|
182
|
+
@log_file ||= "#{basename}.log"
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the file name of this recording.
|
186
|
+
# DEPRECATED: this is kept for existing records only.
|
187
|
+
def file
|
188
|
+
@file ||= "#{basename}.rec"
|
129
189
|
end
|
130
190
|
|
131
191
|
# Returns the YAML file name for this recording.
|
192
|
+
# DEPRECATED: this is kept for existing records only.
|
132
193
|
def yml_file
|
133
|
-
@
|
194
|
+
@yml_file ||= "#{basename}.yml"
|
134
195
|
end
|
135
196
|
|
136
|
-
|
137
|
-
|
138
|
-
def schedule(time)
|
139
|
-
self.delay(:run_at => time).start
|
197
|
+
def current_part
|
198
|
+
parts.last
|
140
199
|
end
|
141
200
|
|
142
|
-
def
|
143
|
-
|
201
|
+
def track_progress
|
202
|
+
current_part.track_progress if current_part
|
144
203
|
set_size
|
145
204
|
set_duration
|
146
205
|
save!
|
147
206
|
end
|
148
207
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
208
|
+
private
|
209
|
+
|
210
|
+
def destroy_all_parts
|
211
|
+
parts.each do |part|
|
212
|
+
part.destroy
|
213
|
+
end
|
214
|
+
self.update_attributes!(:parts => [])
|
215
|
+
end
|
216
|
+
|
217
|
+
def start_worker
|
218
|
+
return if worker_running?
|
219
|
+
setup_next_part
|
220
|
+
worker.start
|
221
|
+
self.running = true
|
222
|
+
self.pid = worker.pid
|
223
|
+
end
|
224
|
+
|
225
|
+
# Start a new monitoring job
|
226
|
+
def start_monitoring_job
|
227
|
+
self.monitoring_job_identifier = Vidibus::Uuid.generate
|
228
|
+
Vidibus::Recording::MonitoringJob.create({
|
229
|
+
:class_name => self.class.to_s,
|
230
|
+
:uuid => uuid,
|
231
|
+
:identifier => monitoring_job_identifier
|
232
|
+
})
|
233
|
+
end
|
234
|
+
|
235
|
+
def setup_next_part
|
236
|
+
number = nil
|
237
|
+
if current_part
|
238
|
+
if current_part.has_data?
|
239
|
+
number = current_part.number + 1
|
240
|
+
else
|
241
|
+
current_part.reset
|
154
242
|
end
|
243
|
+
else
|
244
|
+
number = 1
|
155
245
|
end
|
246
|
+
if number
|
247
|
+
parts.build(:number => number)
|
248
|
+
end
|
249
|
+
current_part.start
|
250
|
+
end
|
251
|
+
|
252
|
+
def schedule(time)
|
253
|
+
self.delay(:run_at => time).start
|
254
|
+
end
|
255
|
+
|
256
|
+
def postprocess
|
257
|
+
current_part.postprocess if current_part
|
258
|
+
set_size
|
259
|
+
set_duration
|
260
|
+
save!
|
156
261
|
end
|
157
262
|
|
158
263
|
def set_size
|
159
|
-
|
264
|
+
accumulate_parts(:size)
|
160
265
|
end
|
161
266
|
|
162
267
|
def set_duration
|
163
|
-
|
268
|
+
accumulate_parts(:duration)
|
164
269
|
end
|
165
270
|
|
166
|
-
def
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
str
|
271
|
+
def accumulate_parts(attr)
|
272
|
+
value = 0
|
273
|
+
parts.each do |part|
|
274
|
+
value += part.send(attr).to_i
|
171
275
|
end
|
276
|
+
self.send("#{attr}=", value)
|
172
277
|
end
|
173
278
|
|
174
279
|
def cleanup
|
175
|
-
|
280
|
+
worker.stop
|
176
281
|
remove_files
|
177
282
|
end
|
178
283
|
|
284
|
+
# DEPRECATED: this is kept for existing records only.
|
179
285
|
def remove_files
|
180
286
|
[file, log_file, yml_file].each do |f|
|
181
287
|
File.delete(f) if File.exists?(f)
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Vidibus::Recording
|
2
|
+
class MonitoringJob
|
3
|
+
INTERVAL = 10.seconds
|
4
|
+
|
5
|
+
def initialize(args)
|
6
|
+
unless @uuid = args[:uuid]
|
7
|
+
raise(ArgumentError, 'No recording UUID given')
|
8
|
+
end
|
9
|
+
unless @class_name = args[:class_name]
|
10
|
+
raise(ArgumentError, 'Must provide class name of recording')
|
11
|
+
end
|
12
|
+
unless @identifier = args[:identifier]
|
13
|
+
raise(ArgumentError, 'Must provide identifier of monitoring job')
|
14
|
+
end
|
15
|
+
ensure_recording
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform
|
19
|
+
r = recording.reload
|
20
|
+
return unless r.monitoring_job_identifier == @identifier
|
21
|
+
if r.worker_running?
|
22
|
+
r.track_progress
|
23
|
+
run_again
|
24
|
+
elsif !r.stopped?
|
25
|
+
r.resume
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns job
|
30
|
+
def self.create(args)
|
31
|
+
job = new(args)
|
32
|
+
Delayed::Job.enqueue(job)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def recording
|
38
|
+
@class_name.constantize.where(:uuid => @uuid).first
|
39
|
+
end
|
40
|
+
|
41
|
+
def ensure_recording
|
42
|
+
recording || raise(ArgumentError, 'No valid recording UUID given')
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_again
|
46
|
+
obj = self.class.new({
|
47
|
+
:uuid => @uuid, :class_name => @class_name, :identifier => @identifier
|
48
|
+
})
|
49
|
+
Delayed::Job.enqueue(obj, 0, INTERVAL.from_now)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Vidibus::Recording
|
2
|
+
class Part
|
3
|
+
include Mongoid::Document
|
4
|
+
include Mongoid::Timestamps
|
5
|
+
include Vidibus::Recording::Helpers
|
6
|
+
|
7
|
+
SIZE_THRESHOLD = 2000
|
8
|
+
|
9
|
+
embedded_in :recording, :polymorphic => true
|
10
|
+
|
11
|
+
field :number, :type => Integer
|
12
|
+
field :info, :type => Hash
|
13
|
+
field :size, :type => Integer
|
14
|
+
field :duration, :type => Integer
|
15
|
+
field :started_at, :type => DateTime
|
16
|
+
field :stopped_at, :type => DateTime
|
17
|
+
|
18
|
+
validates :number, :presence => true
|
19
|
+
|
20
|
+
before_destroy :remove_files
|
21
|
+
|
22
|
+
# Returns the file path of this part.
|
23
|
+
def data_file
|
24
|
+
@data_file ||= "#{basename}.f4v"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the YAML file path of this part.
|
28
|
+
def yml_file
|
29
|
+
@yml_file ||= "#{basename}.yml"
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_data?
|
33
|
+
size.to_i >= SIZE_THRESHOLD
|
34
|
+
end
|
35
|
+
|
36
|
+
def stopped?
|
37
|
+
!!stopped_at
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset
|
41
|
+
remove_files
|
42
|
+
blanks = {}
|
43
|
+
[
|
44
|
+
:info,
|
45
|
+
:size,
|
46
|
+
:duration,
|
47
|
+
:started_at
|
48
|
+
].map {|a| blanks[a] = nil }
|
49
|
+
update_attributes(blanks)
|
50
|
+
end
|
51
|
+
|
52
|
+
def track_progress
|
53
|
+
set_size
|
54
|
+
set_duration
|
55
|
+
end
|
56
|
+
|
57
|
+
def postprocess
|
58
|
+
process_yml_file
|
59
|
+
track_progress
|
60
|
+
self.stopped_at = Time.now
|
61
|
+
# save!
|
62
|
+
end
|
63
|
+
|
64
|
+
def start
|
65
|
+
self.started_at = Time.now
|
66
|
+
self.stopped_at = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def process_yml_file
|
72
|
+
if str = read_and_delete_file(yml_file)
|
73
|
+
if values = YAML::load(str)
|
74
|
+
fix_value_classes!(values)
|
75
|
+
self.info = values
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_size
|
81
|
+
self.size = File.exists?(data_file) ? File.size(data_file) : 0
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_duration
|
85
|
+
self.duration = has_data? ? Time.now - started_at : 0
|
86
|
+
end
|
87
|
+
|
88
|
+
def read_and_delete_file(file)
|
89
|
+
if File.exists?(file)
|
90
|
+
str = File.read(file)
|
91
|
+
File.delete(file)
|
92
|
+
str
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def basename
|
97
|
+
"#{_parent.basename}_#{number}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def remove_files
|
101
|
+
[data_file, yml_file].each do |f|
|
102
|
+
File.delete(f) if File.exists?(f)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
railtie.rb
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module Vidibus::Recording
|
4
|
+
class Worker
|
5
|
+
class ProcessError < StandardError; end
|
6
|
+
|
7
|
+
# START_TIMEOUT = 20
|
8
|
+
STOP_TIMEOUT = 10
|
9
|
+
|
10
|
+
attr_accessor :recording, :pid, :metadata
|
11
|
+
|
12
|
+
def initialize(recording)
|
13
|
+
self.recording = recording
|
14
|
+
self.pid = recording.pid
|
15
|
+
self.metadata = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
self.pid = fork do
|
20
|
+
begin
|
21
|
+
record
|
22
|
+
rescue => e
|
23
|
+
fail(e.inspect)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
Process.detach(pid)
|
27
|
+
pid
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
if running?
|
32
|
+
begin
|
33
|
+
Timeout::timeout(STOP_TIMEOUT) do
|
34
|
+
begin
|
35
|
+
log("Stopping process #{pid}...")
|
36
|
+
# Use SIGQUIT to terminate because DelayedJob traps INT and TERM
|
37
|
+
Process.kill('SIGQUIT', pid)
|
38
|
+
Process.wait(pid)
|
39
|
+
log('STOPPED')
|
40
|
+
rescue Errno::ECHILD
|
41
|
+
log('STOPPED')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue Timeout::Error
|
45
|
+
begin
|
46
|
+
log("Killing process #{pid}")
|
47
|
+
Process.kill('KILL', pid)
|
48
|
+
Process.wait(pid)
|
49
|
+
log('KILLED')
|
50
|
+
rescue Errno::ECHILD
|
51
|
+
log('KILLED')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def running?
|
58
|
+
return false unless pid
|
59
|
+
begin
|
60
|
+
Process.kill(0, pid)
|
61
|
+
return true
|
62
|
+
rescue Errno::ESRCH
|
63
|
+
return false
|
64
|
+
rescue Errno::EPERM
|
65
|
+
raise ProcessError.new("No permission to check process #{pid}")
|
66
|
+
rescue
|
67
|
+
raise ProcessError.new("Unable to determine status of process #{pid}: #{$!}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def record
|
74
|
+
cmd = recording.backend.command
|
75
|
+
log("START: #{recording.stream}", true)
|
76
|
+
Open3::popen3(cmd) do |stdin, stdout, stderr|
|
77
|
+
maxloops = 10
|
78
|
+
loop do
|
79
|
+
begin
|
80
|
+
string = stdout.read_nonblock(1024).force_encoding('UTF-8')
|
81
|
+
log(string)
|
82
|
+
extract_metadata(string) unless metadata
|
83
|
+
recording.backend.detect_error(string)
|
84
|
+
rescue Errno::EAGAIN
|
85
|
+
rescue EOFError
|
86
|
+
if metadata
|
87
|
+
halt('No more data!') && break
|
88
|
+
end
|
89
|
+
rescue Backend::RuntimeError => e
|
90
|
+
fail(e.message) && break
|
91
|
+
end
|
92
|
+
unless metadata
|
93
|
+
maxloops -= 1
|
94
|
+
if maxloops == 0
|
95
|
+
halt('No Metadata has been received so far.') && break
|
96
|
+
end
|
97
|
+
end
|
98
|
+
sleep 2
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def log(msg, print_header = false)
|
104
|
+
if print_header
|
105
|
+
header = "--- #{Time.now.strftime('%F %R:%S %z')}"
|
106
|
+
header << " | Process #{Process.pid}"
|
107
|
+
msg = "#{header}\n#{msg}\n"
|
108
|
+
end
|
109
|
+
msg = "\n#{msg}" unless msg[/A\n/]
|
110
|
+
File.open(recording.log_file, "a") do |f|
|
111
|
+
f.write(msg)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def fail(msg)
|
116
|
+
log("ERROR: #{msg}", true)
|
117
|
+
recording.reload.fail(msg)
|
118
|
+
end
|
119
|
+
|
120
|
+
def halt(msg)
|
121
|
+
log("HALT: #{msg}", true)
|
122
|
+
recording.reload.halt(msg)
|
123
|
+
end
|
124
|
+
|
125
|
+
def extract_metadata(string)
|
126
|
+
self.metadata = recording.backend.extract_metadata(string)
|
127
|
+
if metadata
|
128
|
+
File.open(recording.current_part.yml_file, 'w') do |f|
|
129
|
+
f.write(metadata.to_yaml)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
metadata
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/vidibus/recording.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vidibus-recording
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-07-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -124,39 +124,39 @@ dependencies:
|
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '0'
|
126
126
|
- !ruby/object:Gem::Dependency
|
127
|
-
name:
|
127
|
+
name: rspec
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
129
129
|
none: false
|
130
130
|
requirements:
|
131
|
-
- -
|
131
|
+
- - ~>
|
132
132
|
- !ruby/object:Gem::Version
|
133
|
-
version: '
|
133
|
+
version: '2'
|
134
134
|
type: :development
|
135
135
|
prerelease: false
|
136
136
|
version_requirements: !ruby/object:Gem::Requirement
|
137
137
|
none: false
|
138
138
|
requirements:
|
139
|
-
- -
|
139
|
+
- - ~>
|
140
140
|
- !ruby/object:Gem::Version
|
141
|
-
version: '
|
141
|
+
version: '2'
|
142
142
|
- !ruby/object:Gem::Dependency
|
143
|
-
name:
|
143
|
+
name: rr
|
144
144
|
requirement: !ruby/object:Gem::Requirement
|
145
145
|
none: false
|
146
146
|
requirements:
|
147
|
-
- -
|
147
|
+
- - ! '>='
|
148
148
|
- !ruby/object:Gem::Version
|
149
|
-
version: '
|
149
|
+
version: '0'
|
150
150
|
type: :development
|
151
151
|
prerelease: false
|
152
152
|
version_requirements: !ruby/object:Gem::Requirement
|
153
153
|
none: false
|
154
154
|
requirements:
|
155
|
-
- -
|
155
|
+
- - ! '>='
|
156
156
|
- !ruby/object:Gem::Version
|
157
|
-
version: '
|
157
|
+
version: '0'
|
158
158
|
- !ruby/object:Gem::Dependency
|
159
|
-
name:
|
159
|
+
name: simplecov
|
160
160
|
requirement: !ruby/object:Gem::Requirement
|
161
161
|
none: false
|
162
162
|
requirements:
|
@@ -177,12 +177,21 @@ executables: []
|
|
177
177
|
extensions: []
|
178
178
|
extra_rdoc_files: []
|
179
179
|
files:
|
180
|
+
- lib/generators/vidibus/recording_generator.rb
|
181
|
+
- lib/generators/vidibus/templates/script
|
182
|
+
- lib/vidibus/recording/backend/railtie.rb
|
180
183
|
- lib/vidibus/recording/backend/rtmpdump.rb
|
181
184
|
- lib/vidibus/recording/backend.rb
|
185
|
+
- lib/vidibus/recording/capistrano/recipes.rb
|
186
|
+
- lib/vidibus/recording/capistrano.rb
|
187
|
+
- lib/vidibus/recording/daemon.rb
|
182
188
|
- lib/vidibus/recording/helpers.rb
|
183
|
-
- lib/vidibus/recording/job.rb
|
184
189
|
- lib/vidibus/recording/mongoid.rb
|
190
|
+
- lib/vidibus/recording/monitoring_job.rb
|
191
|
+
- lib/vidibus/recording/part.rb
|
192
|
+
- lib/vidibus/recording/railtie.rb
|
185
193
|
- lib/vidibus/recording/version.rb
|
194
|
+
- lib/vidibus/recording/worker.rb
|
186
195
|
- lib/vidibus/recording.rb
|
187
196
|
- lib/vidibus-recording.rb
|
188
197
|
- app/models/recording.rb
|
@@ -201,6 +210,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
201
210
|
- - ! '>='
|
202
211
|
- !ruby/object:Gem::Version
|
203
212
|
version: '0'
|
213
|
+
segments:
|
214
|
+
- 0
|
215
|
+
hash: 766807753862642645
|
204
216
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
205
217
|
none: false
|
206
218
|
requirements:
|
@@ -1,101 +0,0 @@
|
|
1
|
-
# TODO: extend from Vidibus::Loop
|
2
|
-
|
3
|
-
module Vidibus::Recording
|
4
|
-
class Job
|
5
|
-
class ProcessError < StandardError; end
|
6
|
-
|
7
|
-
attr_accessor :recording, :pid, :metadata
|
8
|
-
|
9
|
-
def initialize(recording)
|
10
|
-
self.recording = recording
|
11
|
-
self.pid = recording.pid
|
12
|
-
self.metadata = nil
|
13
|
-
end
|
14
|
-
|
15
|
-
def start
|
16
|
-
self.pid = fork do
|
17
|
-
begin
|
18
|
-
record!
|
19
|
-
rescue => e
|
20
|
-
fail(e.inspect)
|
21
|
-
return
|
22
|
-
end
|
23
|
-
end
|
24
|
-
Process.detach(pid)
|
25
|
-
pid
|
26
|
-
end
|
27
|
-
|
28
|
-
def stop
|
29
|
-
if pid and running?
|
30
|
-
Process.kill("SIGTERM", pid)
|
31
|
-
sleep 2
|
32
|
-
raise ProcessError.new("Recording job is still running!") if running?
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def running?
|
37
|
-
pid and self.class.running?(pid)
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.running?(pid)
|
41
|
-
begin
|
42
|
-
Process.kill(0, pid)
|
43
|
-
return true
|
44
|
-
rescue Errno::ESRCH
|
45
|
-
return false
|
46
|
-
rescue Errno::EPERM
|
47
|
-
raise ProcessError.new("No permission to check #{pid}")
|
48
|
-
rescue
|
49
|
-
raise ProcessError.new("Unable to determine status for #{pid}: #{$!}")
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
protected
|
54
|
-
|
55
|
-
def record!
|
56
|
-
Open3::popen3(recording.backend.command) do |stdin, stdout, stderr, process|
|
57
|
-
maxloops = 10
|
58
|
-
loop do
|
59
|
-
begin
|
60
|
-
string = stdout.read_nonblock(1024).force_encoding('UTF-8')
|
61
|
-
log(string)
|
62
|
-
extract_metadata(string) unless metadata
|
63
|
-
rescue Errno::EAGAIN
|
64
|
-
rescue EOFError
|
65
|
-
end
|
66
|
-
|
67
|
-
unless metadata
|
68
|
-
maxloops -= 1
|
69
|
-
if maxloops == 0
|
70
|
-
fail('No Metadata has been received. This stream does not work.')
|
71
|
-
return
|
72
|
-
end
|
73
|
-
end
|
74
|
-
sleep 2
|
75
|
-
end
|
76
|
-
end
|
77
|
-
process.join
|
78
|
-
end
|
79
|
-
|
80
|
-
def log(msg)
|
81
|
-
File.open(recording.log_file, "a") do |f|
|
82
|
-
f.write(msg)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def fail(msg)
|
87
|
-
log("\n\n---------\nError:\n#{msg}")
|
88
|
-
recording.fail(msg)
|
89
|
-
end
|
90
|
-
|
91
|
-
def extract_metadata(string)
|
92
|
-
self.metadata = recording.backend.extract_metadata(string)
|
93
|
-
if metadata
|
94
|
-
File.open(recording.yml_file, "w") do |f|
|
95
|
-
f.write(metadata.to_yaml)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
metadata
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|