falcon 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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
+
@@ -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
@@ -0,0 +1,5 @@
1
+ module Falcon
2
+ class Encoding < ::ActiveRecord::Base
3
+ include Falcon::Encoder
4
+ end
5
+ end
@@ -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'
@@ -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
@@ -0,0 +1,12 @@
1
+ require 'rails'
2
+ require 'falcon'
3
+
4
+ module Falcon
5
+ class Engine < ::Rails::Engine
6
+ config.before_initialize do
7
+ ActiveSupport.on_load :active_record do
8
+ ::ActiveRecord::Base.send :include, Falcon::Base
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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,3 @@
1
+ module Falcon
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,7 @@
1
+ Usage:
2
+ rails generate falcon:install
3
+
4
+ Options:
5
+
6
+ Example:
7
+ rails g falcon:install
@@ -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
+