falcon 0.1.3 → 0.2.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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +18 -0
- data/Gemfile +9 -0
- data/README.md +100 -0
- data/Rakefile +64 -36
- data/bin/falcon +28 -0
- data/falcon.gemspec +31 -0
- data/lib/falcon.rb +20 -57
- data/lib/falcon/command.rb +86 -0
- data/lib/falcon/server.rb +73 -0
- data/lib/falcon/version.rb +21 -1
- data/lib/rack/handler/falcon.rb +40 -0
- metadata +148 -77
- data/LICENSE +0 -19
- data/README.rdoc +0 -159
- data/app/models/falcon/encoding.rb +0 -5
- data/lib/falcon/base.rb +0 -98
- data/lib/falcon/encoder.rb +0 -235
- data/lib/falcon/engine.rb +0 -12
- data/lib/falcon/media.rb +0 -154
- data/lib/falcon/profile.rb +0 -96
- data/lib/falcon/profiles.rb +0 -9
- data/lib/generators/falcon/USAGE +0 -7
- data/lib/generators/falcon/install_generator.rb +0 -31
- data/lib/generators/falcon/templates/migrate/create_encodings.rb +0 -29
data/lib/falcon/base.rb
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
module Falcon
|
2
|
-
module Base
|
3
|
-
def self.included(base)
|
4
|
-
base.extend SingletonMethods
|
5
|
-
end
|
6
|
-
|
7
|
-
module SingletonMethods
|
8
|
-
#
|
9
|
-
# falcon_encode 'media', :source => lambda { |file| file.data.path },
|
10
|
-
# :profiles => ['web_mp4', 'web_ogg']
|
11
|
-
#
|
12
|
-
# falcon_encode 'media', :source => :method_return_path_to_file,
|
13
|
-
# :profiles => ['web_mp4', 'web_ogg']
|
14
|
-
# :metadata => :method_return_options_hash,
|
15
|
-
# :encode => lambda { |encoding| encoding.delay.encode }
|
16
|
-
#
|
17
|
-
def falcon_encode(name, options = {})
|
18
|
-
extend ClassMethods
|
19
|
-
include InstanceMethods
|
20
|
-
|
21
|
-
options.assert_valid_keys(:source, :profiles, :metadata, :encode)
|
22
|
-
|
23
|
-
unless respond_to?(:falcon_encoding_definitions)
|
24
|
-
class_attribute :falcon_encoding_definitions, :instance_writer => false
|
25
|
-
self.falcon_encoding_definitions = {}
|
26
|
-
end
|
27
|
-
|
28
|
-
self.falcon_encoding_definitions[name] = options
|
29
|
-
|
30
|
-
has_many :falcon_encodings,
|
31
|
-
:class_name => 'Falcon::Encoding',
|
32
|
-
:as => :videoable,
|
33
|
-
:dependent => :destroy
|
34
|
-
|
35
|
-
after_save :save_falcon_medias
|
36
|
-
|
37
|
-
define_falcon_callbacks :encode, :"#{name}_encode"
|
38
|
-
|
39
|
-
define_method name do |*args|
|
40
|
-
a = falcon_media_for(name)
|
41
|
-
(args.length > 0) ? a.to_s(args.first) : a
|
42
|
-
end
|
43
|
-
|
44
|
-
define_method "#{name}=" do |source_path|
|
45
|
-
falcon_media_for(name).assign(source_path)
|
46
|
-
end
|
47
|
-
|
48
|
-
define_method "#{name}?" do
|
49
|
-
falcon_media_for(name).exist?
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
module ClassMethods
|
55
|
-
def define_falcon_callbacks(*callbacks)
|
56
|
-
define_callbacks *[callbacks, {:terminator => "result == false"}].flatten
|
57
|
-
callbacks.each do |callback|
|
58
|
-
eval <<-end_callbacks
|
59
|
-
def before_#{callback}(*args, &blk)
|
60
|
-
set_callback(:#{callback}, :before, *args, &blk)
|
61
|
-
end
|
62
|
-
def after_#{callback}(*args, &blk)
|
63
|
-
set_callback(:#{callback}, :after, *args, &blk)
|
64
|
-
end
|
65
|
-
end_callbacks
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
module InstanceMethods
|
71
|
-
|
72
|
-
def falcon_media_for(name)
|
73
|
-
@_falcon_medias ||= {}
|
74
|
-
@_falcon_medias[name] ||= Media.new(name, self, self.class.falcon_encoding_definitions[name])
|
75
|
-
end
|
76
|
-
|
77
|
-
def each_falcon_medias
|
78
|
-
self.class.falcon_encoding_definitions.each do |name, definition|
|
79
|
-
yield(name, falcon_media_for(name))
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
protected
|
84
|
-
|
85
|
-
def save_falcon_medias
|
86
|
-
each_falcon_medias do |name, media|
|
87
|
-
media.send(:save)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def destroy_falcon_medias
|
92
|
-
each_falcon_medias do |name, media|
|
93
|
-
media.send(:destroy)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
data/lib/falcon/encoder.rb
DELETED
@@ -1,235 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
require 'web_video'
|
3
|
-
|
4
|
-
module Falcon
|
5
|
-
module Encoder
|
6
|
-
PROCESSING = 1
|
7
|
-
SUCCESS = 2
|
8
|
-
FAILURE = 3
|
9
|
-
|
10
|
-
def self.included(base)
|
11
|
-
base.send :include, InstanceMethods
|
12
|
-
base.send :extend, ClassMethods
|
13
|
-
end
|
14
|
-
|
15
|
-
module ClassMethods
|
16
|
-
def self.extended(base)
|
17
|
-
base.class_eval do
|
18
|
-
belongs_to :videoable, :polymorphic => true
|
19
|
-
|
20
|
-
attr_accessor :ffmpeg_resolution, :ffmpeg_padding
|
21
|
-
|
22
|
-
attr_accessible :name, :profile_name, :source_path
|
23
|
-
|
24
|
-
validates_presence_of :name, :profile_name, :source_path
|
25
|
-
|
26
|
-
before_validation :set_resolution
|
27
|
-
before_destroy :remove_output
|
28
|
-
|
29
|
-
scope :with_profile, lambda {|name| where(:profile_name => Falcon::Profile.detect(name).name) }
|
30
|
-
scope :with_name, lambda {|name| where(:name => name) }
|
31
|
-
scope :processing, where(:status => PROCESSING)
|
32
|
-
scope :success, where(:status => SUCCESS)
|
33
|
-
scope :failure, where(:status => FAILURE)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
module InstanceMethods
|
39
|
-
|
40
|
-
def profile
|
41
|
-
@profile ||= Falcon::Profile.find(profile_name)
|
42
|
-
end
|
43
|
-
|
44
|
-
def resolution
|
45
|
-
self.width ? "#{self.width}x#{self.height}" : nil
|
46
|
-
end
|
47
|
-
|
48
|
-
def transcoder
|
49
|
-
@transcoder ||= ::WebVideo::Transcoder.new(source_path)
|
50
|
-
end
|
51
|
-
|
52
|
-
def output_path
|
53
|
-
@output_path ||= self.profile.path(source_path, name).to_s
|
54
|
-
end
|
55
|
-
|
56
|
-
def output_directory
|
57
|
-
@output_directory ||= File.dirname(self.output_path)
|
58
|
-
end
|
59
|
-
|
60
|
-
def profile_options(input_file, output_file)
|
61
|
-
self.profile.encode_options.merge({
|
62
|
-
:input_file => input_file,
|
63
|
-
:output_file => output_file,
|
64
|
-
:resolution => self.ffmpeg_resolution
|
65
|
-
})
|
66
|
-
end
|
67
|
-
|
68
|
-
# A hash of metadatas for video:
|
69
|
-
#
|
70
|
-
# { :title => '', :author => '', :copyright => '',
|
71
|
-
# :comment => '', :description => '', :language => ''}
|
72
|
-
#
|
73
|
-
def metadata_options
|
74
|
-
videoable.send(name).metadata
|
75
|
-
end
|
76
|
-
|
77
|
-
def encode
|
78
|
-
videoable.run_callbacks(:encode) do
|
79
|
-
videoable.run_callbacks(:"#{name}_encode") do
|
80
|
-
process_encoding
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def processing?
|
86
|
-
self.status == PROCESSING
|
87
|
-
end
|
88
|
-
|
89
|
-
def fail?
|
90
|
-
self.status == FAILURE
|
91
|
-
end
|
92
|
-
|
93
|
-
def success?
|
94
|
-
self.status == SUCCESS
|
95
|
-
end
|
96
|
-
|
97
|
-
def processing!
|
98
|
-
self.status = PROCESSING
|
99
|
-
save(:validate => false)
|
100
|
-
end
|
101
|
-
|
102
|
-
def fail!
|
103
|
-
self.status = FAILURE
|
104
|
-
save(:validate => false)
|
105
|
-
end
|
106
|
-
|
107
|
-
def success!
|
108
|
-
self.status = SUCCESS
|
109
|
-
self.encoded_at = Time.now
|
110
|
-
save(:validate => false)
|
111
|
-
end
|
112
|
-
|
113
|
-
protected
|
114
|
-
|
115
|
-
def process_encoding
|
116
|
-
begun_encoding = Time.now
|
117
|
-
|
118
|
-
processing!
|
119
|
-
|
120
|
-
if encode_source && generate_screenshots
|
121
|
-
self.encoding_time = (Time.now - begun_encoding).to_i
|
122
|
-
success!
|
123
|
-
else
|
124
|
-
fail!
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
def set_resolution
|
129
|
-
unless profile.nil?
|
130
|
-
self.width ||= profile.width
|
131
|
-
self.height ||= profile.height
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Calculate resolution and any padding
|
136
|
-
def ffmpeg_resolution_and_padding_no_cropping(v_width, v_height)
|
137
|
-
in_w = v_width.to_f
|
138
|
-
in_h = v_height.to_f
|
139
|
-
out_w = self.width.to_f
|
140
|
-
out_h = self.height.to_f
|
141
|
-
|
142
|
-
begin
|
143
|
-
aspect = in_w / in_h
|
144
|
-
aspect_inv = in_h / in_w
|
145
|
-
rescue
|
146
|
-
Rails.logger.error "Couldn't do w/h to caculate aspect. Just using the output resolution now."
|
147
|
-
@ffmpeg_resolution = "#{self.width}x#{self.height}"
|
148
|
-
return
|
149
|
-
end
|
150
|
-
|
151
|
-
height = (out_w / aspect.to_f).to_i
|
152
|
-
height -= 1 if height % 2 == 1
|
153
|
-
|
154
|
-
@ffmpeg_resolution = "#{self.width}x#{height}"
|
155
|
-
|
156
|
-
# Keep the video's original width if the height
|
157
|
-
if height > out_h
|
158
|
-
width = (out_h / aspect_inv.to_f).to_i
|
159
|
-
width -= 1 if width % 2 == 1
|
160
|
-
|
161
|
-
@ffmpeg_resolution = "#{width}x#{self.height}"
|
162
|
-
self.width = width
|
163
|
-
self.save(:validate => false)
|
164
|
-
# Otherwise letterbox it
|
165
|
-
elsif height < out_h
|
166
|
-
pad = ((out_h - height.to_f) / 2.0).to_i
|
167
|
-
pad -= 1 if pad % 2 == 1
|
168
|
-
@ffmpeg_padding = "-vf pad=#{self.width}:#{height + pad}:0:#{pad / 2}"
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
def encode_source
|
173
|
-
stream = transcoder.source.video_stream
|
174
|
-
ffmpeg_resolution_and_padding_no_cropping(stream.width, stream.height)
|
175
|
-
options = self.profile_options(self.source_path, output_path)
|
176
|
-
|
177
|
-
begin
|
178
|
-
transcoder.convert(output_path, options) do |command|
|
179
|
-
# Audo
|
180
|
-
command << "-ar $audio_sample_rate$"
|
181
|
-
command << "-ab $audio_bitrate_in_bits$"
|
182
|
-
command << "-acodec $audio_codec$"
|
183
|
-
command << "-ac 1"
|
184
|
-
|
185
|
-
# Video
|
186
|
-
command << "-vcodec $video_codec$"
|
187
|
-
command << "-b $video_bitrate_in_bits$"
|
188
|
-
command << "-bt 240k"
|
189
|
-
command << "-r $fps$"
|
190
|
-
command << "-f $container$"
|
191
|
-
|
192
|
-
# Profile additional arguments
|
193
|
-
command << self.profile.command
|
194
|
-
|
195
|
-
# Metadata options
|
196
|
-
if metadata_options
|
197
|
-
metadata_options.each do |key, value|
|
198
|
-
command << "-metadata #{key}=\"#{value}\""
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
command << self.ffmpeg_padding
|
203
|
-
command << "-y"
|
204
|
-
end
|
205
|
-
rescue ::WebVideo::CommandLineError => e
|
206
|
-
::WebVideo.logger.error("Unable to transcode video #{self.id}: #{e.class} - #{e.message}")
|
207
|
-
return false
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def generate_screenshots
|
212
|
-
image_files = output_path.gsub(File.extname(output_path), '_%2d.jpg')
|
213
|
-
options = {:resolution => self.resolution, :count => 1, :at => :center}
|
214
|
-
image_transcoder = ::WebVideo::Transcoder.new(output_path)
|
215
|
-
|
216
|
-
begin
|
217
|
-
image_transcoder.screenshot(image_files, options) do |command|
|
218
|
-
command << "-vcodec mjpeg"
|
219
|
-
|
220
|
-
# The duration for which image extraction will take place
|
221
|
-
#command << "-t 4"
|
222
|
-
command << "-y"
|
223
|
-
end
|
224
|
-
rescue ::WebVideo::CommandLineError => e
|
225
|
-
::WebVideo.logger.error("Unable to generate screenshots for video #{self.id}: #{e.class} - #{e.message}")
|
226
|
-
return false
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
def remove_output
|
231
|
-
FileUtils.rm(output_path, :force => true) if File.exists?(output_path)
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
end
|
data/lib/falcon/engine.rb
DELETED
data/lib/falcon/media.rb
DELETED
@@ -1,154 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
|
3
|
-
module Falcon
|
4
|
-
class Media
|
5
|
-
def self.default_options
|
6
|
-
@default_options ||= {
|
7
|
-
:profiles => ['web_mp4', 'web_ogg'],
|
8
|
-
:metadata => {},
|
9
|
-
:source => nil,
|
10
|
-
:encode => nil
|
11
|
-
}
|
12
|
-
end
|
13
|
-
|
14
|
-
attr_reader :name, :instance, :options
|
15
|
-
|
16
|
-
def initialize(name, instance, options = {})
|
17
|
-
@name = name
|
18
|
-
@instance = instance
|
19
|
-
@options = self.class.default_options.merge(options)
|
20
|
-
@profiles = @options[:profiles]
|
21
|
-
@encode = @options[:encode]
|
22
|
-
@dirty = false
|
23
|
-
end
|
24
|
-
|
25
|
-
# Array of processing profiles
|
26
|
-
def profiles
|
27
|
-
unless @normalized_profiles
|
28
|
-
@normalized_profiles = {}
|
29
|
-
(@profiles.respond_to?(:call) ? @profiles.call(self) : @profiles).each do |name|
|
30
|
-
@normalized_profiles[name] = Falcon::Profile.find(name)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
@normalized_profiles
|
35
|
-
end
|
36
|
-
|
37
|
-
# List of generated video files
|
38
|
-
def sources
|
39
|
-
@sources ||= profiles.values.map{|profile| url(profile) }
|
40
|
-
end
|
41
|
-
|
42
|
-
# A hash of metadatas for video:
|
43
|
-
#
|
44
|
-
# { :title => '', :author => '', :copyright => '',
|
45
|
-
# :comment => '', :description => '', :language => ''}
|
46
|
-
#
|
47
|
-
def metadata
|
48
|
-
@metadata ||= begin
|
49
|
-
method = @options[:metadata]
|
50
|
-
method.respond_to?(:call) ? method.call(self) : instance.send(method)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
# Path for media source file
|
55
|
-
def source_path
|
56
|
-
@source_path ||= begin
|
57
|
-
method = options[:source]
|
58
|
-
method.respond_to?(:call) ? method.call(instance) : instance.send(method)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def output_directory
|
63
|
-
@output_directory ||= File.dirname(source_path)
|
64
|
-
end
|
65
|
-
|
66
|
-
# Returns true if there are changes that need to be saved.
|
67
|
-
def dirty?
|
68
|
-
@dirty
|
69
|
-
end
|
70
|
-
|
71
|
-
# Returns the path of the generated media file by profile object or profile name
|
72
|
-
def path(profile)
|
73
|
-
Falcon::Profile.detect(profile).path(source_path, name)
|
74
|
-
end
|
75
|
-
|
76
|
-
# Returns the public URL of the media, with a given profile
|
77
|
-
def url(profile)
|
78
|
-
"/" + path(profile).relative_path_from( Rails.root.join('public') )
|
79
|
-
end
|
80
|
-
|
81
|
-
def save
|
82
|
-
flush_deletes
|
83
|
-
create_encodings
|
84
|
-
@dirty = false
|
85
|
-
true
|
86
|
-
end
|
87
|
-
|
88
|
-
# Destroy files end encodings
|
89
|
-
def destroy
|
90
|
-
flush_deletes
|
91
|
-
@dirty = false
|
92
|
-
true
|
93
|
-
end
|
94
|
-
|
95
|
-
# Check if source file exists
|
96
|
-
def exist?
|
97
|
-
File.exist?(source_path)
|
98
|
-
end
|
99
|
-
|
100
|
-
# Check if source encoded by all profiles
|
101
|
-
def all_ready?
|
102
|
-
instance.falcon_encodings.success.count == profiles.keys.size
|
103
|
-
end
|
104
|
-
|
105
|
-
def ready?(profile)
|
106
|
-
instance.falcon_encodings.with_profile(profile).success.exists?
|
107
|
-
end
|
108
|
-
|
109
|
-
def assign(source)
|
110
|
-
if File.exist?(source)
|
111
|
-
@source_path = source
|
112
|
-
@dirty = true
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# Yield generated screenshots and remove them
|
117
|
-
def screenshots(&block)
|
118
|
-
Dir.glob(File.join(output_directory, '*.{jpg,JPG}').to_s).each do |filepath|
|
119
|
-
yield filepath
|
120
|
-
FileUtils.rm(filepath, :force => true)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
protected
|
125
|
-
|
126
|
-
def create_encodings
|
127
|
-
profiles.each do |profile_name, profile|
|
128
|
-
encoding = create_encoding(profile_name)
|
129
|
-
start_encoding(encoding)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def create_encoding(profile_name)
|
134
|
-
instance.falcon_encodings.create(
|
135
|
-
:name => name,
|
136
|
-
:profile_name => profile_name,
|
137
|
-
:source_path => source_path)
|
138
|
-
end
|
139
|
-
|
140
|
-
# Start encoding direcly or send it into method if set
|
141
|
-
def start_encoding(encoding)
|
142
|
-
if @encode
|
143
|
-
@encode.respond_to?(:call) ? @encode.call(encoding) : instance.send(@encode, encoding)
|
144
|
-
else
|
145
|
-
encoding.encode
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
# Clear generated files and created encodings
|
150
|
-
def flush_deletes
|
151
|
-
instance.falcon_encodings.clear
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|