vidibus-recording 0.0.1
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/.gitignore +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +75 -0
- data/LICENSE +20 -0
- data/README.rdoc +10 -0
- data/Rakefile +2 -0
- data/app/models/recording.rb +4 -0
- data/lib/vidibus/recording/backend/rtmpdump.rb +66 -0
- data/lib/vidibus/recording/backend.rb +25 -0
- data/lib/vidibus/recording/helpers.rb +35 -0
- data/lib/vidibus/recording/job.rb +103 -0
- data/lib/vidibus/recording/mongoid.rb +195 -0
- data/lib/vidibus/recording/version.rb +5 -0
- data/lib/vidibus/recording.rb +4 -0
- data/lib/vidibus-recording.rb +17 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/vidibus/recording/backend_spec.rb +29 -0
- data/spec/vidibus/recording/mongoid_spec.rb +111 -0
- data/vidibus-recording.gemspec +33 -0
- metadata +236 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
vidibus-recording (0.0.1)
|
5
|
+
delayed_job_mongoid
|
6
|
+
mongoid (~> 2.0.0.beta.20)
|
7
|
+
open4
|
8
|
+
robustthread
|
9
|
+
vidibus-uuid
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: http://rubygems.org/
|
13
|
+
specs:
|
14
|
+
activemodel (3.0.3)
|
15
|
+
activesupport (= 3.0.3)
|
16
|
+
builder (~> 2.1.2)
|
17
|
+
i18n (~> 0.4)
|
18
|
+
activesupport (3.0.3)
|
19
|
+
bson (1.2.0)
|
20
|
+
builder (2.1.2)
|
21
|
+
daemons (1.1.0)
|
22
|
+
delayed_job (2.1.3)
|
23
|
+
activesupport (~> 3.0)
|
24
|
+
daemons
|
25
|
+
delayed_job_mongoid (1.0.2)
|
26
|
+
delayed_job (~> 2.1.1)
|
27
|
+
mongoid (~> 2.0.0.rc)
|
28
|
+
diff-lcs (1.1.2)
|
29
|
+
i18n (0.5.0)
|
30
|
+
macaddr (1.0.0)
|
31
|
+
mongo (1.2.0)
|
32
|
+
bson (>= 1.2.0)
|
33
|
+
mongoid (2.0.0.rc.6)
|
34
|
+
activemodel (~> 3.0)
|
35
|
+
mongo (~> 1.2)
|
36
|
+
tzinfo (~> 0.3.22)
|
37
|
+
will_paginate (~> 3.0.pre)
|
38
|
+
open4 (1.0.1)
|
39
|
+
rake (0.8.7)
|
40
|
+
relevance-rcov (0.9.2.1)
|
41
|
+
robustthread (0.5.2)
|
42
|
+
rr (1.0.2)
|
43
|
+
rspec (2.0.1)
|
44
|
+
rspec-core (~> 2.0.1)
|
45
|
+
rspec-expectations (~> 2.0.1)
|
46
|
+
rspec-mocks (~> 2.0.1)
|
47
|
+
rspec-core (2.0.1)
|
48
|
+
rspec-expectations (2.0.1)
|
49
|
+
diff-lcs (>= 1.1.2)
|
50
|
+
rspec-mocks (2.0.1)
|
51
|
+
rspec-core (~> 2.0.1)
|
52
|
+
rspec-expectations (~> 2.0.1)
|
53
|
+
tzinfo (0.3.24)
|
54
|
+
uuid (2.3.1)
|
55
|
+
macaddr (~> 1.0)
|
56
|
+
vidibus-uuid (0.3.8)
|
57
|
+
mongoid (~> 2.0.0.beta.20)
|
58
|
+
uuid (~> 2.3.1)
|
59
|
+
will_paginate (3.0.pre2)
|
60
|
+
|
61
|
+
PLATFORMS
|
62
|
+
ruby
|
63
|
+
|
64
|
+
DEPENDENCIES
|
65
|
+
bundler (>= 1.0.0)
|
66
|
+
delayed_job_mongoid
|
67
|
+
mongoid (~> 2.0.0.beta.20)
|
68
|
+
open4
|
69
|
+
rake
|
70
|
+
relevance-rcov
|
71
|
+
robustthread
|
72
|
+
rr
|
73
|
+
rspec (~> 2.0.0.beta.20)
|
74
|
+
vidibus-recording!
|
75
|
+
vidibus-uuid
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Andre Pankratz
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Vidibus::Recording::Backend
|
2
|
+
class Rtmpdump
|
3
|
+
|
4
|
+
PROTOCOLS = %[rtmp rtmpt rtmpe rtmpte rtmps rtmpts]
|
5
|
+
|
6
|
+
attr_accessor :stream, :file, :live, :metadata
|
7
|
+
|
8
|
+
def initialize(attributes)
|
9
|
+
self.stream = attributes[:stream] or raise ConfigurationError.new("No input stream given")
|
10
|
+
self.file = attributes[:file] or raise ConfigurationError.new("No output file defined")
|
11
|
+
self.live = attributes[:live]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Command for starting the recording.
|
15
|
+
# Required options:
|
16
|
+
# :stream, :file
|
17
|
+
# Optional:
|
18
|
+
# :live
|
19
|
+
#
|
20
|
+
def command(options = {})
|
21
|
+
c = %(rtmpdump -r "#{stream}" -o #{file})
|
22
|
+
c << " --live" if live
|
23
|
+
c
|
24
|
+
end
|
25
|
+
|
26
|
+
# Extract metadata from stdout or stderr.
|
27
|
+
# Output delivered by rtmpdump looks like this:
|
28
|
+
#
|
29
|
+
# RTMPDump v2.2
|
30
|
+
# (c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team; license: GPL
|
31
|
+
# Connecting ...
|
32
|
+
# ERROR: rtmp server sent error
|
33
|
+
# Starting Live Stream
|
34
|
+
# Metadata:
|
35
|
+
# author
|
36
|
+
# copyright
|
37
|
+
# description
|
38
|
+
# keywords
|
39
|
+
# rating
|
40
|
+
# title
|
41
|
+
# presetname Custom
|
42
|
+
# creationdate Mon Jan 17 15:22:50 2011
|
43
|
+
# videodevice Osprey-210 Video Device 1
|
44
|
+
# framerate 25.00
|
45
|
+
# width 680.00
|
46
|
+
# height 394.00
|
47
|
+
# videocodecid avc1
|
48
|
+
# videodatarate 650.00
|
49
|
+
# avclevel 31.00
|
50
|
+
# avcprofile 66.00
|
51
|
+
# videokeyframe_frequency5.00
|
52
|
+
# audiodevice Osprey-210 Audio Device 1
|
53
|
+
# audiosamplerate 22050.00
|
54
|
+
# audiochannels 1.00
|
55
|
+
# audioinputvolume 75.00
|
56
|
+
# audiocodecid .mp3
|
57
|
+
# audiodatarate 48.00
|
58
|
+
#
|
59
|
+
def extract_metadata(std)
|
60
|
+
if metadata = std.match(/Metadata\:\n\s+(.+)\Z/m)
|
61
|
+
tuples = $1.scan(/([^\n\s\d]+)\s*([^\n]+)\n/m)
|
62
|
+
self.metadata = Hash[tuples]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Vidibus::Recording
|
2
|
+
module Backend
|
3
|
+
class ConfigurationError < StandardError; end
|
4
|
+
class ProtocolError < ConfigurationError; end
|
5
|
+
|
6
|
+
BACKENDS = %[rtmpdump]
|
7
|
+
|
8
|
+
# Returns an instance of a backend processor
|
9
|
+
# that is able to record the given stream.
|
10
|
+
def self.load(attributes)
|
11
|
+
stream = attributes[:stream] or raise ConfigurationError.new("No input stream given")
|
12
|
+
protocol = stream.match(/^[^:]+/).to_s
|
13
|
+
raise ProtocolError.new(%(No protocol could be derived stream "#{stream}")) if protocol == ""
|
14
|
+
|
15
|
+
for backend in BACKENDS
|
16
|
+
require "recording/backend/#{backend}"
|
17
|
+
backend_class = "Vidibus::Recording::Backend::#{backend.classify}".constantize
|
18
|
+
if backend_class::PROTOCOLS.include?(protocol)
|
19
|
+
return backend_class.new(attributes)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
raise ProtocolError.new(%(No recording backend available for "#{protocol}" protocol.))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Vidibus::Recording
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
# Recursively fixes classes of value strings
|
5
|
+
def fix_value_classes!(value)
|
6
|
+
c = value.class
|
7
|
+
|
8
|
+
# Get nested items in Hash
|
9
|
+
if c == Hash
|
10
|
+
value.each do |v|
|
11
|
+
value[v[0]] = fix_value_classes!(v[1])
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get nested items in Array
|
15
|
+
elsif c == Array
|
16
|
+
value.each_with_index do |v,i|
|
17
|
+
value[i] = fix_value_classes!(v)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Fix classes of values
|
21
|
+
else
|
22
|
+
if value.match /^\d+[\.,]\d+$/
|
23
|
+
value = value.to_f
|
24
|
+
elsif value.match /^\d+$/
|
25
|
+
value = value.to_i
|
26
|
+
elsif value.match /^true$/
|
27
|
+
value = true
|
28
|
+
elsif value.match /^false$/
|
29
|
+
value = false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Vidibus::Recording
|
2
|
+
class Job
|
3
|
+
include Open4
|
4
|
+
|
5
|
+
class ProcessError < StandardError; end
|
6
|
+
|
7
|
+
attr_accessor :recording, :pid
|
8
|
+
|
9
|
+
def initialize(recording)
|
10
|
+
self.recording = recording
|
11
|
+
self.pid = recording.pid
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
start_logger
|
16
|
+
self.pid = fork do
|
17
|
+
start_thread
|
18
|
+
end
|
19
|
+
Process.detach(pid)
|
20
|
+
pid
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop
|
24
|
+
if pid and running?
|
25
|
+
Process.kill("SIGTERM", pid)
|
26
|
+
sleep 2
|
27
|
+
raise ProcessError.new("Recording job is still running!") if running?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def running?
|
32
|
+
pid and self.class.running?(pid)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.running?(pid)
|
36
|
+
begin
|
37
|
+
Process.kill(0, pid)
|
38
|
+
return true
|
39
|
+
rescue Errno::ESRCH
|
40
|
+
return false
|
41
|
+
rescue Errno::EPERM
|
42
|
+
raise ProcessError.new("No permission to check #{pid}")
|
43
|
+
rescue
|
44
|
+
raise ProcessError.new("Unable to determine status for #{pid}: #{$!}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def start_thread
|
51
|
+
stdin = ""
|
52
|
+
stdout = ""
|
53
|
+
stderr = ""
|
54
|
+
last_stderr = ""
|
55
|
+
last_stdout = ""
|
56
|
+
task = background(recording.backend.command, 0=>stdin, 1=>stdout, 2=>stderr)
|
57
|
+
|
58
|
+
waiter = Thread.new {y(task.pid => task.exitstatus)} # t.exitstatus is a blocking call!
|
59
|
+
timeout = 5
|
60
|
+
|
61
|
+
while(status = task.status)
|
62
|
+
unless stderr == last_stderr
|
63
|
+
metadata = extract_metadata(stderr)
|
64
|
+
last_stderr = stderr
|
65
|
+
end
|
66
|
+
|
67
|
+
unless stdout == last_stdout
|
68
|
+
metadata = extract_metadata(stdout)
|
69
|
+
last_stdout = stdout
|
70
|
+
end
|
71
|
+
|
72
|
+
unless metadata
|
73
|
+
timeout -= 1
|
74
|
+
if timeout == 0
|
75
|
+
recording.fail("No Metadata has been received. This stream does not work.")
|
76
|
+
return
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
sleep 2
|
81
|
+
end
|
82
|
+
|
83
|
+
waiter.join
|
84
|
+
end
|
85
|
+
|
86
|
+
def extract_metadata(std)
|
87
|
+
metadata = recording.backend.extract_metadata(std)
|
88
|
+
if metadata
|
89
|
+
File.open(recording.yml_file, "w") do |file|
|
90
|
+
file.write(metadata.to_yaml)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
metadata
|
94
|
+
end
|
95
|
+
|
96
|
+
def start_logger
|
97
|
+
RobustThread.logger = Logger.new(recording.log_file)
|
98
|
+
RobustThread.exception_handler do |exception|
|
99
|
+
RobustThread.log exception
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module Vidibus::Recording
|
2
|
+
module Mongoid
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class ProcessError < StandardError; end
|
6
|
+
class StreamError < StandardError; end
|
7
|
+
|
8
|
+
included do
|
9
|
+
include Vidibus::Recording::Helpers
|
10
|
+
include Vidibus::Uuid::Mongoid
|
11
|
+
|
12
|
+
field :name
|
13
|
+
field :stream
|
14
|
+
field :live, :type => Boolean
|
15
|
+
field :pid, :type => Integer
|
16
|
+
field :info, :type => Hash
|
17
|
+
field :size, :type => Integer
|
18
|
+
field :duration, :type => Integer
|
19
|
+
field :log
|
20
|
+
field :error
|
21
|
+
field :scheduled_at, :type => DateTime
|
22
|
+
field :started_at, :type => DateTime
|
23
|
+
field :stopped_at, :type => DateTime
|
24
|
+
field :failed_at, :type => DateTime
|
25
|
+
|
26
|
+
validates :name, :presence => true
|
27
|
+
validates :stream, :format => {:with => /^rtmp.*?:\/\/.+$/}
|
28
|
+
|
29
|
+
before_destroy :cleanup
|
30
|
+
end
|
31
|
+
|
32
|
+
# Starts a recording job now, unless it has been done already.
|
33
|
+
# Provide a Time object to schedule start.
|
34
|
+
def start(time = :now)
|
35
|
+
return false if done? or started?
|
36
|
+
if time == :now
|
37
|
+
job.start
|
38
|
+
update_attributes(:pid => job.pid, :started_at => Time.now)
|
39
|
+
job.pid
|
40
|
+
else
|
41
|
+
schedule(time)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Resets data and stars anew.
|
46
|
+
def restart(time = :now)
|
47
|
+
stop
|
48
|
+
reset
|
49
|
+
start(time)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Stops the recording job and starts postprocessing.
|
53
|
+
def stop
|
54
|
+
return false if !started_at? or done?
|
55
|
+
job.stop
|
56
|
+
self.pid = nil
|
57
|
+
self.stopped_at = Time.now
|
58
|
+
postprocess
|
59
|
+
end
|
60
|
+
|
61
|
+
# Receives an error from recording job and stores it.
|
62
|
+
# The job gets stopped and postprocessing is started.
|
63
|
+
def fail(msg)
|
64
|
+
return if done?
|
65
|
+
job.stop
|
66
|
+
self.pid = nil
|
67
|
+
self.error = msg
|
68
|
+
self.failed_at = Time.now
|
69
|
+
postprocess
|
70
|
+
end
|
71
|
+
|
72
|
+
# Removes all acquired data
|
73
|
+
def reset
|
74
|
+
remove_files
|
75
|
+
blank = {}
|
76
|
+
[:started_at, :stopped_at, :failed_at, :info, :log, :error, :size, :duration].map {|a| blank[a] = nil }
|
77
|
+
update_attributes(blank)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns an instance of the recording job.
|
81
|
+
def job
|
82
|
+
@job ||= Vidibus::Recording::Job.new(self)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns an instance of a fitting recording backend.
|
86
|
+
def backend
|
87
|
+
@backend ||= Vidibus::Recording::Backend.load(:stream => stream, :file => file, :live => live)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns true if recording has either been stopped or failed.
|
91
|
+
def done?
|
92
|
+
stopped_at or failed?
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns true if recording has failed.
|
96
|
+
def failed?
|
97
|
+
!!failed_at
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns true if if job has been started.
|
101
|
+
def started?
|
102
|
+
!!started_at
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns true if recording job is still running.
|
106
|
+
def running?
|
107
|
+
started? and job.running?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return folder to store recordings in.
|
111
|
+
def folder
|
112
|
+
@folder ||= begin
|
113
|
+
f = ["recordings"]
|
114
|
+
f.unshift(Rails.root) if defined?(Rails)
|
115
|
+
path = File.join(f)
|
116
|
+
FileUtils.mkdir_p(path) unless File.exist?(path)
|
117
|
+
path
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns the file name of this recording.
|
122
|
+
def file
|
123
|
+
@file ||= "#{folder}/#{uuid}.rec"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns the log file name for this recording.
|
127
|
+
def log_file
|
128
|
+
@log_file ||= file.gsub(/\.[^\.]+$/, ".log")
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns the YAML file name for this recording.
|
132
|
+
def yml_file
|
133
|
+
@info_file ||= file.gsub(/\.[^\.]+$/, ".yml")
|
134
|
+
end
|
135
|
+
|
136
|
+
protected
|
137
|
+
|
138
|
+
def schedule(time)
|
139
|
+
self.delay(:run_at => time).start
|
140
|
+
end
|
141
|
+
|
142
|
+
def postprocess
|
143
|
+
process_log_file
|
144
|
+
process_yml_file
|
145
|
+
set_size
|
146
|
+
set_duration
|
147
|
+
save!
|
148
|
+
end
|
149
|
+
|
150
|
+
def process_log_file
|
151
|
+
if str = read_file(log_file)
|
152
|
+
str.gsub!(/\A[^\n]+\n/, "") # remove first line
|
153
|
+
unless str == ""
|
154
|
+
self.log = str
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def process_yml_file
|
160
|
+
if str = read_file(yml_file)
|
161
|
+
if values = YAML::load(str)
|
162
|
+
fix_value_classes!(values)
|
163
|
+
self.info = values
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def set_size
|
169
|
+
self.size = File.exists?(file) ? File.size(file) : nil
|
170
|
+
end
|
171
|
+
|
172
|
+
def set_duration
|
173
|
+
self.duration = failed? ? 0 : Time.now - started_at
|
174
|
+
end
|
175
|
+
|
176
|
+
def read_file(file)
|
177
|
+
if File.exists?(file)
|
178
|
+
str = File.read(file)
|
179
|
+
File.delete(file)
|
180
|
+
str
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def cleanup
|
185
|
+
job.stop
|
186
|
+
remove_files
|
187
|
+
end
|
188
|
+
|
189
|
+
def remove_files
|
190
|
+
[file, log_file, yml_file].each do |f|
|
191
|
+
File.delete(f) if File.exists?(f)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "open4"
|
2
|
+
require "yaml"
|
3
|
+
require "robustthread"
|
4
|
+
require "delayed_job_mongoid"
|
5
|
+
require "active_support/core_ext"
|
6
|
+
require "vidibus-uuid"
|
7
|
+
|
8
|
+
module Vidibus
|
9
|
+
module Recording
|
10
|
+
if defined?(Rails)
|
11
|
+
class Engine < ::Rails::Engine; end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
$:.unshift(File.join(File.dirname(__FILE__), "vidibus"))
|
17
|
+
require "recording"
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
3
|
+
|
4
|
+
require "rubygems"
|
5
|
+
require "rspec"
|
6
|
+
require "rr"
|
7
|
+
require "mongoid"
|
8
|
+
|
9
|
+
require "vidibus-recording"
|
10
|
+
require "app/models/recording"
|
11
|
+
|
12
|
+
Mongoid.configure do |config|
|
13
|
+
name = "vidibus-recording_test"
|
14
|
+
host = "localhost"
|
15
|
+
config.master = Mongo::Connection.new.db(name)
|
16
|
+
config.logger = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.mock_with :rr
|
21
|
+
config.before(:each) do
|
22
|
+
Mongoid.master.collections.select {|c| c.name !~ /system/}.each(&:drop)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Helper for stubbing time. Define String to be set as Time.now.
|
27
|
+
# Usage:
|
28
|
+
# stub_time('01.01.2010 14:00')
|
29
|
+
# stub_time(2.days.ago)
|
30
|
+
#
|
31
|
+
def stub_time(string = nil)
|
32
|
+
string ||= Time.now.to_s(:db)
|
33
|
+
now = Time.parse(string.to_s)
|
34
|
+
stub(Time).now { now }
|
35
|
+
now
|
36
|
+
end
|
37
|
+
|
38
|
+
#I18n.load_path += Dir[File.join('config', 'locales', '**', '*.{rb,yml}')]
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Vidibus::Recording::Backend" do
|
4
|
+
|
5
|
+
describe ".load" do
|
6
|
+
it "should return an error unless a stream attribute is given" do
|
7
|
+
expect {
|
8
|
+
Vidibus::Recording::Backend.load({})
|
9
|
+
}.to raise_error(Vidibus::Recording::Backend::ConfigurationError)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should return a backend instance for given stream protocol" do
|
13
|
+
Vidibus::Recording::Backend.load(:stream => "rtmp://something", :file => "test").
|
14
|
+
should be_an_instance_of(Vidibus::Recording::Backend::Rtmpdump)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return an error unless a stream attribute with consumable protocol is given" do
|
18
|
+
expect {
|
19
|
+
Vidibus::Recording::Backend.load({:stream => "mms://something", :file => "test"})
|
20
|
+
}.to raise_error(Vidibus::Recording::Backend::ProtocolError)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return an error unless a stream attribute with a protocol is given" do
|
24
|
+
expect {
|
25
|
+
Vidibus::Recording::Backend.load({:stream => "something", :file => "test"})
|
26
|
+
}.to raise_error(Vidibus::Recording::Backend::ProtocolError)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Vidibus::Recording::Mongoid" do
|
4
|
+
|
5
|
+
let(:this) do
|
6
|
+
Recording.new(:name => "N-TV Live", :stream => "rtmp://fms.rtl.de/ntvlive/livestream/channel1", :live => true)
|
7
|
+
end
|
8
|
+
|
9
|
+
def cleanup(recording)
|
10
|
+
Process.kill("SIGTERM", recording.pid) if recording.pid
|
11
|
+
delete_safely(recording.file)
|
12
|
+
delete_safely(recording.log_file)
|
13
|
+
delete_safely(recording.yml_file)
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete_safely(file)
|
17
|
+
return unless file.match(/.{32}\..{3}/)
|
18
|
+
File.delete(file) if File.exists?(file)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "validation" do
|
22
|
+
it "should pass with valid attributes" do
|
23
|
+
this.should be_valid
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should fail without a stream" do
|
27
|
+
this.stream = nil
|
28
|
+
this.should be_invalid
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should fail without a valid stream address" do
|
32
|
+
this.stream = "something"
|
33
|
+
this.should be_invalid
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should fail without a valid rtmp stream address" do
|
37
|
+
this.stream = "rtmp://something"
|
38
|
+
this.should be_valid
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should fail without a name" do
|
42
|
+
this.name = nil
|
43
|
+
this.should be_invalid
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "job" do
|
48
|
+
it "should return a job instance" do
|
49
|
+
this.job.should be_an_instance_of(Vidibus::Recording::Job)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "backend" do
|
54
|
+
it "should return a backend instance for given stream protocol" do
|
55
|
+
this.backend.should be_an_instance_of(Vidibus::Recording::Backend::Rtmpdump)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "start" do
|
60
|
+
before {this.save}
|
61
|
+
|
62
|
+
it "should return a process id" do
|
63
|
+
this.start.should be_a(Fixnum)
|
64
|
+
end
|
65
|
+
|
66
|
+
context "without params" do
|
67
|
+
it "should start a recording job now" do
|
68
|
+
pid = this.start
|
69
|
+
Vidibus::Recording::Job.running?(pid).should be_true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "with a given Time" do
|
74
|
+
it "should schedule a recording job" do
|
75
|
+
stub_time("2011-01-12 00:00")
|
76
|
+
run_at = 10.minutes.since
|
77
|
+
this.start(run_at)
|
78
|
+
Delayed::Backend::Mongoid::Job.count.should eql(1)
|
79
|
+
Delayed::Backend::Mongoid::Job.first.run_at.should eql(run_at)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
after {cleanup(this)}
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "stop" do
|
87
|
+
before {this.save}
|
88
|
+
|
89
|
+
it "should return false unless recording has been started" do
|
90
|
+
this.stop.should be_false
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should return false if recording is done" do
|
94
|
+
this.stopped_at = Time.now
|
95
|
+
this.stop.should be_false
|
96
|
+
end
|
97
|
+
|
98
|
+
context "with started job" do
|
99
|
+
before {this.start}
|
100
|
+
|
101
|
+
it "should stop the recording job" do
|
102
|
+
pid = this.pid
|
103
|
+
this.stop
|
104
|
+
sleep 1
|
105
|
+
Vidibus::Recording::Job.running?(pid).should be_false
|
106
|
+
end
|
107
|
+
|
108
|
+
after {cleanup(this)}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/vidibus/recording/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "vidibus-recording"
|
6
|
+
s.rubyforge_project = "vidibus-recording"
|
7
|
+
s.version = Vidibus::Recording::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = "Andre Pankratz"
|
10
|
+
s.email = "andre@vidibus.com"
|
11
|
+
s.homepage = "http://rubygems.org/gems/vidibus-rtmpdump"
|
12
|
+
s.summary = "Video stream recording tools"
|
13
|
+
s.description = "Allows recording of RTMP video streams. Uses RTMPdump."
|
14
|
+
|
15
|
+
s.required_rubygems_version = ">= 1.3.6"
|
16
|
+
|
17
|
+
s.add_dependency "mongoid", "~> 2.0.0.beta.20"
|
18
|
+
s.add_dependency "open4"
|
19
|
+
s.add_dependency "robustthread"
|
20
|
+
s.add_dependency "delayed_job_mongoid"
|
21
|
+
|
22
|
+
s.add_dependency "vidibus-uuid"
|
23
|
+
|
24
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
25
|
+
s.add_development_dependency "rake"
|
26
|
+
s.add_development_dependency "rspec", "~> 2.0.0.beta.20"
|
27
|
+
s.add_development_dependency "rr"
|
28
|
+
s.add_development_dependency "relevance-rcov"
|
29
|
+
|
30
|
+
s.files = `git ls-files`.split("\n")
|
31
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
32
|
+
s.require_path = 'lib'
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vidibus-recording
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Andre Pankratz
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-23 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: mongoid
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 62196427
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 0
|
33
|
+
- 0
|
34
|
+
- beta
|
35
|
+
- 20
|
36
|
+
version: 2.0.0.beta.20
|
37
|
+
type: :runtime
|
38
|
+
version_requirements: *id001
|
39
|
+
- !ruby/object:Gem::Dependency
|
40
|
+
name: open4
|
41
|
+
prerelease: false
|
42
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
hash: 3
|
48
|
+
segments:
|
49
|
+
- 0
|
50
|
+
version: "0"
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: robustthread
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
type: :runtime
|
66
|
+
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: delayed_job_mongoid
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
type: :runtime
|
80
|
+
version_requirements: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: vidibus-uuid
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
hash: 3
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
type: :runtime
|
94
|
+
version_requirements: *id005
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: bundler
|
97
|
+
prerelease: false
|
98
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 23
|
104
|
+
segments:
|
105
|
+
- 1
|
106
|
+
- 0
|
107
|
+
- 0
|
108
|
+
version: 1.0.0
|
109
|
+
type: :development
|
110
|
+
version_requirements: *id006
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
prerelease: false
|
114
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
hash: 3
|
120
|
+
segments:
|
121
|
+
- 0
|
122
|
+
version: "0"
|
123
|
+
type: :development
|
124
|
+
version_requirements: *id007
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
prerelease: false
|
128
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ~>
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
hash: 62196427
|
134
|
+
segments:
|
135
|
+
- 2
|
136
|
+
- 0
|
137
|
+
- 0
|
138
|
+
- beta
|
139
|
+
- 20
|
140
|
+
version: 2.0.0.beta.20
|
141
|
+
type: :development
|
142
|
+
version_requirements: *id008
|
143
|
+
- !ruby/object:Gem::Dependency
|
144
|
+
name: rr
|
145
|
+
prerelease: false
|
146
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
hash: 3
|
152
|
+
segments:
|
153
|
+
- 0
|
154
|
+
version: "0"
|
155
|
+
type: :development
|
156
|
+
version_requirements: *id009
|
157
|
+
- !ruby/object:Gem::Dependency
|
158
|
+
name: relevance-rcov
|
159
|
+
prerelease: false
|
160
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
hash: 3
|
166
|
+
segments:
|
167
|
+
- 0
|
168
|
+
version: "0"
|
169
|
+
type: :development
|
170
|
+
version_requirements: *id010
|
171
|
+
description: Allows recording of RTMP video streams. Uses RTMPdump.
|
172
|
+
email: andre@vidibus.com
|
173
|
+
executables: []
|
174
|
+
|
175
|
+
extensions: []
|
176
|
+
|
177
|
+
extra_rdoc_files: []
|
178
|
+
|
179
|
+
files:
|
180
|
+
- .gitignore
|
181
|
+
- Gemfile
|
182
|
+
- Gemfile.lock
|
183
|
+
- LICENSE
|
184
|
+
- README.rdoc
|
185
|
+
- Rakefile
|
186
|
+
- app/models/recording.rb
|
187
|
+
- lib/vidibus-recording.rb
|
188
|
+
- lib/vidibus/recording.rb
|
189
|
+
- lib/vidibus/recording/backend.rb
|
190
|
+
- lib/vidibus/recording/backend/rtmpdump.rb
|
191
|
+
- lib/vidibus/recording/helpers.rb
|
192
|
+
- lib/vidibus/recording/job.rb
|
193
|
+
- lib/vidibus/recording/mongoid.rb
|
194
|
+
- lib/vidibus/recording/version.rb
|
195
|
+
- spec/spec_helper.rb
|
196
|
+
- spec/vidibus/recording/backend_spec.rb
|
197
|
+
- spec/vidibus/recording/mongoid_spec.rb
|
198
|
+
- vidibus-recording.gemspec
|
199
|
+
has_rdoc: true
|
200
|
+
homepage: http://rubygems.org/gems/vidibus-rtmpdump
|
201
|
+
licenses: []
|
202
|
+
|
203
|
+
post_install_message:
|
204
|
+
rdoc_options: []
|
205
|
+
|
206
|
+
require_paths:
|
207
|
+
- lib
|
208
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
209
|
+
none: false
|
210
|
+
requirements:
|
211
|
+
- - ">="
|
212
|
+
- !ruby/object:Gem::Version
|
213
|
+
hash: 3
|
214
|
+
segments:
|
215
|
+
- 0
|
216
|
+
version: "0"
|
217
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
218
|
+
none: false
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
hash: 23
|
223
|
+
segments:
|
224
|
+
- 1
|
225
|
+
- 3
|
226
|
+
- 6
|
227
|
+
version: 1.3.6
|
228
|
+
requirements: []
|
229
|
+
|
230
|
+
rubyforge_project: vidibus-recording
|
231
|
+
rubygems_version: 1.3.7
|
232
|
+
signing_key:
|
233
|
+
specification_version: 3
|
234
|
+
summary: Video stream recording tools
|
235
|
+
test_files: []
|
236
|
+
|