video2gif 0.0.33 → 0.0.34
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/video2gif/ffmpeg.rb +158 -135
- data/lib/video2gif/ffmpeg/subtitles.rb +61 -3
- data/lib/video2gif/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc6e44b0ef580d23810cfaaae7449d4c0aa42e4e21a4a549f14cd549ed78a6e4
|
4
|
+
data.tar.gz: 229342c782db8a61f1e7706e13f00e25ad5a289bd796e9b2975482a16e2d8d31
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 18edb4ce95a5f30993cd2adddc22ade8bc1e135c322ad150bfa051590e46dc676b4ae5d611bc81addf1c521bc0143f755faa9381893120ccf894609d8635b69e
|
7
|
+
data.tar.gz: aeb1a6e3183f0e1c4b9c7fbac2015d8a8d289cf9f177b6e1ea18699301cd63e6c981ca2a2416fb9354b90fabb22363867c8c28d9e2b35dcdcffe2e743674fcc3
|
data/lib/video2gif/ffmpeg.rb
CHANGED
@@ -1,162 +1,185 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'ffmpeg/subtitles'
|
4
|
+
|
3
5
|
|
4
6
|
module Video2gif
|
5
7
|
module FFmpeg
|
8
|
+
include Subtitles
|
9
|
+
|
6
10
|
CROP_REGEX = /crop=([0-9]+\:[0-9]+\:[0-9]+\:[0-9]+)/
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
|
12
|
+
def self.video_info(options)
|
13
|
+
options[:probe_infos][:streams].find { |s| s[:codec_type] == 'video' }
|
14
|
+
end
|
11
15
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
if options[:subtitles] && options[:probe_infos][:streams].any? { |s| s[:codec_type] == 'subtitle' }
|
16
|
-
subtitle_info = options[:probe_infos][:streams]
|
17
|
-
.find_all { |s| s[:codec_type] == 'subtitle' }
|
18
|
-
.fetch(options[:subtitle_index], nil)
|
19
|
-
end
|
16
|
+
def self.rate(options)
|
17
|
+
"setpts=PTS/#{options[:rate]}" if options[:rate]
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
].join(':') + '[subs]'
|
32
|
-
filtergraph << '[0:v][subs]overlay=format=auto'
|
33
|
-
end
|
20
|
+
def self.interpolate(options)
|
21
|
+
if options[:rate] && Float(options[:rate]) < 1 # only interpolate slowed down video
|
22
|
+
minterpolate_parameters = []
|
23
|
+
|
24
|
+
minterpolate_parameters << 'mi_mode=mci'
|
25
|
+
minterpolate_parameters << 'mc_mode=aobmc'
|
26
|
+
minterpolate_parameters << 'me_mode=bidir'
|
27
|
+
minterpolate_parameters << 'me=epzs'
|
28
|
+
minterpolate_parameters << 'vsbmc=1'
|
29
|
+
minterpolate_parameters << "fps=#{video_info(options)[:avg_frame_rate] }/#{options[:rate]}"
|
34
30
|
|
35
|
-
|
36
|
-
# don't end up trying to interpolate frames in that we already
|
37
|
-
# dropped.
|
38
|
-
if options[:rate]
|
39
|
-
filtergraph << "setpts=PTS/#{options[:rate]}" if options[:rate]
|
40
|
-
if Float(options[:rate]) < 1 # interpolate slowed down video
|
41
|
-
minterpolate = []
|
42
|
-
minterpolate << 'mi_mode=mci'
|
43
|
-
minterpolate << 'mc_mode=aobmc'
|
44
|
-
minterpolate << 'me_mode=bidir'
|
45
|
-
minterpolate << 'me=epzs'
|
46
|
-
minterpolate << 'vsbmc=1'
|
47
|
-
minterpolate << "fps=#{video_info[:avg_frame_rate] }/#{options[:rate]}"
|
48
|
-
filtergraph << 'minterpolate=' + minterpolate.join(':')
|
49
|
-
end
|
31
|
+
'minterpolate=' + minterpolate_parameters.join(':')
|
50
32
|
end
|
33
|
+
end
|
51
34
|
|
52
|
-
|
53
|
-
|
54
|
-
|
35
|
+
def self.rate_with_interpolation(options)
|
36
|
+
[
|
37
|
+
rate(options),
|
38
|
+
interpolate(options)
|
39
|
+
]
|
40
|
+
end
|
55
41
|
|
56
|
-
|
57
|
-
|
42
|
+
def self.fps(options)
|
43
|
+
"fps=#{ options[:fps] || 10 }"
|
44
|
+
end
|
58
45
|
|
59
|
-
|
60
|
-
|
61
|
-
crop << "h=#{options[:hregion]}" if options[:hregion]
|
62
|
-
crop << "x=#{options[:xoffset]}" if options[:xoffset]
|
63
|
-
crop << "y=#{options[:yoffset]}" if options[:yoffset]
|
64
|
-
filtergraph << 'crop=' + crop.join(':') unless crop.empty?
|
46
|
+
def self.crop(options)
|
47
|
+
crop_parameters = []
|
65
48
|
|
66
|
-
|
49
|
+
crop_parameters << "w=#{options[:wregion]}" if options[:wregion]
|
50
|
+
crop_parameters << "h=#{options[:hregion]}" if options[:hregion]
|
51
|
+
crop_parameters << "x=#{options[:xoffset]}" if options[:xoffset]
|
52
|
+
crop_parameters << "y=#{options[:yoffset]}" if options[:yoffset]
|
53
|
+
|
54
|
+
'crop=' + crop_parameters.join(':') unless crop_parameters.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.zscale(options)
|
58
|
+
zscale_parameters = []
|
59
|
+
|
60
|
+
zscale_parameters << 'dither=none'
|
61
|
+
zscale_parameters << 'filter=lanczos'
|
62
|
+
zscale_parameters << "width=#{ options[:width] || 400 }"
|
63
|
+
zscale_parameters << "height=trunc(#{ options[:width] || 400 }/dar)"
|
64
|
+
|
65
|
+
'zscale=' + zscale_parameters.join(':')
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.tonemap(options)
|
69
|
+
%W[
|
70
|
+
zscale=transfer=linear:npl=100
|
71
|
+
zscale=npl=100
|
72
|
+
format=gbrpf32le
|
73
|
+
zscale=primaries=bt709
|
74
|
+
tonemap=tonemap=#{options[:tonemap]}:desat=0
|
75
|
+
zscale=transfer=bt709:matrix=bt709:range=tv
|
76
|
+
format=yuv420p
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.zscale_and_tonemap(options)
|
67
81
|
if options[:tonemap]
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
dither=none
|
73
|
-
filter=lanczos
|
74
|
-
width=#{ options[:width] || 400 }
|
75
|
-
height=trunc(#{ options[:width] || 400 }/dar)
|
76
|
-
].join(':')
|
77
|
-
filtergraph << 'zscale=transfer=linear:npl=100'
|
78
|
-
filtergraph << 'zscale=npl=100'
|
79
|
-
filtergraph << 'format=gbrpf32le'
|
80
|
-
filtergraph << 'zscale=primaries=bt709'
|
81
|
-
filtergraph << "tonemap=tonemap=#{options[:tonemap]}:desat=0"
|
82
|
-
filtergraph << 'zscale=transfer=bt709:matrix=bt709:range=tv'
|
83
|
-
filtergraph << 'format=yuv420p'
|
84
|
-
else
|
85
|
-
# If we're not attempting to convert HDR to SDR, the standard
|
86
|
-
# 'scale' filter is preferred (if we're resizing at all).
|
87
|
-
filtergraph << 'scale=' + %W[
|
88
|
-
flags=lanczos
|
89
|
-
sws_dither=none
|
90
|
-
width=#{ options[:width] || 400 }
|
91
|
-
height=trunc(#{ options[:width] || 400 }/dar)
|
92
|
-
].join(':') unless options[:tonemap]
|
82
|
+
[
|
83
|
+
zscale(options),
|
84
|
+
tonemap(options)
|
85
|
+
]
|
93
86
|
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.scale(options)
|
90
|
+
unless options[:tonemap]
|
91
|
+
scale_parameters = []
|
92
|
+
|
93
|
+
scale_parameters << 'flags=lanczos'
|
94
|
+
scale_parameters << 'sws_dither=none'
|
95
|
+
scale_parameters << "width=#{ options[:width] || 400 }"
|
96
|
+
scale_parameters << "height=trunc(#{ options[:width] || 400 }/dar)"
|
94
97
|
|
95
|
-
|
96
|
-
# it won't be affected.
|
97
|
-
filtergraph << "eq=contrast=#{options[:contrast]}" if options[:contrast]
|
98
|
-
filtergraph << "eq=brightness=#{options[:brightness]}" if options[:brightness]
|
99
|
-
filtergraph << "eq=saturation=#{options[:saturation]}" if options[:saturation]
|
100
|
-
filtergraph << "eq=gamma=#{options[:gamma]}" if options[:gamma]
|
101
|
-
filtergraph << "eq=gamma_r=#{options[:gamma_r]}" if options[:gamma_r]
|
102
|
-
filtergraph << "eq=gamma_g=#{options[:gamma_g]}" if options[:gamma_g]
|
103
|
-
filtergraph << "eq=gamma_b=#{options[:gamma_b]}" if options[:gamma_b]
|
104
|
-
|
105
|
-
# Embed text subtitles later so that they don't get processed by
|
106
|
-
# cropping, etc., which might accidentally crop them out.
|
107
|
-
if options[:subtitles] &&
|
108
|
-
options[:probe_infos][:streams].any? { |s| s[:codec_type] == 'subtitle' } &&
|
109
|
-
Subtitles::KNOWN_TEXT_FORMATS.include?(subtitle_info[:codec_name])
|
110
|
-
filtergraph << "setpts=PTS+#{Utils.duration_to_seconds(options[:seek])}/TB"
|
111
|
-
filtergraph << "subtitles='#{options[:input_filename]}':si=#{options[:subtitle_index]}"
|
112
|
-
filtergraph << 'setpts=PTS-STARTPTS'
|
98
|
+
'scale=' + scale_parameters.join(':')
|
113
99
|
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.eq(options)
|
103
|
+
eq_parameters = []
|
104
|
+
|
105
|
+
eq_parameters << "contrast=#{options[:contrast]}" if options[:contrast]
|
106
|
+
eq_parameters << "brightness=#{options[:brightness]}" if options[:brightness]
|
107
|
+
eq_parameters << "saturation=#{options[:saturation]}" if options[:saturation]
|
108
|
+
eq_parameters << "gamma=#{options[:gamma]}" if options[:gamma]
|
109
|
+
eq_parameters << "gamma_r=#{options[:gamma_r]}" if options[:gamma_r]
|
110
|
+
eq_parameters << "gamma_g=#{options[:gamma_g]}" if options[:gamma_g]
|
111
|
+
eq_parameters << "gamma_b=#{options[:gamma_b]}" if options[:gamma_b]
|
112
|
+
|
113
|
+
'eq=' + eq_parameters.join(":")
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.text(options)
|
117
|
+
options[:text].gsub(/\\n/, '')
|
118
|
+
.gsub(/([:])/, '\\\\\\\\\\1')
|
119
|
+
.gsub(/([,])/, '\\\\\\1')
|
120
|
+
.gsub(/\b'\b/, "\u2019")
|
121
|
+
.gsub(/\B"\b([^"\u201C\u201D\u201E\u201F\u2033\u2036\r\n]+)\b?"\B/, "\u201C\\1\u201D")
|
122
|
+
.gsub(/\B'\b([^'\u2018\u2019\u201A\u201B\u2032\u2035\r\n]+)\b?'\B/, "\u2018\\1\u2019")
|
123
|
+
end
|
114
124
|
|
115
|
-
|
116
|
-
# generation to ensure the color looks appropriate.
|
125
|
+
def self.drawtext(options)
|
117
126
|
if options[:text]
|
118
127
|
count_of_lines = options[:text].scan(/\\n/).count + 1
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
fontsize='#{ options[:textsize] || 32 }'
|
131
|
-
fontcolor='#{ options[:textcolor] || 'white' }'
|
132
|
-
borderw='#{ options[:textborder] || 1 }'
|
133
|
-
fontfile='#{ options[:textfont] || 'Arial'}'\\\\:style='#{options[:textvariant] || 'Bold' }'
|
134
|
-
text='#{text}'
|
135
|
-
].join(':')
|
128
|
+
|
129
|
+
drawtext_parameters = []
|
130
|
+
drawtext_parameters << "x='#{ options[:xpos] || '(main_w/2-text_w/2)' }'"
|
131
|
+
drawtext_parameters << "y='#{ options[:ypos] || "(main_h-line_h*1.5*#{count_of_lines})" }'"
|
132
|
+
drawtext_parameters << "fontsize='#{ options[:textsize] || 32 }'"
|
133
|
+
drawtext_parameters << "fontcolor='#{ options[:textcolor] || 'white' }'"
|
134
|
+
drawtext_parameters << "borderw='#{ options[:textborder] || 1 }'"
|
135
|
+
drawtext_parameters << "fontfile='#{ options[:textfont] || 'Arial'}'\\\\:style='#{options[:textvariant] || 'Bold' }'"
|
136
|
+
drawtext_parameters << "text='#{text(options)}'"
|
137
|
+
|
138
|
+
'drawtext=' + drawtext_parameters.join(':')
|
136
139
|
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.split
|
143
|
+
'split[palettegen][paletteuse]'
|
144
|
+
end
|
137
145
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
146
|
+
def self.palettegen(options)
|
147
|
+
palettegen_parameters = []
|
148
|
+
|
149
|
+
palettegen_parameters << "#{ options[:palette] || 256 }"
|
150
|
+
palettegen_parameters << "stats_mode=#{options[:palettemode] || 'diff'}"
|
151
|
+
|
152
|
+
'[palettegen]palettegen=' + palettegen_parameters.join(':') + '[palette]'
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.paletteuse(options)
|
156
|
+
paletteuse_parameters = []
|
157
|
+
|
158
|
+
paletteuse_parameters << "dither=#{options[:dither] || 'floyd_steinberg'}"
|
159
|
+
paletteuse_parameters << 'diff_mode=rectangle'
|
160
|
+
paletteuse_parameters << "#{options[:palettemode] == 'single' ? 'new=1' : ''}"
|
161
|
+
|
162
|
+
'[paletteuse][palette]paletteuse=' + paletteuse_parameters.join(':')
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.filtergraph(options)
|
166
|
+
filtergraph = []
|
167
|
+
|
168
|
+
filtergraph << bitmap_subtitles_scale_overlay(options)
|
169
|
+
filtergraph << rate_with_interpolation(options)
|
170
|
+
filtergraph << fps(options)
|
171
|
+
filtergraph << options[:autocrop] if options[:autocrop]
|
172
|
+
filtergraph << crop(options)
|
173
|
+
filtergraph << zscale_and_tonemap(options)
|
174
|
+
filtergraph << scale(options)
|
175
|
+
filtergraph << eq(options)
|
176
|
+
filtergraph << text_subtitles(options)
|
177
|
+
filtergraph << drawtext(options)
|
178
|
+
filtergraph << split
|
179
|
+
filtergraph << palettegen(options)
|
180
|
+
filtergraph << paletteuse(options)
|
181
|
+
|
182
|
+
filtergraph.flatten.compact
|
160
183
|
end
|
161
184
|
|
162
185
|
def self.ffprobe_command(options, logger, executable: 'ffprobe')
|
@@ -26,15 +26,73 @@ module Video2gif
|
|
26
26
|
ttml
|
27
27
|
vplayer
|
28
28
|
webvtt
|
29
|
-
]
|
29
|
+
].freeze
|
30
30
|
|
31
31
|
KNOWN_BITMAP_FORMATS = %w[
|
32
32
|
dvb_subtitle
|
33
33
|
dvd_subtitle
|
34
34
|
hdmv_pgs_subtitle
|
35
35
|
xsub
|
36
|
-
]
|
36
|
+
].freeze
|
37
|
+
|
38
|
+
def self.included(m)
|
39
|
+
m.extend(ClassMethods)
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def has_subtitles(options)
|
44
|
+
options[:subtitles] && options[:probe_infos][:streams].any? { |s| s[:codec_type] == 'subtitle' }
|
45
|
+
end
|
46
|
+
|
47
|
+
def subtitle_info(options)
|
48
|
+
if has_subtitles(options)
|
49
|
+
options[:probe_infos][:streams].find_all { |s| s[:codec_type] == 'subtitle' }
|
50
|
+
.fetch(options[:subtitle_index], nil)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_bitmap_subtitles(options)
|
55
|
+
has_subtitles(options) && KNOWN_BITMAP_FORMATS.include?(subtitle_info(options)[:codec_name])
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_text_subtitles(options)
|
59
|
+
has_subtitles(options) && KNOWN_TEXT_FORMATS.include?(subtitle_info(options)[:codec_name])
|
60
|
+
end
|
61
|
+
|
62
|
+
def subtitles_scale(options)
|
63
|
+
scale_parameters = []
|
64
|
+
|
65
|
+
scale_parameters << 'flags=lanczos'
|
66
|
+
scale_parameters << 'sws_dither=none'
|
67
|
+
scale_parameters << "width=#{video_info(options)[:width]}"
|
68
|
+
scale_parameters << "height=#{video_info(options)[:height]}"
|
69
|
+
|
70
|
+
"[0:s:#{options[:subtitle_index]}]scale=#{scale_parameters.join(':')}[subs]"
|
71
|
+
end
|
72
|
+
|
73
|
+
def subtitles_overlay
|
74
|
+
'[0:v][subs]overlay=format=auto'
|
75
|
+
end
|
76
|
+
|
77
|
+
def bitmap_subtitles_scale_overlay(options)
|
78
|
+
if has_bitmap_subtitles(options)
|
79
|
+
[
|
80
|
+
subtitles_scale(options),
|
81
|
+
subtitles_overlay
|
82
|
+
]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def text_subtitles(options)
|
87
|
+
if has_text_subtitles(options)
|
88
|
+
%W[
|
89
|
+
setpts=PTS+#{Utils.duration_to_seconds(options[:seek])}/TB
|
90
|
+
subtitles='#{options[:input_filename]}':si=#{options[:subtitle_index]}
|
91
|
+
setpts=PTS-STARTPTS
|
92
|
+
]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
37
96
|
end
|
38
97
|
end
|
39
98
|
end
|
40
|
-
|
data/lib/video2gif/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: video2gif
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.34
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emily St.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-07-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|