ffmpeg_core 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4efe5152c9dd01b8ebb2d34c9f6d6877371d017845242297f57f65044c968648
4
- data.tar.gz: a1b09735ebc0acc9c21b252e3fa32b4168e0c5694eab8d72fa299e7c8c88dc9f
3
+ metadata.gz: afead7a6fce7f95748207153b521b35a223f2efd7add35664e12ba3e27f86b23
4
+ data.tar.gz: 3052072fb4a3541518cc5d12bf333b58b77a77f2b3b4deec44d51b89ddbac678
5
5
  SHA512:
6
- metadata.gz: f3c4ace64795f63226082eb3eeb6e395c0f35976b86de25a3a8ed7c46973ce7d2b234de2485a6e3cd083060653da3f2d6a8f8c3a254260a15d5e472462097a21
7
- data.tar.gz: 7b390fe28da3e17efe7a4523001aa41a0aa70685fc8e5d5951faf55e5d2feabe96e3aa5e48f94c0f3a0c70016a23b49c7b063739ae953bde41bba92b524e36b7
6
+ metadata.gz: d725d25a1d211186ede000ac81d58f20c57fc8f46ba5a3897b006a8e92e128b37f5f9cfb9295633bfe0a7dd878a0f99793329c388c1c2a920bc585256b4740db
7
+ data.tar.gz: 5e683ec8fb39dc1e669abaeaa8e3f7254de8f92ef4593ed6869ba7a5a4c02dab2d6ee8005243dbf31e24be27b11a689c89de736c0eca69e93be38b7d9e5fc531
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ 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.0] - 2026-01-26
9
+
10
+ ### Added
11
+
12
+ - **Complex Filters & Mapping:** Added `filter_graph` (for `-filter_complex`) and `maps` (for `-map`) options to `transcode`.
13
+ - **Hardware Acceleration:** Added `:hwaccel` option to `transcode` (supports `:nvenc`, `:vaapi`, `:qsv`) with automatic encoder detection.
14
+
15
+ ## [0.3.0] - 2026-01-16
16
+
17
+ ### Added
18
+
19
+ - **Remote Input Support:** `Movie.new` now accepts HTTP, HTTPS, RTMP, and RTSP URLs.
20
+ - **Crop Support:** Added `crop` option to `transcode` (e.g., `crop: { width: 100, height: 100, x: 10, y: 10 }`).
21
+ - **Advanced Metadata:** Added `video_profile` and `video_level` to `Probe`.
22
+ - Rotation detection from `side_data_list` for better compatibility with newer video formats.
23
+
24
+ ### Fixed
25
+
26
+ - **Rotation Geometry:** `Probe#width` and `Probe#height` now correctly swap values if the video is rotated 90 or 270 degrees.
27
+
8
28
  ## [0.2.0] - 2026-01-15
9
29
 
10
30
  ### Added
data/README.md CHANGED
@@ -11,6 +11,8 @@ 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)**
15
+ - **Remote input support (HTTP/HTTPS/RTMP/RTSP)**
14
16
  - Proper error handling with detailed context
15
17
  - Thread-safe configuration
16
18
  - Simple, intuitive API
@@ -41,19 +43,26 @@ bundle install
41
43
  ```ruby
42
44
  require "ffmpeg_core"
43
45
 
44
- # Load a video file
46
+ # Load a video file or remote URL
45
47
  movie = FFmpegCore::Movie.new("input.mp4")
48
+ # movie = FFmpegCore::Movie.new("http://example.com/video.mp4")
46
49
 
47
50
  # Get metadata
48
51
  movie.duration # => 120.5 (seconds)
49
- movie.resolution # => "1920x1080"
52
+ movie.resolution # => "1920x1080" (automatically swapped if rotated)
53
+ movie.width # => 1920
54
+ movie.height # => 1080
50
55
  movie.video_codec # => "h264"
51
56
  movie.audio_codec # => "aac"
52
57
  movie.frame_rate # => 29.97
53
58
  movie.bitrate # => 5000 (kb/s)
54
- movie.rotation # => 90 (degrees)
55
- movie.aspect_ratio # => "16:9"
56
59
  movie.valid? # => true
60
+
61
+ # Access detailed metadata via probe
62
+ movie.probe.rotation # => 90 (degrees)
63
+ movie.probe.aspect_ratio # => "16:9"
64
+ movie.probe.video_profile # => "High"
65
+ movie.probe.video_level # => 41
57
66
  ```
58
67
 
59
68
  ### Transcoding
@@ -71,13 +80,76 @@ movie.transcode("output.mp4", {
71
80
  video_codec: "libx264",
72
81
  audio_codec: "aac",
73
82
  video_bitrate: "1000k",
83
+ audio_bitrate: "128k",
74
84
  resolution: "1280x720",
85
+ crop: { width: 500, height: 500, x: 10, y: 10 }, # Crop video
75
86
  video_filter: "scale=1280:-1,transpose=1", # Resize and rotate
87
+ audio_filter: "volume=0.5", # Reduce volume
76
88
  preset: "slow", # ffmpeg preset (ultrafast, fast, medium, slow, etc.)
77
89
  crf: 23 # Constant Rate Factor (0-51)
78
90
  })
79
91
  ```
80
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
+
119
+ ### Using Filters
120
+
121
+ FFmpegCore supports raw FFmpeg filter strings for both video (`video_filter` or `-vf`) and audio (`audio_filter` or `-af`).
122
+
123
+ **Common Video Filters:**
124
+
125
+ ```ruby
126
+ movie.transcode("output.mp4", {
127
+ # Scale to width 1280, keep aspect ratio
128
+ video_filter: "scale=1280:-1",
129
+
130
+ # Crop 100x100 starting at position (10,10)
131
+ video_filter: "crop=100:100:10:10",
132
+
133
+ # Rotate 90 degrees clockwise
134
+ video_filter: "transpose=1",
135
+
136
+ # Chain multiple filters (Scale then Rotate)
137
+ video_filter: "scale=1280:-1,transpose=1"
138
+ })
139
+ ```
140
+
141
+ **Common Audio Filters:**
142
+
143
+ ```ruby
144
+ movie.transcode("output.mp4", {
145
+ # Increase volume by 50%
146
+ audio_filter: "volume=1.5",
147
+
148
+ # Fade in first 5 seconds
149
+ audio_filter: "afade=t=in:ss=0:d=5"
150
+ })
151
+ ```
152
+
81
153
  ### Screenshots
82
154
 
83
155
  ```ruby
@@ -86,7 +158,7 @@ movie = FFmpegCore::Movie.new("input.mp4")
86
158
  # Extract screenshot at specific time
87
159
  movie.screenshot("thumbnail.jpg", seek_time: 5)
88
160
 
89
- # With resolution
161
+ # With resolution and quality
90
162
  movie.screenshot("thumbnail.jpg", {
91
163
  seek_time: 10,
92
164
  resolution: "640x360",
@@ -106,7 +178,7 @@ end
106
178
 
107
179
  ## Error Handling
108
180
 
109
- FFmpegCore provides specific error classes for different failure scenarios:
181
+ FFmpegCore provides specific error classes for different failure scenarios. All execution errors (transcoding, probing, screenshots) inherit from `FFmpegCore::ExecutionError`, which provides access to the command, exit status, and stderr output.
110
182
 
111
183
  ```ruby
112
184
  begin
@@ -115,9 +187,9 @@ begin
115
187
  rescue FFmpegCore::InvalidInputError => e
116
188
  # File doesn't exist or is not readable
117
189
  puts "Input error: #{e.message}"
118
- rescue FFmpegCore::TranscodingError => e
119
- # FFmpeg transcoding failed
120
- puts "Transcoding failed: #{e.message}"
190
+ rescue FFmpegCore::ExecutionError => e
191
+ # Covers TranscodingError, ProbeError, and ScreenshotError
192
+ puts "Execution failed: #{e.message}"
121
193
  puts "Command: #{e.command}"
122
194
  puts "Exit status: #{e.exit_status}"
123
195
  puts "Stderr: #{e.stderr}"
@@ -129,14 +201,16 @@ end
129
201
 
130
202
  ### Error Classes
131
203
 
132
- | Error | Description |
133
- | --------------------------------- | -------------------------------------- |
134
- | `FFmpegCore::Error` | Base error class |
135
- | `FFmpegCore::BinaryNotFoundError` | FFmpeg/FFprobe not found |
136
- | `FFmpegCore::InvalidInputError` | Input file doesn't exist or unreadable |
137
- | `FFmpegCore::ProbeError` | Failed to extract metadata |
138
- | `FFmpegCore::TranscodingError` | FFmpeg transcoding failed |
139
- | `FFmpegCore::ScreenshotError` | Screenshot extraction failed |
204
+ | Error | Description | Parent |
205
+ | --------------------------------- | -------------------------------------- | ------ |
206
+ | `FFmpegCore::Error` | Base error class | StandardError |
207
+ | `FFmpegCore::BinaryNotFoundError` | FFmpeg/FFprobe not found | Error |
208
+ | `FFmpegCore::InvalidInputError` | Input file doesn't exist or unreadable | Error |
209
+ | `FFmpegCore::OutputError` | Output file cannot be written | Error |
210
+ | `FFmpegCore::ExecutionError` | Base for command execution errors | Error |
211
+ | `FFmpegCore::ProbeError` | Failed to extract metadata | ExecutionError |
212
+ | `FFmpegCore::TranscodingError` | FFmpeg transcoding failed | ExecutionError |
213
+ | `FFmpegCore::ScreenshotError` | Screenshot extraction failed | ExecutionError |
140
214
 
141
215
  ## Development
142
216
 
@@ -153,4 +227,4 @@ bundle exec rubocop
153
227
 
154
228
  ## License
155
229
 
156
- MIT License. See [LICENSE.txt](LICENSE.txt) for details.
230
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
data/lefthook.yml ADDED
@@ -0,0 +1,8 @@
1
+ pre-commit:
2
+ parallel: true
3
+ jobs:
4
+ - name: rubocop
5
+ glob: "*.rb"
6
+ run: bundle exec rubocop --force-exclusion {staged_files}
7
+ - name: rspec
8
+ run: bundle exec rspec
@@ -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,8 +15,30 @@ 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
 
24
+ def detect_encoders
25
+ return Set.new unless @ffmpeg_binary
26
+
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
36
+ end
37
+ encoders
38
+ rescue
39
+ Set.new
40
+ end
41
+
16
42
  def detect_binary(name)
17
43
  # Check common locations
18
44
  paths = ENV["PATH"].split(File::PATH_SEPARATOR)
@@ -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
@@ -39,16 +39,32 @@ module FFmpegCore
39
39
  video_stream&.dig("codec_name")
40
40
  end
41
41
 
42
+ def video_profile
43
+ video_stream&.dig("profile")
44
+ end
45
+
46
+ def video_level
47
+ video_stream&.dig("level")
48
+ end
49
+
42
50
  def audio_codec
43
51
  audio_stream&.dig("codec_name")
44
52
  end
45
53
 
46
54
  def width
47
- video_stream&.dig("width")
55
+ val = video_stream&.dig("width")
56
+ return val unless val
57
+ return val if (rotation || 0) % 180 == 0
58
+
59
+ video_stream&.dig("height")
48
60
  end
49
61
 
50
62
  def height
51
- video_stream&.dig("height")
63
+ val = video_stream&.dig("height")
64
+ return val unless val
65
+ return val if (rotation || 0) % 180 == 0
66
+
67
+ video_stream&.dig("width")
52
68
  end
53
69
 
54
70
  def frame_rate
@@ -79,6 +95,10 @@ module FFmpegCore
79
95
  tags = video_stream.fetch("tags", {})
80
96
  return tags["rotate"].to_i if tags["rotate"]
81
97
 
98
+ # Try side_data_list (common in some newer formats)
99
+ side_data = video_stream.fetch("side_data_list", []).find { |sd| sd.key?("rotation") }
100
+ return side_data["rotation"].to_i if side_data
101
+
82
102
  # Default to 0 if not found
83
103
  0
84
104
  end
@@ -102,6 +122,8 @@ module FFmpegCore
102
122
  end
103
123
 
104
124
  def validate_file!
125
+ return if %r{^(https?|rtmp|rtsp)://}.match?(path)
126
+
105
127
  raise InvalidInputError, "File does not exist: #{path}" unless File.exist?(path)
106
128
  raise InvalidInputError, "File is not readable: #{path}" unless File.readable?(path)
107
129
  end
@@ -25,6 +25,8 @@ module FFmpegCore
25
25
  private
26
26
 
27
27
  def validate_input!
28
+ return if %r{^(https?|rtmp|rtsp)://}.match?(input_path)
29
+
28
30
  raise InvalidInputError, "Input file does not exist: #{input_path}" unless File.exist?(input_path)
29
31
  end
30
32
 
@@ -36,6 +38,9 @@ module FFmpegCore
36
38
  def build_command
37
39
  cmd = [FFmpegCore.configuration.ffmpeg_binary]
38
40
 
41
+ # Apply HW Accel if requested
42
+ cmd += resolve_hwaccel_codec
43
+
39
44
  # Input file
40
45
  cmd += ["-i", input_path]
41
46
 
@@ -57,8 +62,16 @@ module FFmpegCore
57
62
  # Frame rate
58
63
  cmd += ["-r", options[:frame_rate].to_s] if options[:frame_rate]
59
64
 
60
- # Video filter
61
- cmd += ["-vf", options[:video_filter]] if options[:video_filter]
65
+ # Video filters
66
+ video_filters = []
67
+ video_filters << options[:video_filter] if options[:video_filter]
68
+
69
+ if options[:crop]
70
+ crop = options[:crop]
71
+ video_filters << "crop=#{crop[:width]}:#{crop[:height]}:#{crop[:x]}:#{crop[:y]}"
72
+ end
73
+
74
+ cmd += ["-vf", video_filters.join(",")] unless video_filters.empty?
62
75
 
63
76
  # Audio filter
64
77
  cmd += ["-af", options[:audio_filter]] if options[:audio_filter]
@@ -69,6 +82,21 @@ module FFmpegCore
69
82
  # Constant Rate Factor (CRF)
70
83
  cmd += ["-crf", options[:crf].to_s] if options[:crf]
71
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
+
72
100
  # Custom options (array of strings)
73
101
  cmd += options[:custom] if options[:custom]
74
102
 
@@ -137,5 +165,50 @@ module FFmpegCore
137
165
  end
138
166
  end
139
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
140
213
  end
141
214
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FFmpegCore
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffmpeg_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev
@@ -21,6 +21,7 @@ files:
21
21
  - LICENSE.txt
22
22
  - README.md
23
23
  - Rakefile
24
+ - lefthook.yml
24
25
  - lib/ffmpeg_core.rb
25
26
  - lib/ffmpeg_core/configuration.rb
26
27
  - lib/ffmpeg_core/errors.rb
@@ -52,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
53
  - !ruby/object:Gem::Version
53
54
  version: '0'
54
55
  requirements: []
55
- rubygems_version: 4.0.3
56
+ rubygems_version: 4.0.4
56
57
  specification_version: 4
57
58
  summary: Modern Ruby wrapper for FFmpeg
58
59
  test_files: []