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 +14 -0
- data/lib/paperclip-ffmpeg.rb +2 -159
- data/lib/processors/ffmpeg.rb +159 -0
- data/lib/processors/qtfaststart.rb +46 -0
- data/paperclip-ffmpeg.gemspec +1 -1
- metadata +4 -2
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
|
-------
|
data/lib/paperclip-ffmpeg.rb
CHANGED
@@ -1,160 +1,3 @@
|
|
1
1
|
require "paperclip"
|
2
|
-
|
3
|
-
|
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
|
data/paperclip-ffmpeg.gemspec
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: paperclip-ffmpeg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 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-
|
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
|