falcon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+