ffmpeg_core 0.3.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +28 -2
- data/lefthook.yml +8 -0
- data/lib/ffmpeg_core/configuration.rb +72 -15
- data/lib/ffmpeg_core/movie.rb +3 -0
- data/lib/ffmpeg_core/probe.rb +1 -1
- data/lib/ffmpeg_core/transcoder.rb +66 -3
- data/lib/ffmpeg_core/version.rb +2 -2
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3a1322c33ef390363223c0f3a307f269e84ed3484d2491e2d7340111d33d7a6
|
|
4
|
+
data.tar.gz: d98fcf0cf9881e837783cda58d1d43784c87310db8e22468e5b0623c9a8d6716
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e50f4fb86e61b7d7e858ed90a047f37fa92d7ec9c9216cf8238b7aaf7f48b7c78aa237c36a8c154e864976e8b65c0ac78a93697c1047342df113037fed6f035
|
|
7
|
+
data.tar.gz: 621b5d98bd486b260a4c208c091f18113484d29df66cb005f8e005204dedd324fc5d737e22d662bbf2dbda788f8cdba002a5a313ba18abcc28d926c948c2cf11
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-04-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Binary detection on Windows: `where` fallback now correctly resolves ffmpeg/ffprobe when not on PATH
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Simplified binary detection: removed redundant Ruby PATH scan, extracted lookup steps into focused private methods
|
|
17
|
+
- Expanded `Configuration` spec: ENV override, `BinaryNotFoundError`, known-path fallback, `reset_configuration!`
|
|
18
|
+
|
|
19
|
+
## [0.4.0] - 2026-01-26
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Complex Filters & Mapping:** Added `filter_graph` (for `-filter_complex`) and `maps` (for `-map`) options to `transcode`.
|
|
24
|
+
- **Hardware Acceleration:** Added `:hwaccel` option to `transcode` (supports `:nvenc`, `:vaapi`, `:qsv`) with automatic encoder detection.
|
|
25
|
+
|
|
8
26
|
## [0.3.0] - 2026-01-16
|
|
9
27
|
|
|
10
28
|
### Added
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Modern Ruby wrapper for FFmpeg with clean API and proper error handling.
|
|
|
11
11
|
- Zero runtime dependencies
|
|
12
12
|
- **Real-time progress reporting**
|
|
13
13
|
- **Support for video/audio filters and quality presets**
|
|
14
|
+
- **Hardware Acceleration (NVENC, VAAPI, QSV)**
|
|
14
15
|
- **Remote input support (HTTP/HTTPS/RTMP/RTSP)**
|
|
15
16
|
- Proper error handling with detailed context
|
|
16
17
|
- Thread-safe configuration
|
|
@@ -85,11 +86,36 @@ movie.transcode("output.mp4", {
|
|
|
85
86
|
video_filter: "scale=1280:-1,transpose=1", # Resize and rotate
|
|
86
87
|
audio_filter: "volume=0.5", # Reduce volume
|
|
87
88
|
preset: "slow", # ffmpeg preset (ultrafast, fast, medium, slow, etc.)
|
|
88
|
-
crf: 23
|
|
89
|
-
custom: %w[-map 0:v -map 0:a] # Custom FFmpeg flags
|
|
89
|
+
crf: 23 # Constant Rate Factor (0-51)
|
|
90
90
|
})
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Complex Filter Graphs & Stream Mapping
|
|
94
|
+
|
|
95
|
+
Use structured APIs for `-filter_complex` and `-map` to build complex pipelines without raw string hacks.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
movie.transcode("out.mp4", {
|
|
99
|
+
filter_graph: [
|
|
100
|
+
"[0:v]crop=320:240:0:0[c]",
|
|
101
|
+
"[c]scale=640:480[outv]"
|
|
102
|
+
],
|
|
103
|
+
maps: ["[outv]", "0:a"]
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Hardware Acceleration
|
|
108
|
+
|
|
109
|
+
Opt-in to hardware-accelerated encoding with automatic encoder detection and graceful fallback.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# Automatically switches to h264_nvenc if available, falls back to libx264 otherwise
|
|
113
|
+
movie.transcode("out.mp4", hwaccel: :nvenc)
|
|
114
|
+
|
|
115
|
+
# Supports :nvenc, :vaapi, and :qsv
|
|
116
|
+
movie.transcode("out.mp4", hwaccel: :vaapi)
|
|
117
|
+
```
|
|
118
|
+
|
|
93
119
|
### Using Filters
|
|
94
120
|
|
|
95
121
|
FFmpegCore supports raw FFmpeg filter strings for both video (`video_filter` or `-vf`) and audio (`audio_filter` or `-af`).
|
data/lefthook.yml
ADDED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
require "open3"
|
|
6
|
+
|
|
3
7
|
module FFmpegCore
|
|
4
8
|
# Configuration for FFmpegCore library
|
|
5
9
|
class Configuration
|
|
@@ -11,26 +15,79 @@ module FFmpegCore
|
|
|
11
15
|
@timeout = 30 # seconds
|
|
12
16
|
end
|
|
13
17
|
|
|
18
|
+
def encoders
|
|
19
|
+
@encoders ||= detect_encoders
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
private
|
|
15
23
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
paths = ENV["PATH"].split(File::PATH_SEPARATOR)
|
|
19
|
-
paths.each do |path|
|
|
20
|
-
binary = File.join(path, name)
|
|
21
|
-
return binary if File.executable?(binary)
|
|
22
|
-
end
|
|
24
|
+
def detect_encoders
|
|
25
|
+
return Set.new unless @ffmpeg_binary
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
stdout, _stderr, status = Open3.capture3(@ffmpeg_binary, "-encoders")
|
|
28
|
+
return Set.new unless status.success?
|
|
29
|
+
|
|
30
|
+
encoders = Set.new
|
|
31
|
+
stdout.each_line do |line|
|
|
32
|
+
# Match lines like: " V..... libx264 ..."
|
|
33
|
+
if line =~ /^\s*[VAS][\w.]+\s+(\w+)/
|
|
34
|
+
encoders.add($1)
|
|
35
|
+
end
|
|
31
36
|
end
|
|
37
|
+
encoders
|
|
38
|
+
rescue
|
|
39
|
+
Set.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def detect_binary(name)
|
|
43
|
+
binary_from_env(name) ||
|
|
44
|
+
binary_from_system_lookup(name) ||
|
|
45
|
+
binary_from_known_paths(name) ||
|
|
46
|
+
raise(BinaryNotFoundError, <<~MSG)
|
|
47
|
+
#{name} not found.
|
|
48
|
+
Install FFmpeg and ensure it's in PATH.
|
|
49
|
+
macOS: brew install ffmpeg
|
|
50
|
+
Linux: apt install ffmpeg / yum install ffmpeg
|
|
51
|
+
Windows: choco install ffmpeg or scoop install ffmpeg
|
|
52
|
+
MSG
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Checks FFMPEGCORE_<NAME> env variable for an explicit binary override.
|
|
56
|
+
def binary_from_env(name)
|
|
57
|
+
path = ENV["FFMPEGCORE_#{name.upcase}"]
|
|
58
|
+
path if path && File.executable?(path)
|
|
59
|
+
end
|
|
32
60
|
|
|
33
|
-
|
|
61
|
+
# Uses the OS-native `which` (Unix) or `where` (Windows) command.
|
|
62
|
+
def binary_from_system_lookup(name)
|
|
63
|
+
cmd = Gem.win_platform? ? "where #{name}" : "which #{name}"
|
|
64
|
+
stdout, status = Open3.capture2(cmd)
|
|
65
|
+
return unless status.success?
|
|
66
|
+
|
|
67
|
+
path = stdout.lines.first&.strip
|
|
68
|
+
path if path && File.executable?(path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Falls back to a list of well-known installation paths.
|
|
72
|
+
def binary_from_known_paths(name)
|
|
73
|
+
known_paths(name).find { |p| File.executable?(p) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def known_paths(name)
|
|
77
|
+
if Gem.win_platform?
|
|
78
|
+
[
|
|
79
|
+
"C:/ffmpeg/bin/#{name}.exe",
|
|
80
|
+
"C:/ProgramData/chocolatey/bin/#{name}.exe",
|
|
81
|
+
"#{ENV["USERPROFILE"]}/scoop/apps/ffmpeg/current/bin/#{name}.exe"
|
|
82
|
+
]
|
|
83
|
+
else
|
|
84
|
+
[
|
|
85
|
+
"/opt/homebrew/bin/#{name}", # macOS ARM
|
|
86
|
+
"/usr/local/bin/#{name}", # macOS Intel / Linux
|
|
87
|
+
"/usr/bin/#{name}",
|
|
88
|
+
"/snap/bin/#{name}"
|
|
89
|
+
]
|
|
90
|
+
end
|
|
34
91
|
end
|
|
35
92
|
end
|
|
36
93
|
|
data/lib/ffmpeg_core/movie.rb
CHANGED
|
@@ -26,6 +26,9 @@ module FFmpegCore
|
|
|
26
26
|
# @option options [String, Integer] :audio_bitrate Audio bitrate (e.g., "128k" or 128)
|
|
27
27
|
# @option options [String] :resolution Resolution (e.g., "1280x720")
|
|
28
28
|
# @option options [Integer, Float] :frame_rate Frame rate (e.g., 30)
|
|
29
|
+
# @option options [Array<String>, String] :filter_graph Complex filter graph (e.g., ["[0:v]crop=..."])
|
|
30
|
+
# @option options [Array<String>, String] :maps Stream maps (e.g., ["[outv]", "0:a"])
|
|
31
|
+
# @option options [Symbol] :hwaccel Hardware acceleration (:nvenc, :vaapi, :qsv)
|
|
29
32
|
# @option options [Array<String>] :custom Custom FFmpeg flags
|
|
30
33
|
# @yield [Float] Progress ratio (0.0 to 1.0)
|
|
31
34
|
# @return [String] Path to transcoded file
|
data/lib/ffmpeg_core/probe.rb
CHANGED
|
@@ -122,7 +122,7 @@ module FFmpegCore
|
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
def validate_file!
|
|
125
|
-
return if
|
|
125
|
+
return if %r{^(https?|rtmp|rtsp)://}.match?(path)
|
|
126
126
|
|
|
127
127
|
raise InvalidInputError, "File does not exist: #{path}" unless File.exist?(path)
|
|
128
128
|
raise InvalidInputError, "File is not readable: #{path}" unless File.readable?(path)
|
|
@@ -25,7 +25,7 @@ module FFmpegCore
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
def validate_input!
|
|
28
|
-
return if
|
|
28
|
+
return if %r{^(https?|rtmp|rtsp)://}.match?(input_path)
|
|
29
29
|
|
|
30
30
|
raise InvalidInputError, "Input file does not exist: #{input_path}" unless File.exist?(input_path)
|
|
31
31
|
end
|
|
@@ -38,6 +38,9 @@ module FFmpegCore
|
|
|
38
38
|
def build_command
|
|
39
39
|
cmd = [FFmpegCore.configuration.ffmpeg_binary]
|
|
40
40
|
|
|
41
|
+
# Apply HW Accel if requested
|
|
42
|
+
cmd += resolve_hwaccel_codec
|
|
43
|
+
|
|
41
44
|
# Input file
|
|
42
45
|
cmd += ["-i", input_path]
|
|
43
46
|
|
|
@@ -62,12 +65,12 @@ module FFmpegCore
|
|
|
62
65
|
# Video filters
|
|
63
66
|
video_filters = []
|
|
64
67
|
video_filters << options[:video_filter] if options[:video_filter]
|
|
65
|
-
|
|
68
|
+
|
|
66
69
|
if options[:crop]
|
|
67
70
|
crop = options[:crop]
|
|
68
71
|
video_filters << "crop=#{crop[:width]}:#{crop[:height]}:#{crop[:x]}:#{crop[:y]}"
|
|
69
72
|
end
|
|
70
|
-
|
|
73
|
+
|
|
71
74
|
cmd += ["-vf", video_filters.join(",")] unless video_filters.empty?
|
|
72
75
|
|
|
73
76
|
# Audio filter
|
|
@@ -79,6 +82,21 @@ module FFmpegCore
|
|
|
79
82
|
# Constant Rate Factor (CRF)
|
|
80
83
|
cmd += ["-crf", options[:crf].to_s] if options[:crf]
|
|
81
84
|
|
|
85
|
+
# Filter Complex / Filter Graph
|
|
86
|
+
filter_graph = options[:filter_graph] || options[:filter_complex]
|
|
87
|
+
if filter_graph
|
|
88
|
+
graph_string = filter_graph.is_a?(Array) ? filter_graph.join(";") : filter_graph
|
|
89
|
+
cmd += ["-filter_complex", graph_string]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Maps
|
|
93
|
+
maps = options[:maps] || options[:map]
|
|
94
|
+
if maps
|
|
95
|
+
Array(maps).each do |map|
|
|
96
|
+
cmd += ["-map", map]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
82
100
|
# Custom options (array of strings)
|
|
83
101
|
cmd += options[:custom] if options[:custom]
|
|
84
102
|
|
|
@@ -147,5 +165,50 @@ module FFmpegCore
|
|
|
147
165
|
end
|
|
148
166
|
end
|
|
149
167
|
end
|
|
168
|
+
|
|
169
|
+
def resolve_hwaccel_codec
|
|
170
|
+
return [] unless options[:hwaccel]
|
|
171
|
+
|
|
172
|
+
current_codec = options[:video_codec] || "libx264"
|
|
173
|
+
family = detect_codec_family(current_codec)
|
|
174
|
+
return [] unless family
|
|
175
|
+
|
|
176
|
+
hw_type = options[:hwaccel].to_sym
|
|
177
|
+
target_encoder = HW_ENCODERS.dig(family, hw_type)
|
|
178
|
+
|
|
179
|
+
if target_encoder && FFmpegCore.configuration.encoders.include?(target_encoder)
|
|
180
|
+
options[:video_codec] = target_encoder
|
|
181
|
+
return HW_FLAGS[hw_type] || []
|
|
182
|
+
end
|
|
183
|
+
[]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def detect_codec_family(codec)
|
|
187
|
+
case codec.to_s
|
|
188
|
+
when /x264|h264|avc/i
|
|
189
|
+
:h264
|
|
190
|
+
when /x265|hevc/i
|
|
191
|
+
:hevc
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
HW_FLAGS = {
|
|
196
|
+
vaapi: ["-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi"],
|
|
197
|
+
qsv: ["-hwaccel", "qsv"],
|
|
198
|
+
nvenc: ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"]
|
|
199
|
+
}.freeze
|
|
200
|
+
|
|
201
|
+
HW_ENCODERS = {
|
|
202
|
+
h264: {
|
|
203
|
+
nvenc: "h264_nvenc",
|
|
204
|
+
vaapi: "h264_vaapi",
|
|
205
|
+
qsv: "h264_qsv"
|
|
206
|
+
},
|
|
207
|
+
hevc: {
|
|
208
|
+
nvenc: "hevc_nvenc",
|
|
209
|
+
vaapi: "hevc_vaapi",
|
|
210
|
+
qsv: "hevc_qsv"
|
|
211
|
+
}
|
|
212
|
+
}.freeze
|
|
150
213
|
end
|
|
151
214
|
end
|
data/lib/ffmpeg_core/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ffmpeg_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexey Poimtsev
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: simplecov
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
description: A clean, well-tested FFmpeg wrapper with modern Ruby conventions, proper
|
|
13
27
|
error handling, and zero dependencies.
|
|
14
28
|
email:
|
|
@@ -21,6 +35,7 @@ files:
|
|
|
21
35
|
- LICENSE.txt
|
|
22
36
|
- README.md
|
|
23
37
|
- Rakefile
|
|
38
|
+
- lefthook.yml
|
|
24
39
|
- lib/ffmpeg_core.rb
|
|
25
40
|
- lib/ffmpeg_core/configuration.rb
|
|
26
41
|
- lib/ffmpeg_core/errors.rb
|
|
@@ -52,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
52
67
|
- !ruby/object:Gem::Version
|
|
53
68
|
version: '0'
|
|
54
69
|
requirements: []
|
|
55
|
-
rubygems_version: 4.0.
|
|
70
|
+
rubygems_version: 4.0.6
|
|
56
71
|
specification_version: 4
|
|
57
72
|
summary: Modern Ruby wrapper for FFmpeg
|
|
58
73
|
test_files: []
|