d-streamio-ffmpeg 3.0.3
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.
- checksums.yaml +7 -0
- data/CHANGELOG +263 -0
- data/LICENSE +20 -0
- data/README.md +286 -0
- data/lib/ffmpeg/encoding_options.rb +200 -0
- data/lib/ffmpeg/errors.rb +6 -0
- data/lib/ffmpeg/io_monkey.rb +42 -0
- data/lib/ffmpeg/movie.rb +257 -0
- data/lib/ffmpeg/transcoder.rb +141 -0
- data/lib/ffmpeg/version.rb +3 -0
- data/lib/streamio-ffmpeg.rb +108 -0
- metadata +95 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module FFMPEG
|
4
|
+
class Transcoder
|
5
|
+
attr_reader :command, :input
|
6
|
+
|
7
|
+
@@timeout = 30
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :timeout
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(input, output_file, options = EncodingOptions.new, transcoder_options = {})
|
14
|
+
if input.is_a?(FFMPEG::Movie)
|
15
|
+
@movie = input
|
16
|
+
@input = input.path
|
17
|
+
end
|
18
|
+
@output_file = output_file
|
19
|
+
|
20
|
+
if options.is_a?(Array) || options.is_a?(EncodingOptions)
|
21
|
+
@raw_options = options
|
22
|
+
elsif options.is_a?(Hash)
|
23
|
+
@raw_options = EncodingOptions.new(options)
|
24
|
+
else
|
25
|
+
raise ArgumentError, "Unknown options format '#{options.class}', should be either EncodingOptions, Hash or Array."
|
26
|
+
end
|
27
|
+
|
28
|
+
@transcoder_options = transcoder_options
|
29
|
+
@errors = []
|
30
|
+
|
31
|
+
apply_transcoder_options
|
32
|
+
|
33
|
+
@input = @transcoder_options[:input] unless @transcoder_options[:input].nil?
|
34
|
+
|
35
|
+
input_options = @transcoder_options[:input_options] || []
|
36
|
+
iopts = []
|
37
|
+
input_options.each { |k, v| iopts += ['-' + k.to_s, v] }
|
38
|
+
|
39
|
+
@command = [FFMPEG.ffmpeg_binary, '-y', *iopts, '-i', @input, *@raw_options.to_a, @output_file]
|
40
|
+
end
|
41
|
+
|
42
|
+
def run(&block)
|
43
|
+
transcode_movie(&block)
|
44
|
+
if @transcoder_options[:validate]
|
45
|
+
validate_output_file(&block)
|
46
|
+
return encoded
|
47
|
+
else
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def encoding_succeeded?
|
53
|
+
@errors << "no output file created" and return false unless File.exist?(@output_file)
|
54
|
+
@errors << "encoded file is invalid" and return false unless encoded.valid?
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def encoded
|
59
|
+
@encoded ||= Movie.new(@output_file)
|
60
|
+
end
|
61
|
+
|
62
|
+
def timeout
|
63
|
+
self.class.timeout
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
# frame= 4855 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/
|
68
|
+
def transcode_movie
|
69
|
+
FFMPEG.logger.info("Running transcoding...\n#{command}\n")
|
70
|
+
@output = ""
|
71
|
+
|
72
|
+
Open3.popen3(*command) do |_stdin, _stdout, stderr, wait_thr|
|
73
|
+
begin
|
74
|
+
yield(0.0) if block_given?
|
75
|
+
next_line = Proc.new do |line|
|
76
|
+
fix_encoding(line)
|
77
|
+
@output << line
|
78
|
+
if line.include?("time=")
|
79
|
+
if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
|
80
|
+
time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
|
81
|
+
else # better make sure it wont blow up in case of unexpected output
|
82
|
+
time = 0.0
|
83
|
+
end
|
84
|
+
|
85
|
+
if @movie
|
86
|
+
progress = time / @movie.duration
|
87
|
+
yield(progress) if block_given?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if timeout
|
93
|
+
stderr.each_with_timeout(wait_thr.pid, timeout, 'size=', &next_line)
|
94
|
+
else
|
95
|
+
stderr.each('size=', &next_line)
|
96
|
+
end
|
97
|
+
|
98
|
+
rescue Timeout::Error => e
|
99
|
+
FFMPEG.logger.error "Process hung...\n@command\n#{command}\nOutput\n#{@output}\n"
|
100
|
+
raise Error, "Process hung. Full output: #{@output}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate_output_file(&block)
|
106
|
+
if encoding_succeeded?
|
107
|
+
yield(1.0) if block_given?
|
108
|
+
FFMPEG.logger.info "Transcoding of #{input} to #{@output_file} succeeded\n"
|
109
|
+
else
|
110
|
+
errors = "Errors: #{@errors.join(", ")}. "
|
111
|
+
FFMPEG.logger.error "Failed encoding...\n#{command}\n\n#{@output}\n#{errors}\n"
|
112
|
+
raise Error, "Failed encoding.#{errors}Full output: #{@output}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def apply_transcoder_options
|
117
|
+
# if true runs #validate_output_file
|
118
|
+
@transcoder_options[:validate] = @transcoder_options.fetch(:validate) { true }
|
119
|
+
|
120
|
+
return if @movie.nil? || @movie.calculated_aspect_ratio.nil?
|
121
|
+
case @transcoder_options[:preserve_aspect_ratio].to_s
|
122
|
+
when "width"
|
123
|
+
new_height = @raw_options.width / @movie.calculated_aspect_ratio
|
124
|
+
new_height = new_height.ceil.even? ? new_height.ceil : new_height.floor
|
125
|
+
new_height += 1 if new_height.odd? # needed if new_height ended up with no decimals in the first place
|
126
|
+
@raw_options[:resolution] = "#{@raw_options.width}x#{new_height}"
|
127
|
+
when "height"
|
128
|
+
new_width = @raw_options.height * @movie.calculated_aspect_ratio
|
129
|
+
new_width = new_width.ceil.even? ? new_width.ceil : new_width.floor
|
130
|
+
new_width += 1 if new_width.odd?
|
131
|
+
@raw_options[:resolution] = "#{new_width}x#{@raw_options.height}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def fix_encoding(output)
|
136
|
+
output[/test/]
|
137
|
+
rescue ArgumentError
|
138
|
+
output.force_encoding("ISO-8859-1")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
require 'ffmpeg/version'
|
7
|
+
require 'ffmpeg/errors'
|
8
|
+
require 'ffmpeg/movie'
|
9
|
+
require 'ffmpeg/io_monkey'
|
10
|
+
require 'ffmpeg/transcoder'
|
11
|
+
require 'ffmpeg/encoding_options'
|
12
|
+
|
13
|
+
module FFMPEG
|
14
|
+
# FFMPEG logs information about its progress when it's transcoding.
|
15
|
+
# Jack in your own logger through this method if you wish to.
|
16
|
+
#
|
17
|
+
# @param [Logger] log your own logger
|
18
|
+
# @return [Logger] the logger you set
|
19
|
+
def self.logger=(log)
|
20
|
+
@logger = log
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get FFMPEG logger.
|
24
|
+
#
|
25
|
+
# @return [Logger]
|
26
|
+
def self.logger
|
27
|
+
return @logger if @logger
|
28
|
+
logger = Logger.new(STDOUT)
|
29
|
+
logger.level = Logger::INFO
|
30
|
+
@logger = logger
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the path of the ffmpeg binary.
|
34
|
+
# Can be useful if you need to specify a path such as /usr/local/bin/ffmpeg
|
35
|
+
#
|
36
|
+
# @param [String] path to the ffmpeg binary
|
37
|
+
# @return [String] the path you set
|
38
|
+
# @raise Errno::ENOENT if the ffmpeg binary cannot be found
|
39
|
+
def self.ffmpeg_binary=(bin)
|
40
|
+
if bin.is_a?(String) && !File.executable?(bin)
|
41
|
+
raise Errno::ENOENT, "the ffmpeg binary, \'#{bin}\', is not executable"
|
42
|
+
end
|
43
|
+
@ffmpeg_binary = bin
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get the path to the ffmpeg binary, defaulting to 'ffmpeg'
|
47
|
+
#
|
48
|
+
# @return [String] the path to the ffmpeg binary
|
49
|
+
# @raise Errno::ENOENT if the ffmpeg binary cannot be found
|
50
|
+
def self.ffmpeg_binary
|
51
|
+
@ffmpeg_binary || which('ffmpeg')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the path to the ffprobe binary, defaulting to what is on ENV['PATH']
|
55
|
+
#
|
56
|
+
# @return [String] the path to the ffprobe binary
|
57
|
+
# @raise Errno::ENOENT if the ffprobe binary cannot be found
|
58
|
+
def self.ffprobe_binary
|
59
|
+
@ffprobe_binary || which('ffprobe')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Set the path of the ffprobe binary.
|
63
|
+
# Can be useful if you need to specify a path such as /usr/local/bin/ffprobe
|
64
|
+
#
|
65
|
+
# @param [String] path to the ffprobe binary
|
66
|
+
# @return [String] the path you set
|
67
|
+
# @raise Errno::ENOENT if the ffprobe binary cannot be found
|
68
|
+
def self.ffprobe_binary=(bin)
|
69
|
+
if bin.is_a?(String) && !File.executable?(bin)
|
70
|
+
raise Errno::ENOENT, "the ffprobe binary, \'#{bin}\', is not executable"
|
71
|
+
end
|
72
|
+
@ffprobe_binary = bin
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get the maximum number of http redirect attempts
|
76
|
+
#
|
77
|
+
# @return [Integer] the maximum number of retries
|
78
|
+
def self.max_http_redirect_attempts
|
79
|
+
@max_http_redirect_attempts.nil? ? 10 : @max_http_redirect_attempts
|
80
|
+
end
|
81
|
+
|
82
|
+
# Set the maximum number of http redirect attempts.
|
83
|
+
#
|
84
|
+
# @param [Integer] the maximum number of retries
|
85
|
+
# @return [Integer] the number of retries you set
|
86
|
+
# @raise Errno::ENOENT if the value is negative or not an Integer
|
87
|
+
def self.max_http_redirect_attempts=(v)
|
88
|
+
raise Errno::ENOENT, 'max_http_redirect_attempts must be an integer' if v && !v.is_a?(Integer)
|
89
|
+
raise Errno::ENOENT, 'max_http_redirect_attempts may not be negative' if v && v < 0
|
90
|
+
@max_http_redirect_attempts = v
|
91
|
+
end
|
92
|
+
|
93
|
+
# Cross-platform way of finding an executable in the $PATH.
|
94
|
+
#
|
95
|
+
# which('ruby') #=> /usr/bin/ruby
|
96
|
+
# see: http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
|
97
|
+
def self.which(cmd)
|
98
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
99
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
100
|
+
exts.each { |ext|
|
101
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
102
|
+
return exe if File.executable? exe
|
103
|
+
}
|
104
|
+
end
|
105
|
+
raise Errno::ENOENT, "the #{cmd} binary could not be found in #{ENV['PATH']}"
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: d-streamio-ffmpeg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rackfish AB
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-06-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: multi_json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.1'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- support@rackfish.com
|
58
|
+
- bikeath1337.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- CHANGELOG
|
64
|
+
- LICENSE
|
65
|
+
- README.md
|
66
|
+
- lib/ffmpeg/encoding_options.rb
|
67
|
+
- lib/ffmpeg/errors.rb
|
68
|
+
- lib/ffmpeg/io_monkey.rb
|
69
|
+
- lib/ffmpeg/movie.rb
|
70
|
+
- lib/ffmpeg/transcoder.rb
|
71
|
+
- lib/ffmpeg/version.rb
|
72
|
+
- lib/streamio-ffmpeg.rb
|
73
|
+
homepage: https://github.com/luongvietdung/streamio-ffmpeg
|
74
|
+
licenses: []
|
75
|
+
metadata: {}
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubygems_version: 3.0.1
|
92
|
+
signing_key:
|
93
|
+
specification_version: 4
|
94
|
+
summary: Wraps ffmpeg to read metadata and transcodes videos.
|
95
|
+
test_files: []
|