falcon 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.rdoc +159 -0
- data/Rakefile +43 -0
- data/app/models/falcon/encoding.rb +5 -0
- data/lib/falcon.rb +58 -0
- data/lib/falcon/base.rb +99 -0
- data/lib/falcon/encoder.rb +223 -0
- data/lib/falcon/engine.rb +12 -0
- data/lib/falcon/media.rb +158 -0
- data/lib/falcon/profile.rb +96 -0
- data/lib/falcon/profiles.rb +9 -0
- data/lib/falcon/version.rb +3 -0
- data/lib/generators/falcon/USAGE +7 -0
- data/lib/generators/falcon/install_generator.rb +31 -0
- data/lib/generators/falcon/templates/migrate/create_encodings.rb +29 -0
- metadata +98 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010-2011 Aimbulance.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
= Falcon
|
2
|
+
|
3
|
+
Video encoding tool.
|
4
|
+
|
5
|
+
== Install
|
6
|
+
|
7
|
+
gem 'falcon'
|
8
|
+
|
9
|
+
rails generate falcon:install
|
10
|
+
|
11
|
+
rake db:migrate
|
12
|
+
|
13
|
+
== Usage
|
14
|
+
|
15
|
+
=== Create profiles
|
16
|
+
|
17
|
+
By default avariable two profiles "web_mp4" and "web_ogg":
|
18
|
+
|
19
|
+
Falcon::Profile.new("web_mp4", {:player => 'flash', :container => "mp4", :extname => 'mp4',
|
20
|
+
:width => 480, :height => 320, :video_codec => "libx264",
|
21
|
+
:video_bitrate => 500, :fps => 29.97, :audio_codec => "libfaac",
|
22
|
+
:command => "-vpre medium",
|
23
|
+
:audio_bitrate => 128, :audio_sample_rate => 48000})
|
24
|
+
|
25
|
+
Falcon::Profile.new("web_ogg", {:player => 'html5', :container => "ogg", :extname => 'ogv',
|
26
|
+
:width => 480, :height => 320, :video_codec => "libtheora",
|
27
|
+
:video_bitrate => 500, :fps => 29.97, :audio_codec => "libvorbis",
|
28
|
+
:audio_bitrate => 128, :audio_sample_rate => 48000})
|
29
|
+
|
30
|
+
=== Update profiles
|
31
|
+
|
32
|
+
Falcon::Profile['web_mp4'].update({:width => 800, :height => 600})
|
33
|
+
|
34
|
+
=== Model
|
35
|
+
|
36
|
+
Model has attachment file, we declare "falcon_encode" method and pass :source and :profiles options:
|
37
|
+
|
38
|
+
class VideoFile
|
39
|
+
has_attached_file :data,
|
40
|
+
:url => "/assets/video_files/:id/:filename",
|
41
|
+
:path => ":rails_root/public/assets/video_files/:id/:filename"
|
42
|
+
|
43
|
+
validates_attachment_presence :data
|
44
|
+
validates_attachment_size :data, :less_than => 200.megabytes
|
45
|
+
validates_attachment_content_type :data, :content_type => Falcon::CONTENT_TYPES
|
46
|
+
|
47
|
+
attr_accessible :data
|
48
|
+
|
49
|
+
falcon_encode 'media', :source => lambda { |file| file.data.path },
|
50
|
+
:profiles => ['web_mp4', 'web_ogg']
|
51
|
+
end
|
52
|
+
|
53
|
+
=== Metadata options
|
54
|
+
|
55
|
+
You can provide metadata options in your model, just set option "metadata":
|
56
|
+
|
57
|
+
class VideoFile
|
58
|
+
|
59
|
+
...
|
60
|
+
|
61
|
+
falcon_encode 'media', :source => lambda { |file| file.data.path },
|
62
|
+
:metadata => :media_metadata_options,
|
63
|
+
:profiles => ['web_mp4', 'web_ogg']
|
64
|
+
|
65
|
+
# A hash of metadatas for video:
|
66
|
+
#
|
67
|
+
# { :title => '', :author => '', :copyright => '',
|
68
|
+
# :comment => '', :description => '', :language => ''}
|
69
|
+
#
|
70
|
+
def media_metadata_options
|
71
|
+
{ :title => title, :author => user.name, :language => 'rus' }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
=== Background processing
|
76
|
+
|
77
|
+
Video encoding take a long time, so you must use background process. I recomended "delayed_job" or "resque".
|
78
|
+
To send encoding in background, just set option method "encode":
|
79
|
+
|
80
|
+
Resque example:
|
81
|
+
|
82
|
+
class VideoFile
|
83
|
+
|
84
|
+
...
|
85
|
+
|
86
|
+
falcon_encode 'media', :source => lambda { |file| file.data.path },
|
87
|
+
:metadata => :media_metadata_options,
|
88
|
+
:encode => lambda { |encoding| Resque.enqueue(JobEncoding, encoding.id) },
|
89
|
+
:profiles => ['web_mp4', 'web_ogg']
|
90
|
+
end
|
91
|
+
|
92
|
+
class JobEncoding
|
93
|
+
@queue = :encoding_queue
|
94
|
+
|
95
|
+
def self.perform(encoding_id)
|
96
|
+
encoding = Falcon::Encoding.find(encoding_id)
|
97
|
+
encoding.encode
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
For delayed_job:
|
102
|
+
|
103
|
+
class VideoFile
|
104
|
+
|
105
|
+
...
|
106
|
+
|
107
|
+
falcon_encode 'media', :source => lambda { |file| file.data.path },
|
108
|
+
:metadata => :media_metadata_options,
|
109
|
+
:encode => lambda { |encoding| encoding.delay.encode },
|
110
|
+
:profiles => ['web_mp4', 'web_ogg']
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
=== Callbacks
|
115
|
+
|
116
|
+
class VideoFile
|
117
|
+
|
118
|
+
...
|
119
|
+
|
120
|
+
falcon_encode 'media', :source => lambda { |file| file.data.path }
|
121
|
+
|
122
|
+
before_encode :method_name
|
123
|
+
before_media_encode :method_name
|
124
|
+
after_media_encode :method_name
|
125
|
+
after_encode :method_name
|
126
|
+
end
|
127
|
+
|
128
|
+
=== Screenshots
|
129
|
+
|
130
|
+
class VideoFile
|
131
|
+
has_many :screenshots, :dependent => :destroy
|
132
|
+
|
133
|
+
...
|
134
|
+
|
135
|
+
falcon_encode 'media', :source => lambda { |file| file.data.path }
|
136
|
+
|
137
|
+
after_media_encode :save_screenshots
|
138
|
+
|
139
|
+
def save_screenshots
|
140
|
+
media.screenshots do |filepath|
|
141
|
+
self.screenshots.create(:data => File.new(filepath))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
=== Path and url
|
147
|
+
|
148
|
+
class VideoFile
|
149
|
+
falcon_encode 'media', :source => lambda { |file| file.data.path }
|
150
|
+
|
151
|
+
def media_url(profile_name)
|
152
|
+
media.url(profile_name)
|
153
|
+
end
|
154
|
+
|
155
|
+
def media_path(profile_name)
|
156
|
+
media.path(profile_name)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require File.join(File.dirname(__FILE__), 'lib', 'falcon', 'version')
|
6
|
+
|
7
|
+
desc 'Default: run unit tests.'
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
desc 'Test the falcon plugin.'
|
11
|
+
Rake::TestTask.new(:test) do |t|
|
12
|
+
t.libs << 'lib'
|
13
|
+
t.libs << 'test'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = true
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Generate documentation for the falcon plugin.'
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
21
|
+
rdoc.title = 'Falcon'
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
23
|
+
rdoc.rdoc_files.include('README.rdoc')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
require 'jeweler'
|
29
|
+
Jeweler::Tasks.new do |s|
|
30
|
+
s.name = "falcon"
|
31
|
+
s.version = Falcon::VERSION.dup
|
32
|
+
s.summary = "Background video encoding"
|
33
|
+
s.description = "Background video encoding via resque"
|
34
|
+
s.email = "galeta.igor@gmail.com"
|
35
|
+
s.homepage = "https://github.com/galetahub/falcon"
|
36
|
+
s.authors = ["Igor Galeta", "Pavlo Galeta"]
|
37
|
+
s.files = FileList["[A-Z]*", "{app,config,lib}/**/*"] - %w(Gemfile Gemfile.lock)
|
38
|
+
end
|
39
|
+
|
40
|
+
Jeweler::GemcutterTasks.new
|
41
|
+
rescue LoadError
|
42
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
43
|
+
end
|
data/lib/falcon.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Falcon
|
2
|
+
autoload :Profile, 'falcon/profile'
|
3
|
+
autoload :Base, 'falcon/base'
|
4
|
+
autoload :Encoder, 'falcon/encoder'
|
5
|
+
autoload :Media, 'falcon/media'
|
6
|
+
|
7
|
+
CONTENT_TYPES = [
|
8
|
+
'application/x-mp4',
|
9
|
+
'video/mpeg',
|
10
|
+
'video/quicktime',
|
11
|
+
'video/x-la-asf',
|
12
|
+
'video/x-ms-asf',
|
13
|
+
'video/x-msvideo',
|
14
|
+
'video/x-sgi-movie',
|
15
|
+
'video/x-flv',
|
16
|
+
'flv-application/octet-stream',
|
17
|
+
'application/octet-stream',
|
18
|
+
'video/3gpp',
|
19
|
+
'video/3gpp2',
|
20
|
+
'video/3gpp-tt',
|
21
|
+
'video/BMPEG',
|
22
|
+
'video/BT656',
|
23
|
+
'video/CelB',
|
24
|
+
'video/DV',
|
25
|
+
'video/H261',
|
26
|
+
'video/H263',
|
27
|
+
'video/H263-1998',
|
28
|
+
'video/H263-2000',
|
29
|
+
'video/H264',
|
30
|
+
'video/JPEG',
|
31
|
+
'video/MJ2',
|
32
|
+
'video/MP1S',
|
33
|
+
'video/MP2P',
|
34
|
+
'video/MP2T',
|
35
|
+
'video/mp4',
|
36
|
+
'video/MP4V-ES',
|
37
|
+
'video/MPV',
|
38
|
+
'video/mpeg4',
|
39
|
+
'video/mpeg4-generic',
|
40
|
+
'video/nv',
|
41
|
+
'video/parityfec',
|
42
|
+
'video/pointer',
|
43
|
+
'video/raw',
|
44
|
+
'video/rtx',
|
45
|
+
'video/x-matroska',
|
46
|
+
'video/x-ms-wmv',
|
47
|
+
'video/divxplus',
|
48
|
+
'video/avi',
|
49
|
+
'video/divx',
|
50
|
+
'video/vnd.objectvideo' ]
|
51
|
+
|
52
|
+
def self.table_name_prefix
|
53
|
+
'falcon_'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
require 'falcon/profiles'
|
58
|
+
require 'falcon/engine'
|
data/lib/falcon/base.rb
ADDED
@@ -0,0 +1,99 @@
|
|
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 => :delete_all
|
34
|
+
|
35
|
+
after_save :save_falcon_medias
|
36
|
+
before_destroy :destroy_falcon_medias
|
37
|
+
|
38
|
+
define_falcon_callbacks :encode, :"#{name}_encode"
|
39
|
+
|
40
|
+
define_method name do |*args|
|
41
|
+
a = falcon_media_for(name)
|
42
|
+
(args.length > 0) ? a.to_s(args.first) : a
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method "#{name}=" do |source_path|
|
46
|
+
falcon_media_for(name).assign(source_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method "#{name}?" do
|
50
|
+
falcon_media_for(name).exist?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module ClassMethods
|
56
|
+
def define_falcon_callbacks(*callbacks)
|
57
|
+
define_callbacks *[callbacks, {:terminator => "result == false"}].flatten
|
58
|
+
callbacks.each do |callback|
|
59
|
+
eval <<-end_callbacks
|
60
|
+
def before_#{callback}(*args, &blk)
|
61
|
+
set_callback(:#{callback}, :before, *args, &blk)
|
62
|
+
end
|
63
|
+
def after_#{callback}(*args, &blk)
|
64
|
+
set_callback(:#{callback}, :after, *args, &blk)
|
65
|
+
end
|
66
|
+
end_callbacks
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module InstanceMethods
|
72
|
+
|
73
|
+
def falcon_media_for(name)
|
74
|
+
@_falcon_medias ||= {}
|
75
|
+
@_falcon_medias[name] ||= Media.new(name, self, self.class.falcon_encoding_definitions[name])
|
76
|
+
end
|
77
|
+
|
78
|
+
def each_falcon_medias
|
79
|
+
self.class.falcon_encoding_definitions.each do |name, definition|
|
80
|
+
yield(name, falcon_media_for(name))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def save_falcon_medias
|
87
|
+
each_falcon_medias do |name, media|
|
88
|
+
media.send(:save)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def destroy_falcon_medias
|
93
|
+
each_falcon_medias do |name, media|
|
94
|
+
media.send(:destroy)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,223 @@
|
|
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
|
+
|
28
|
+
scope :with_profile, lambda {|name| where(:profile_name => Falcon::Profile.detect(name).name) }
|
29
|
+
scope :with_name, lambda {|name| where(:name => name) }
|
30
|
+
scope :processing, where(:status => PROCESSING)
|
31
|
+
scope :success, where(:status => SUCCESS)
|
32
|
+
scope :failure, where(:status => FAILURE)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module InstanceMethods
|
38
|
+
|
39
|
+
def profile
|
40
|
+
@profile ||= Falcon::Profile.find(profile_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def resolution
|
44
|
+
self.width ? "#{self.width}x#{self.height}" : nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def transcoder
|
48
|
+
@transcoder ||= ::WebVideo::Transcoder.new(source_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def output_path
|
52
|
+
@output_path ||= self.profile.path(source_path, name).to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
def output_directory
|
56
|
+
@output_directory ||= File.dirname(self.output_path)
|
57
|
+
end
|
58
|
+
|
59
|
+
def profile_options(input_file, output_file)
|
60
|
+
self.profile.encode_options.merge({
|
61
|
+
:input_file => input_file,
|
62
|
+
:output_file => output_file,
|
63
|
+
:resolution => self.ffmpeg_resolution
|
64
|
+
#:resolution_and_padding => self.ffmpeg_resolution_and_padding_no_cropping
|
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
|
+
protected
|
98
|
+
|
99
|
+
def process_encoding
|
100
|
+
begun_encoding = Time.now
|
101
|
+
|
102
|
+
self.status = PROCESSING
|
103
|
+
self.save(:validate => false)
|
104
|
+
|
105
|
+
begin
|
106
|
+
self.encode_source
|
107
|
+
self.generate_screenshots
|
108
|
+
|
109
|
+
self.status = SUCCESS
|
110
|
+
self.encoded_at = Time.now
|
111
|
+
self.encoding_time = (Time.now - begun_encoding).to_i
|
112
|
+
self.save(:validate => false)
|
113
|
+
rescue
|
114
|
+
self.status = FAILURE
|
115
|
+
self.save(:validate => false)
|
116
|
+
raise
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_resolution
|
121
|
+
unless profile.nil?
|
122
|
+
self.width ||= profile.width
|
123
|
+
self.height ||= profile.height
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def ffmpeg_resolution_and_padding_no_cropping(v_width, v_height)
|
128
|
+
# Calculate resolution and any padding
|
129
|
+
in_w = v_width.to_f #self.video.width.to_f
|
130
|
+
in_h = v_height.to_f #self.video.height.to_f
|
131
|
+
out_w = self.width.to_f
|
132
|
+
out_h = self.height.to_f
|
133
|
+
|
134
|
+
begin
|
135
|
+
aspect = in_w / in_h
|
136
|
+
aspect_inv = in_h / in_w
|
137
|
+
rescue
|
138
|
+
#Merb.logger.error "Couldn't do w/h to caculate aspect. Just using the output resolution now."
|
139
|
+
@ffmpeg_resolution = %(#{self.width}x#{self.height} )
|
140
|
+
return
|
141
|
+
end
|
142
|
+
|
143
|
+
height = (out_w / aspect.to_f).to_i
|
144
|
+
height -= 1 if height % 2 == 1
|
145
|
+
|
146
|
+
@ffmpeg_resolution = %(#{self.width}x#{height} )
|
147
|
+
|
148
|
+
# Keep the video's original width if the height
|
149
|
+
if height > out_h
|
150
|
+
width = (out_h / aspect_inv.to_f).to_i
|
151
|
+
width -= 1 if width % 2 == 1
|
152
|
+
|
153
|
+
@ffmpeg_resolution = %(#{width}x#{self.height} )
|
154
|
+
self.width = width
|
155
|
+
self.save
|
156
|
+
# Otherwise letterbox it
|
157
|
+
elsif height < out_h
|
158
|
+
pad = ((out_h - height.to_f) / 2.0).to_i
|
159
|
+
pad -= 1 if pad % 2 == 1
|
160
|
+
@ffmpeg_padding = %(-padtop #{pad} -padbottom #{pad})
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def encode_source
|
165
|
+
#stream = transcoder.source.video_stream
|
166
|
+
ffmpeg_resolution_and_padding_no_cropping(self.width, self.height)
|
167
|
+
options = self.profile_options(self.source_path, output_path)
|
168
|
+
|
169
|
+
begin
|
170
|
+
transcoder.convert(output_path, options) do |command|
|
171
|
+
# Audo
|
172
|
+
command << "-ar $audio_sample_rate$"
|
173
|
+
command << "-ab $audio_bitrate_in_bits$"
|
174
|
+
command << "-acodec $audio_codec$"
|
175
|
+
command << "-ac 1"
|
176
|
+
|
177
|
+
# Video
|
178
|
+
command << "-vcodec $video_codec$"
|
179
|
+
command << "-b $video_bitrate_in_bits$"
|
180
|
+
command << "-bt 240k"
|
181
|
+
command << "-r $fps$"
|
182
|
+
command << "-f $container$"
|
183
|
+
|
184
|
+
# Profile additional arguments
|
185
|
+
command << self.profile.command
|
186
|
+
|
187
|
+
# Metadata options
|
188
|
+
if metadata_options
|
189
|
+
metadata_options.each do |key, value|
|
190
|
+
command << "-metadata #{key}=\"#{value}\""
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
command << @ffmpeg_padding
|
195
|
+
command << "-y"
|
196
|
+
end
|
197
|
+
rescue ::WebVideo::CommandLineError => e
|
198
|
+
::WebVideo.logger.error("Unable to transcode video #{self.id}: #{e.class} - #{e.message}")
|
199
|
+
return false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def generate_screenshots
|
204
|
+
image_files = output_path.gsub(File.extname(output_path), '_%2d.jpg')
|
205
|
+
options = {:resolution => self.resolution, :count => 1, :at => :center}
|
206
|
+
image_transcoder = ::WebVideo::Transcoder.new(output_path)
|
207
|
+
|
208
|
+
begin
|
209
|
+
image_transcoder.screenshot(image_files, options) do |command|
|
210
|
+
command << "-vcodec mjpeg"
|
211
|
+
|
212
|
+
# The duration for which image extraction will take place
|
213
|
+
#command << "-t 4"
|
214
|
+
command << "-y"
|
215
|
+
end
|
216
|
+
rescue ::WebVideo::CommandLineError => e
|
217
|
+
::WebVideo.logger.error("Unable to generate screenshots for video #{self.id}: #{e.class} - #{e.message}")
|
218
|
+
return false
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
data/lib/falcon/media.rb
ADDED
@@ -0,0 +1,158 @@
|
|
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("#{output_directory}/*.{jpg,JPG}").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.delete_all
|
152
|
+
|
153
|
+
profiles.each do |profile_name, profile|
|
154
|
+
FileUtils.rm(path(profile), :force => true)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Falcon
|
2
|
+
class Profile
|
3
|
+
cattr_accessor :all
|
4
|
+
@@all = []
|
5
|
+
|
6
|
+
attr_accessor :name, :player, :container, :extname, :width, :height, :fps, :command
|
7
|
+
attr_accessor :video_bitrate, :video_codec
|
8
|
+
attr_accessor :audio_codec, :audio_bitrate, :audio_sample_rate
|
9
|
+
|
10
|
+
DEFAULTS = {
|
11
|
+
:player => 'flash',
|
12
|
+
:container => "mp4",
|
13
|
+
:extname => 'mp4',
|
14
|
+
:width => 480,
|
15
|
+
:height => 320,
|
16
|
+
:fps => 29.97,
|
17
|
+
:video_codec => "libx264",
|
18
|
+
:video_bitrate => 500,
|
19
|
+
:command => nil,
|
20
|
+
:audio_codec => "libfaac",
|
21
|
+
:audio_bitrate => 128,
|
22
|
+
:audio_sample_rate => 48000
|
23
|
+
}
|
24
|
+
|
25
|
+
def initialize(name, options = {})
|
26
|
+
options.assert_valid_keys(DEFAULTS.keys)
|
27
|
+
options = DEFAULTS.merge(options)
|
28
|
+
|
29
|
+
@name = name.to_s
|
30
|
+
|
31
|
+
if self.class.exists?(@name)
|
32
|
+
raise "Profile name: #{@name} already registered."
|
33
|
+
end
|
34
|
+
|
35
|
+
options.each do |key, value|
|
36
|
+
send("#{key}=", value)
|
37
|
+
end
|
38
|
+
|
39
|
+
@@all << self
|
40
|
+
end
|
41
|
+
|
42
|
+
def audio_bitrate_in_bits
|
43
|
+
self.audio_bitrate.to_i * 1024
|
44
|
+
end
|
45
|
+
|
46
|
+
def video_bitrate_in_bits
|
47
|
+
self.video_bitrate.to_i * 1024
|
48
|
+
end
|
49
|
+
|
50
|
+
def path(source, prefix = nil)
|
51
|
+
dirname = File.dirname(source)
|
52
|
+
filename = File.basename(source, File.extname(source))
|
53
|
+
filename = [prefix, filename].compact.join('_') + '.' + extname
|
54
|
+
|
55
|
+
Pathname.new(File.join(dirname, filename))
|
56
|
+
end
|
57
|
+
|
58
|
+
def encode_options
|
59
|
+
{
|
60
|
+
:container => self.container,
|
61
|
+
:video_codec => self.video_codec,
|
62
|
+
:video_bitrate_in_bits => self.video_bitrate_in_bits.to_s,
|
63
|
+
:fps => self.fps,
|
64
|
+
:audio_codec => self.audio_codec.to_s,
|
65
|
+
:audio_bitrate => self.audio_bitrate.to_s,
|
66
|
+
:audio_bitrate_in_bits => self.audio_bitrate_in_bits.to_s,
|
67
|
+
:audio_sample_rate => self.audio_sample_rate.to_s
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def update(options)
|
72
|
+
options.each do |key, value|
|
73
|
+
send("#{key}=", value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class << self
|
78
|
+
def find(name)
|
79
|
+
@@all.detect { |p| p.name == name.to_s }
|
80
|
+
end
|
81
|
+
alias :get :find
|
82
|
+
|
83
|
+
def [](name)
|
84
|
+
find(name)
|
85
|
+
end
|
86
|
+
|
87
|
+
def exists?(name)
|
88
|
+
!find(name).nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
def detect(name)
|
92
|
+
name.is_a?(Falcon::Profile) ? name : find(name.to_s)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Falcon::Profile.new("web_mp4", {:player => 'flash', :container => "mp4", :extname => 'mp4',
|
2
|
+
:width => 480, :height => 320, :video_codec => "libx264",
|
3
|
+
:video_bitrate => 500, :fps => 29.97, :audio_codec => "libfaac",
|
4
|
+
:audio_bitrate => 128, :audio_sample_rate => 48000})
|
5
|
+
|
6
|
+
Falcon::Profile.new("web_ogg", {:player => 'html5', :container => "ogg", :extname => 'ogv',
|
7
|
+
:width => 480, :height => 320, :video_codec => "libtheora",
|
8
|
+
:video_bitrate => 500, :fps => 29.97, :audio_codec => "libvorbis",
|
9
|
+
:audio_bitrate => 128, :audio_sample_rate => 48000})
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Falcon
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
desc "Create falcon migration"
|
10
|
+
source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
11
|
+
|
12
|
+
# copy migration files
|
13
|
+
def create_migrations
|
14
|
+
migration_template "migrate/create_encodings.rb", File.join('db/migrate', "falcon_create_encodings.rb")
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.next_migration_number(dirname)
|
18
|
+
if ActiveRecord::Base.timestamped_migrations
|
19
|
+
current_time.utc.strftime("%Y%m%d%H%M%S")
|
20
|
+
else
|
21
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.current_time
|
26
|
+
@current_time ||= Time.now
|
27
|
+
@current_time += 1.minute
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class FalconCreateEncodings < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :falcon_encodings do |t|
|
4
|
+
t.string :name, :limit => 50, :null => false
|
5
|
+
t.string :profile_name, :limit => 50, :null => false
|
6
|
+
t.string :source_path, :null => false
|
7
|
+
|
8
|
+
t.string :videoable_type, :limit => 50
|
9
|
+
t.integer :videoable_id
|
10
|
+
|
11
|
+
t.integer :status, :default => 0
|
12
|
+
t.integer :progress
|
13
|
+
t.integer :width
|
14
|
+
t.integer :height
|
15
|
+
|
16
|
+
t.integer :encoding_time
|
17
|
+
t.datetime :encoded_at
|
18
|
+
|
19
|
+
t.timestamps
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index :falcon_encodings, [:videoable_type, :videoable_id]
|
23
|
+
add_index :falcon_encodings, :status
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.down
|
27
|
+
drop_table :falcon_encodings
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: falcon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Igor Galeta
|
14
|
+
- Pavlo Galeta
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-05-18 00:00:00 +03:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
type: :runtime
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 17
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 1
|
33
|
+
- 1
|
34
|
+
version: 1.1.1
|
35
|
+
name: web_video
|
36
|
+
version_requirements: *id001
|
37
|
+
prerelease: false
|
38
|
+
description: Background video encoding via resque
|
39
|
+
email: galeta.igor@gmail.com
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files:
|
45
|
+
- LICENSE
|
46
|
+
- README.rdoc
|
47
|
+
files:
|
48
|
+
- LICENSE
|
49
|
+
- README.rdoc
|
50
|
+
- Rakefile
|
51
|
+
- app/models/falcon/encoding.rb
|
52
|
+
- lib/falcon.rb
|
53
|
+
- lib/falcon/base.rb
|
54
|
+
- lib/falcon/encoder.rb
|
55
|
+
- lib/falcon/engine.rb
|
56
|
+
- lib/falcon/media.rb
|
57
|
+
- lib/falcon/profile.rb
|
58
|
+
- lib/falcon/profiles.rb
|
59
|
+
- lib/falcon/version.rb
|
60
|
+
- lib/generators/falcon/USAGE
|
61
|
+
- lib/generators/falcon/install_generator.rb
|
62
|
+
- lib/generators/falcon/templates/migrate/create_encodings.rb
|
63
|
+
has_rdoc: true
|
64
|
+
homepage: https://github.com/galetahub/falcon
|
65
|
+
licenses: []
|
66
|
+
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 3
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 3
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
requirements: []
|
91
|
+
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 1.6.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 3
|
96
|
+
summary: Background video encoding
|
97
|
+
test_files: []
|
98
|
+
|