paperclip-ffmpeg 0.8.0 → 0.9.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/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