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 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