paperclip-ffmpeg 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -45,6 +45,20 @@ This will produce:
45
45
 
46
46
  You may optionally add `<attachment>_meta` to your model and paperclip-ffmpeg will add information about the processed video.
47
47
 
48
+ Streaming Video
49
+ -------------------
50
+
51
+ When ffmpeg produces mp4 files, it places the moov atom at the end which makes it unstreamable. To handle this, paperclip-ffmpeg includes a processor to run qtfaststart after producing the video file.
52
+
53
+ In your model:
54
+
55
+ class Lesson < ActiveRecord::Base
56
+ has_attached_file :video, :styles => {
57
+ :mobile => {:geometry => "400x300", :format => 'mp4', :streaming => true}
58
+ }, :processors => [:ffmpeg, :qtfaststart]
59
+ end
60
+
61
+ See [danielgtaylor/qtfaststart](https://github.com/danielgtaylor/qtfaststart) for instructions on how to setup qtfaststart.
48
62
 
49
63
  License
50
64
  -------
@@ -1,160 +1,3 @@
1
1
  require "paperclip"
2
- module Paperclip
3
- class Ffmpeg < Processor
4
- attr_accessor :geometry, :format, :whiny, :convert_options
5
-
6
- # Creates a Video object set to work on the +file+ given. It
7
- # will attempt to transcode the video into one defined by +target_geometry+
8
- # which is a "WxH"-style string. +format+ should be specified.
9
- # Video transcoding will raise no errors unless
10
- # +whiny+ is true (which it is, by default. If +convert_options+ is
11
- # set, the options will be appended to the convert command upon video transcoding
12
- def initialize file, options = {}, attachment = nil
13
- @convert_options = {
14
- :input => {},
15
- :output => { :y => nil }
16
- }
17
- unless options[:convert_options].nil? || options[:convert_options].class != Hash
18
- unless options[:convert_options][:input].nil? || options[:convert_options][:input].class != Hash
19
- @convert_options[:input].reverse_merge! options[:convert_options][:input]
20
- end
21
- unless options[:convert_options][:output].nil? || options[:convert_options][:output].class != Hash
22
- @convert_options[:output].reverse_merge! options[:convert_options][:output]
23
- end
24
- end
25
-
26
- @geometry = options[:geometry]
27
- @file = file
28
- @keep_aspect = !@geometry.nil? && @geometry[-1,1] != '!'
29
- @pad_only = @keep_aspect && @geometry[-1,1] == '#'
30
- @enlarge_only = @keep_aspect && @geometry[-1,1] == '<'
31
- @shrink_only = @keep_aspect && @geometry[-1,1] == '>'
32
- @whiny = options[:whiny].nil? ? true : options[:whiny]
33
- @format = options[:format]
34
- @time = options[:time].nil? ? 3 : options[:time]
35
- @current_format = File.extname(@file.path)
36
- @basename = File.basename(@file.path, @current_format)
37
- @meta = identify
38
- attachment.instance_write(:meta, @meta)
39
- end
40
- # Performs the transcoding of the +file+ into a thumbnail/video. Returns the Tempfile
41
- # that contains the new image/video.
42
- def make
43
- src = @file
44
- dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
45
- dst.binmode
46
-
47
- parameters = []
48
- # Add geometry
49
- if @geometry
50
- # Extract target dimensions
51
- if @geometry =~ /(\d*)x(\d*)/
52
- target_width = $1
53
- target_height = $2
54
- end
55
- # Only calculate target dimensions if we have current dimensions
56
- unless @meta[:size].nil?
57
- current_geometry = @meta[:size].split('x')
58
- # Current width and height
59
- current_width = current_geometry[0]
60
- current_height = current_geometry[1]
61
- if @keep_aspect
62
- if @enlarge_only
63
- if current_width.to_i < target_width.to_i
64
- # Keep aspect ratio
65
- width = target_width.to_i
66
- height = (width.to_f / (@meta[:aspect].to_f)).to_i
67
- @convert_options[:output][:s] = "#{width.to_i}x#{height.to_i}"
68
- else
69
- return nil
70
- end
71
- elsif @shrink_only
72
- if current_width.to_i > target_width.to_i
73
- # Keep aspect ratio
74
- width = target_width.to_i
75
- height = (width.to_f / (@meta[:aspect].to_f)).to_i
76
- @convert_options[:output][:s] = "#{width.to_i}x#{height.to_i}"
77
- else
78
- return nil
79
- end
80
- elsif @pad_only
81
- # Keep aspect ratio
82
- width = target_width.to_i
83
- height = (width.to_f / (@meta[:aspect].to_f)).to_i
84
- # We should add half the delta as a padding offset Y
85
- pad_y = (target_height.to_f - height.to_f) / 2
86
- if pad_y > 0
87
- @convert_options[:output][:vf] = "scale=#{width}:-1,pad=#{width.to_i}:#{target_height.to_i}:0:#{pad_y}:black"
88
- else
89
- @convert_options[:output][:vf] = "scale=#{width}:-1,crop=#{width.to_i}:#{height.to_i}"
90
- end
91
- else
92
- # Keep aspect ratio
93
- width = target_width.to_i
94
- height = (width.to_f / (@meta[:aspect].to_f)).to_i
95
- @convert_options[:output][:s] = "#{width.to_i}x#{height.to_i}"
96
- end
97
- else
98
- # Do not keep aspect ratio
99
- @convert_options[:output][:s] = "#{target_width.to_i}x#{target_height.to_i}"
100
- end
101
- end
102
- end
103
- # Add format
104
- case @format
105
- when 'jpg', 'jpeg', 'png', 'gif' # Images
106
- @convert_options[:input][:ss] = @time
107
- @convert_options[:input][:vframes] = 1
108
- @convert_options[:output][:f] = 'image2'
109
- end
110
-
111
- # Add source
112
- parameters << @convert_options[:input].map { |k,v| "-#{k.to_s} #{v} "}
113
- parameters << "-i :source"
114
- parameters << @convert_options[:output].map { |k,v| "-#{k.to_s} #{v} "}
115
- parameters << ":dest"
116
-
117
- parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
118
-
119
- Paperclip.log("[paperclip][ffmpeg] #{parameters}")
120
- begin
121
- success = Paperclip.run("ffmpeg", parameters, :source => "#{File.expand_path(src.path)}", :dest => File.expand_path(dst.path))
122
- rescue Cocaine::ExitStatusError => e
123
- raise PaperclipError, "error while processing video for #{@basename}: #{e}" if @whiny
124
- end
125
-
126
- dst
127
- end
128
-
129
- def identify
130
- meta = {}
131
- command = "ffmpeg -i #{File.expand_path(@file.path)} 2>&1"
132
- Paperclip.log(command)
133
- ffmpeg = IO.popen(command)
134
- ffmpeg.each("\r") do |line|
135
- if line =~ /((\d*)\s.?)fps,/
136
- meta[:fps] = $1.to_i
137
- end
138
- # Matching lines like:
139
- # Video: h264, yuvj420p, 640x480 [PAR 72:72 DAR 4:3], 10301 kb/s, 30 fps, 30 tbr, 600 tbn, 600 tbc
140
- if line =~ /Video:(.*)/
141
- v = $1.to_s.split(',')
142
- size = v[2].strip!.split(' ').first
143
- meta[:size] = size.to_s
144
- meta[:aspect] = size.split('x').first.to_f / size.split('x').last.to_f
145
- end
146
- # Matching Duration: 00:01:31.66, start: 0.000000, bitrate: 10404 kb/s
147
- if line =~ /Duration:(\s.?(\d*):(\d*):(\d*\.\d*))/
148
- meta[:length] = $2.to_s + ":" + $3.to_s + ":" + $4.to_s
149
- end
150
- end
151
- meta
152
- end
153
- end
154
-
155
- class Attachment
156
- def meta
157
- instance_read(:meta)
158
- end
159
- end
160
- end
2
+ require "processors/ffmpeg"
3
+ require "processors/qtfaststart"
@@ -0,0 +1,159 @@
1
+ module Paperclip
2
+ class Ffmpeg < Processor
3
+ attr_accessor :geometry, :format, :whiny, :convert_options
4
+
5
+ # Creates a Video object set to work on the +file+ given. It
6
+ # will attempt to transcode the video into one defined by +target_geometry+
7
+ # which is a "WxH"-style string. +format+ should be specified.
8
+ # Video transcoding will raise no errors unless
9
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
10
+ # set, the options will be appended to the convert command upon video transcoding.
11
+ def initialize file, options = {}, attachment = nil
12
+ @convert_options = {
13
+ :input => {},
14
+ :output => { :y => nil }
15
+ }
16
+ unless options[:convert_options].nil? || options[:convert_options].class != Hash
17
+ unless options[:convert_options][:input].nil? || options[:convert_options][:input].class != Hash
18
+ @convert_options[:input].reverse_merge! options[:convert_options][:input]
19
+ end
20
+ unless options[:convert_options][:output].nil? || options[:convert_options][:output].class != Hash
21
+ @convert_options[:output].reverse_merge! options[:convert_options][:output]
22
+ end
23
+ end
24
+
25
+ @geometry = options[:geometry]
26
+ @file = file
27
+ @keep_aspect = !@geometry.nil? && @geometry[-1,1] != '!'
28
+ @pad_only = @keep_aspect && @geometry[-1,1] == '#'
29
+ @enlarge_only = @keep_aspect && @geometry[-1,1] == '<'
30
+ @shrink_only = @keep_aspect && @geometry[-1,1] == '>'
31
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
32
+ @format = options[:format]
33
+ @time = options[:time].nil? ? 3 : options[:time]
34
+ @current_format = File.extname(@file.path)
35
+ @basename = File.basename(@file.path, @current_format)
36
+ @meta = identify
37
+ attachment.instance_write(:meta, @meta)
38
+ end
39
+ # Performs the transcoding of the +file+ into a thumbnail/video. Returns the Tempfile
40
+ # that contains the new image/video.
41
+ def make
42
+ src = @file
43
+ dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
44
+ dst.binmode
45
+
46
+ parameters = []
47
+ # Add geometry
48
+ if @geometry
49
+ # Extract target dimensions
50
+ if @geometry =~ /(\d*)x(\d*)/
51
+ target_width = $1
52
+ target_height = $2
53
+ end
54
+ # Only calculate target dimensions if we have current dimensions
55
+ unless @meta[:size].nil?
56
+ current_geometry = @meta[:size].split('x')
57
+ # Current width and height
58
+ current_width = current_geometry[0]
59
+ current_height = current_geometry[1]
60
+ if @keep_aspect
61
+ if @enlarge_only
62
+ if current_width.to_i < target_width.to_i
63
+ # Keep aspect ratio
64
+ width = target_width.to_i
65
+ height = (width.to_f / (@meta[:aspect].to_f)).to_i
66
+ @convert_options[:output][:s] = "#{width.to_i/2*2}x#{height.to_i/2*2}"
67
+ else
68
+ return nil
69
+ end
70
+ elsif @shrink_only
71
+ if current_width.to_i > target_width.to_i
72
+ # Keep aspect ratio
73
+ width = target_width.to_i
74
+ height = (width.to_f / (@meta[:aspect].to_f)).to_i
75
+ @convert_options[:output][:s] = "#{width.to_i/2*2}x#{height.to_i/2*2}"
76
+ else
77
+ return nil
78
+ end
79
+ elsif @pad_only
80
+ # Keep aspect ratio
81
+ width = target_width.to_i
82
+ height = (width.to_f / (@meta[:aspect].to_f)).to_i
83
+ # We should add half the delta as a padding offset Y
84
+ pad_y = (target_height.to_f - height.to_f) / 2
85
+ if pad_y > 0
86
+ @convert_options[:output][:vf] = "scale=#{width}:-1,pad=#{width.to_i}:#{target_height.to_i}:0:#{pad_y}:black"
87
+ else
88
+ @convert_options[:output][:vf] = "scale=#{width}:-1,crop=#{width.to_i}:#{height.to_i}"
89
+ end
90
+ else
91
+ # Keep aspect ratio
92
+ width = target_width.to_i
93
+ height = (width.to_f / (@meta[:aspect].to_f)).to_i
94
+ @convert_options[:output][:s] = "#{width.to_i/2*2}x#{height.to_i/2*2}"
95
+ end
96
+ else
97
+ # Do not keep aspect ratio
98
+ @convert_options[:output][:s] = "#{target_width.to_i/2*2}x#{target_height.to_i/2*2}"
99
+ end
100
+ end
101
+ end
102
+ # Add format
103
+ case @format
104
+ when 'jpg', 'jpeg', 'png', 'gif' # Images
105
+ @convert_options[:input][:ss] = @time
106
+ @convert_options[:output][:vframes] = 1
107
+ @convert_options[:output][:f] = 'image2'
108
+ end
109
+
110
+ # Add source
111
+ parameters << @convert_options[:input].map { |k,v| "-#{k.to_s} #{v} "}
112
+ parameters << "-i :source"
113
+ parameters << @convert_options[:output].map { |k,v| "-#{k.to_s} #{v} "}
114
+ parameters << ":dest"
115
+
116
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
117
+
118
+ Paperclip.log("[ffmpeg] #{parameters}")
119
+ begin
120
+ success = Paperclip.run("ffmpeg", parameters, :source => "#{File.expand_path(src.path)}", :dest => File.expand_path(dst.path))
121
+ rescue Cocaine::ExitStatusError => e
122
+ raise PaperclipError, "error while processing video for #{@basename}: #{e}" if @whiny
123
+ end
124
+
125
+ dst
126
+ end
127
+
128
+ def identify
129
+ meta = {}
130
+ command = "ffmpeg -i #{File.expand_path(@file.path)} 2>&1"
131
+ Paperclip.log(command)
132
+ ffmpeg = IO.popen(command)
133
+ ffmpeg.each("\r") do |line|
134
+ if line =~ /((\d*)\s.?)fps,/
135
+ meta[:fps] = $1.to_i
136
+ end
137
+ # Matching lines like:
138
+ # Video: h264, yuvj420p, 640x480 [PAR 72:72 DAR 4:3], 10301 kb/s, 30 fps, 30 tbr, 600 tbn, 600 tbc
139
+ if line =~ /Video:(.*)/
140
+ v = $1.to_s.split(',')
141
+ size = v[2].strip!.split(' ').first
142
+ meta[:size] = size.to_s
143
+ meta[:aspect] = size.split('x').first.to_f / size.split('x').last.to_f
144
+ end
145
+ # Matching Duration: 00:01:31.66, start: 0.000000, bitrate: 10404 kb/s
146
+ if line =~ /Duration:(\s.?(\d*):(\d*):(\d*\.\d*))/
147
+ meta[:length] = $2.to_s + ":" + $3.to_s + ":" + $4.to_s
148
+ end
149
+ end
150
+ meta
151
+ end
152
+ end
153
+
154
+ class Attachment
155
+ def meta
156
+ instance_read(:meta)
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,46 @@
1
+ module Paperclip
2
+ class Qtfaststart < Processor
3
+ attr_accessor :streaming, :format, :whiny
4
+
5
+ # Creates a Video object set to work on the +file+ given. It
6
+ # will attempt to reposition the moov atom in the video given
7
+ # if +streaming+ is set.
8
+ def initialize file, options = {}, attachment = nil
9
+ @streaming = options[:streaming]
10
+ @file = file
11
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
12
+ @format = options[:format]
13
+ @current_format = File.extname(@file.path)
14
+ @basename = File.basename(@file.path, @current_format)
15
+ attachment.instance_write(:meta, @meta)
16
+ end
17
+
18
+ # Performs the atom repositioning on +file+.
19
+ # Returns the Tempfile that contains the new video or the original
20
+ # file if +streaming+ wasn't set.
21
+ def make
22
+ return @file unless @streaming
23
+
24
+ src = @file
25
+ dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
26
+ dst.binmode
27
+
28
+ parameters = []
29
+ # Add source
30
+ parameters << ":source"
31
+ # Add destination
32
+ parameters << ":dest"
33
+
34
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
35
+
36
+ Paperclip.log("[qtfaststart] #{parameters}")
37
+ begin
38
+ success = Paperclip.run("qtfaststart", parameters, :source => "#{File.expand_path(src.path)}", :dest => File.expand_path(dst.path))
39
+ rescue Cocaine::ExitStatusError => e
40
+ raise PaperclipError, "error while processing video for #{@basename}: #{e}" if @whiny
41
+ end
42
+
43
+ dst
44
+ end
45
+ end
46
+ end
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "paperclip-ffmpeg"
6
- s.version = '0.8.0'
6
+ s.version = '0.9.0'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Omar Abdel-Wahab"]
9
9
  s.email = ["owahab@gmail.com"]
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: paperclip-ffmpeg
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.8.0
5
+ version: 0.9.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Omar Abdel-Wahab
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2012-02-06 00:00:00 +02:00
13
+ date: 2012-04-08 00:00:00 +02:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -39,6 +39,8 @@ files:
39
39
  - README.md
40
40
  - Rakefile
41
41
  - lib/paperclip-ffmpeg.rb
42
+ - lib/processors/ffmpeg.rb
43
+ - lib/processors/qtfaststart.rb
42
44
  - paperclip-ffmpeg.gemspec
43
45
  has_rdoc: true
44
46
  homepage: http://github.com/owahab/paperclip-ffmpeg