vidibus-recording 0.0.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -3,8 +3,9 @@
3
3
  Allows recording of RTMP video streams. Uses RTMPdump.
4
4
  Further description goes here.
5
5
 
6
+ Requires Ruby 1.9
6
7
 
7
8
 
8
9
  == Copyright
9
10
 
10
- Copyright (c) 2011 Andre Pankratz. See LICENSE for details.
11
+ Copyright (c) 2013 Andre Pankratz. See LICENSE for details.
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,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../config/environment', __FILE__)
4
+ require 'vidibus/recorder/daemon'
5
+
6
+ Vidibus::WatchFolder::Daemon.new(ARGV).daemonize
@@ -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] or raise ConfigurationError.new("No input stream given")
18
- self.file = attributes[:file] or raise ConfigurationError.new("No output file defined")
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
- %(rtmpdump #{args.join(" ")} 2>&1)
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; license: GPL
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] or raise ConfigurationError.new("No input stream given")
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
@@ -32,4 +32,4 @@ module Vidibus::Recording
32
32
  value
33
33
  end
34
34
  end
35
- end
35
+ 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 job now, unless it has been done already.
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? or started?
35
+ return false if done? || started?
36
36
  if time == :now
37
- job.start
38
- update_attributes(:pid => job.pid, :started_at => Time.now)
39
- job.pid
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
- # Resets data and stars anew.
46
- def restart(time = :now)
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(time)
60
+ start
50
61
  end
51
62
 
52
- # Stops the recording job and starts postprocessing.
63
+ # Stops the recording worker and starts postprocessing.
53
64
  def stop
54
- return false if !started_at? or done?
55
- job.stop
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 job and stores it.
62
- # The job gets stopped and postprocessing is started.
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 if done?
65
- job.stop
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
- # Removes all acquired data
95
+ # TODO: really a public method?
96
+ # Removes all acquired data!
73
97
  def reset
74
98
  remove_files
75
99
  blank = {}
76
- [:started_at, :stopped_at, :failed_at, :info, :log, :error, :size, :duration].map {|a| blank[a] = nil }
77
- update_attributes(blank)
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
- # Returns an instance of the recording job.
81
- def job
82
- @job ||= Vidibus::Recording::Job.new(self)
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(:stream => stream, :file => file, :live => live)
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
- stopped_at or failed?
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 if job has been started.
140
+ # Returns true if recording has been started.
101
141
  def started?
102
142
  !!started_at
103
143
  end
104
144
 
105
- # Returns true if recording job is still running.
106
- def running?
107
- started? and job.running?
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 = ["recordings"]
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
- # Returns the file name of this recording.
122
- def file
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 ||= file.gsub(/\.[^\.]+$/, ".log")
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
- @info_file ||= file.gsub(/\.[^\.]+$/, ".yml")
194
+ @yml_file ||= "#{basename}.yml"
134
195
  end
135
196
 
136
- protected
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 postprocess
143
- process_yml_file
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
- def process_yml_file
150
- if str = read_file(yml_file)
151
- if values = YAML::load(str)
152
- fix_value_classes!(values)
153
- self.info = values
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
- self.size = File.exists?(file) ? File.size(file) : nil
264
+ accumulate_parts(:size)
160
265
  end
161
266
 
162
267
  def set_duration
163
- self.duration = failed? ? 0 : Time.now - started_at
268
+ accumulate_parts(:duration)
164
269
  end
165
270
 
166
- def read_file(file)
167
- if File.exists?(file)
168
- str = File.read(file)
169
- File.delete(file)
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
- job.stop
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
@@ -1,5 +1,5 @@
1
1
  module Vidibus
2
2
  module Recording
3
- VERSION = '0.0.7'
3
+ VERSION = '1.0.0'
4
4
  end
5
5
  end
@@ -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
@@ -1,4 +1,6 @@
1
- require 'vidibus/recording/job'
1
+ require 'vidibus/recording/worker'
2
+ require 'vidibus/recording/monitoring_job'
2
3
  require 'vidibus/recording/backend'
3
4
  require 'vidibus/recording/helpers'
5
+ require 'vidibus/recording/part'
4
6
  require 'vidibus/recording/mongoid'
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.7
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-05-30 00:00:00.000000000 Z
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: rcov
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: '0'
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: '0'
141
+ version: '2'
142
142
  - !ruby/object:Gem::Dependency
143
- name: rspec
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: '2'
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: '2'
157
+ version: '0'
158
158
  - !ruby/object:Gem::Dependency
159
- name: rr
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