ffmpeg_core 0.5.0 → 0.6.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: bb9323f5aae7b7f162d4b43f219f14506af5fa012e033451a2021e92bec14128
4
- data.tar.gz: 7164b154e6b542e2179f430418cd3b42fb68cc48a5dff62a679b13eb135b3de7
3
+ metadata.gz: 83185cde1635bd7362c737cba6da5c723be5407cd7457ea5bbac2e0b43206365
4
+ data.tar.gz: bf4dea2ee0d08d1e8af7b8193cef697f2b8dc1fcda8f5d86efed89e1d23c8f4e
5
5
  SHA512:
6
- metadata.gz: f97a26864b497d77ae8b862daa0b45154f038cbc94dfbf56703a4879ccbac680b294acdacf35d20538fb736d046dbd85c55514bc6d7e7a8c0d6c41030878f40d
7
- data.tar.gz: 5235e82f757aa787cb76dbed3af50a5bd4eff074c9476918f90d9d8c8187f658979a6ca0b773d762569f631394326c27c45d45d265f1a0e13f9eb2890a1bba26
6
+ metadata.gz: fbe0cf4fd2b09c72c143aeeb1281889f7291e816d7602c6d12852503f647fbc71100e106cf41c33c5e76b9728258d3ecc547b1407632748b6512e92086376265
7
+ data.tar.gz: 056ea62e71626ac7969cee7242fb14936b15aa9098311b631c36f14c87c61289f6da48a41e99e7745fb7032965a3550d9f1c59e56969b43bba6af33f4b25977f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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.6.0] - 2026-04-14
9
+
10
+ ### Added
11
+
12
+ - **Compositor — multi-input video composition:**
13
+ - New `FFmpegCore::Compositor` class for operations requiring multiple input files in a single FFmpeg command
14
+ - Accepts an array of local paths or remote URLs as `input_paths`
15
+ - `:filter_complex` option (String or Array — array elements are joined with `;`)
16
+ - `:maps` option for stream mapping
17
+ - Progress reporting via block (requires `:duration`)
18
+ - Typical use cases: overlay (picture-in-picture), concatenation, side-by-side (`hstack`)
19
+ - README updated with usage examples
20
+
8
21
  ## [0.5.0] - 2026-04-13
9
22
 
10
23
  ### Added
data/README.md CHANGED
@@ -11,12 +11,25 @@ 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
+ - **Hardware Acceleration (NVENC, VAAPI, QSV, Vulkan AV1, D3D12)**
15
15
  - **Remote input support (HTTP/HTTPS/RTMP/RTSP)**
16
+ - **Rich metadata: chapters, subtitles, EXIF, audio properties**
17
+ - **Video operations: cut, audio extraction, batch screenshots**
18
+ - **Multi-input composition: overlay, concatenation, side-by-side**
16
19
  - Proper error handling with detailed context
17
20
  - Thread-safe configuration
18
21
  - Simple, intuitive API
19
22
 
23
+ ## FFmpeg Version Requirements
24
+
25
+ | Feature | Minimum FFmpeg version |
26
+ |---|---|
27
+ | Core transcoding, probing | Any recent version |
28
+ | AV1 hardware encoding (Vulkan) | 8.0+ |
29
+ | EXIF metadata parsing | 8.1+ |
30
+ | D3D12 hardware acceleration | 8.1+ |
31
+ | Chapter metadata (`chapters`) | Any (via `-show_chapters`) |
32
+
20
33
  ## Requirements
21
34
 
22
35
  - Ruby 3.2+
@@ -47,7 +60,7 @@ require "ffmpeg_core"
47
60
  movie = FFmpegCore::Movie.new("input.mp4")
48
61
  # movie = FFmpegCore::Movie.new("http://example.com/video.mp4")
49
62
 
50
- # Get metadata
63
+ # Basic metadata
51
64
  movie.duration # => 120.5 (seconds)
52
65
  movie.resolution # => "1920x1080" (automatically swapped if rotated)
53
66
  movie.width # => 1920
@@ -57,12 +70,32 @@ movie.audio_codec # => "aac"
57
70
  movie.frame_rate # => 29.97
58
71
  movie.bitrate # => 5000 (kb/s)
59
72
  movie.valid? # => true
73
+ movie.has_video? # => true
74
+ movie.has_audio? # => true
60
75
 
61
- # Access detailed metadata via probe
76
+ # Video stream details
62
77
  movie.probe.rotation # => 90 (degrees)
63
78
  movie.probe.aspect_ratio # => "16:9"
64
79
  movie.probe.video_profile # => "High"
65
80
  movie.probe.video_level # => 41
81
+ movie.probe.pixel_format # => "yuv420p"
82
+
83
+ # Audio stream details
84
+ movie.probe.audio_sample_rate # => 48000
85
+ movie.probe.audio_channels # => 2
86
+ movie.probe.audio_channel_layout # => "stereo"
87
+ movie.probe.audio_streams # => [{ "codec_type" => "audio", ... }, ...]
88
+
89
+ # Subtitles and chapters
90
+ movie.probe.subtitle_streams # => [{ "codec_name" => "subrip", ... }]
91
+ movie.probe.chapters # => [{ "tags" => { "title" => "Intro" }, ... }]
92
+
93
+ # File-level tags and EXIF (FFmpeg 8.1+ for full EXIF support)
94
+ movie.probe.tags # => { "title" => "My Video", "artist" => "Author" }
95
+ movie.probe.exif # => { "creation_time" => "2024-06-15T14:30:00Z", ... }
96
+
97
+ # Container format
98
+ movie.probe.format_name # => "mov,mp4,m4a,3gp,3g2,mj2"
66
99
  ```
67
100
 
68
101
  ### Transcoding
@@ -104,18 +137,103 @@ movie.transcode("out.mp4", {
104
137
  })
105
138
  ```
106
139
 
140
+ ### Multi-Input Composition
141
+
142
+ Use `FFmpegCore::Compositor` when you need multiple input files in a single FFmpeg command — overlay, concatenation, side-by-side, and any other `-filter_complex` operation.
143
+
144
+ ```ruby
145
+ # Overlay (picture-in-picture): place overlay.mp4 on top of background.mp4
146
+ FFmpegCore::Compositor.new(
147
+ ["background.mp4", "overlay.mp4"],
148
+ "output.mp4",
149
+ filter_complex: "[0:v][1:v]overlay=10:10[v]",
150
+ maps: ["[v]", "0:a"]
151
+ ).run
152
+
153
+ # Concatenate two clips sequentially
154
+ FFmpegCore::Compositor.new(
155
+ ["part1.mp4", "part2.mp4"],
156
+ "output.mp4",
157
+ filter_complex: "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]",
158
+ maps: ["[v]", "[a]"]
159
+ ).run
160
+
161
+ # Side-by-side (horizontal stack)
162
+ FFmpegCore::Compositor.new(
163
+ ["left.mp4", "right.mp4"],
164
+ "output.mp4",
165
+ filter_complex: "[0:v][1:v]hstack[v]",
166
+ maps: ["[v]", "0:a"]
167
+ ).run
168
+
169
+ # With progress reporting (requires :duration)
170
+ FFmpegCore::Compositor.new(
171
+ ["a.mp4", "b.mp4"],
172
+ "output.mp4",
173
+ filter_complex: "[0:v][1:v]overlay[v]",
174
+ maps: ["[v]", "0:a"],
175
+ duration: 120.0
176
+ ).run do |progress|
177
+ puts "#{(progress * 100).round}%"
178
+ end
179
+ ```
180
+
181
+ > **Note:** Stream indices (`[0:v]`, `[1:v]`, etc.) correspond to the position of each file in the input array.
182
+
183
+ ### Cutting / Trimming
184
+
185
+ Lossless trim using stream copy — no re-encoding, nearly instant:
186
+
187
+ ```ruby
188
+ # Trim by start time + duration
189
+ movie.cut("clip.mp4", start_time: 30, duration: 60)
190
+
191
+ # Trim by start and end time
192
+ movie.cut("clip.mp4", start_time: 30, end_time: 90)
193
+ ```
194
+
195
+ > **Note:** `-c copy` seeks to the nearest keyframe. For frame-accurate trimming, use `transcode` with `custom: ["-ss", "30", "-to", "90"]`.
196
+
197
+ ### Audio Extraction
198
+
199
+ ```ruby
200
+ # Extract audio with automatic codec detection from file extension
201
+ movie.extract_audio("audio.aac")
202
+
203
+ # Specify codec explicitly
204
+ movie.extract_audio("audio.mp3", codec: "libmp3lame")
205
+ movie.extract_audio("audio.opus", codec: "libopus")
206
+ ```
207
+
208
+ ### Multiple Screenshots
209
+
210
+ ```ruby
211
+ # Extract 5 screenshots distributed evenly across the video
212
+ paths = movie.screenshots("thumbs/", count: 5)
213
+ # => ["thumbs/screenshot_001.jpg", ..., "thumbs/screenshot_005.jpg"]
214
+ ```
215
+
107
216
  ### Hardware Acceleration
108
217
 
109
218
  Opt-in to hardware-accelerated encoding with automatic encoder detection and graceful fallback.
110
219
 
111
220
  ```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)
221
+ # H.264 / HEVC classic accelerators
222
+ movie.transcode("out.mp4", hwaccel: :nvenc) # NVIDIA CUDA
223
+ movie.transcode("out.mp4", hwaccel: :vaapi) # Linux VAAPI
224
+ movie.transcode("out.mp4", hwaccel: :qsv) # Intel Quick Sync
225
+
226
+ # AV1 — requires FFmpeg 8.0+
227
+ movie.transcode("out.mp4", video_codec: "libaom-av1", hwaccel: :nvenc) # NVIDIA
228
+ movie.transcode("out.mp4", video_codec: "libaom-av1", hwaccel: :vaapi) # VAAPI
229
+ movie.transcode("out.mp4", video_codec: "libaom-av1", hwaccel: :vulkan) # Vulkan compute
230
+
231
+ # D3D12 — Windows only, requires FFmpeg 8.1+
232
+ movie.transcode("out.mp4", hwaccel: :d3d12)
117
233
  ```
118
234
 
235
+ All accelerators gracefully fall back to software encoding if the hardware encoder is not available.
236
+
119
237
  ### Using Filters
120
238
 
121
239
  FFmpegCore supports raw FFmpeg filter strings for both video (`video_filter` or `-vf`) and audio (`audio_filter` or `-af`).
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module FFmpegCore
6
+ # Execute FFmpeg operations with multiple input files
7
+ #
8
+ # @example Overlay (picture-in-picture)
9
+ # FFmpegCore::Compositor.new(
10
+ # ["background.mp4", "overlay.mp4"],
11
+ # "output.mp4",
12
+ # filter_complex: "[0:v][1:v]overlay=10:10[v]",
13
+ # maps: ["[v]", "0:a"]
14
+ # ).run
15
+ #
16
+ # @example Concatenate
17
+ # FFmpegCore::Compositor.new(
18
+ # ["part1.mp4", "part2.mp4"],
19
+ # "output.mp4",
20
+ # filter_complex: "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]",
21
+ # maps: ["[v]", "[a]"]
22
+ # ).run
23
+ class Compositor
24
+ attr_reader :input_paths, :output_path, :options
25
+
26
+ # @param input_paths [Array<String>] Paths or URLs to input files (at least one required)
27
+ # @param output_path [String] Path to output file
28
+ # @param options [Hash] Compositing options
29
+ # @option options [String] :video_codec Video codec (e.g., "libx264")
30
+ # @option options [String] :audio_codec Audio codec (e.g., "aac")
31
+ # @option options [String, Integer] :video_bitrate Video bitrate (e.g., "1000k" or 1000)
32
+ # @option options [String, Integer] :audio_bitrate Audio bitrate (e.g., "128k" or 128)
33
+ # @option options [Array<String>, String] :filter_complex FFmpeg filter graph referencing input streams by index
34
+ # (e.g., "[0:v][1:v]overlay=0:0[v]"). Array elements are joined with semicolons.
35
+ # @option options [Array<String>, String] :maps Stream maps selecting outputs from the filter graph
36
+ # (e.g., ["[v]", "0:a"])
37
+ # @option options [Float] :duration Total duration in seconds, used for progress reporting
38
+ # @option options [Array<String>] :custom Raw FFmpeg flags appended verbatim (e.g., ["-shortest"])
39
+ # @raise [ArgumentError] if input_paths is empty
40
+ def initialize(input_paths, output_path, options = {})
41
+ raise ArgumentError, "At least one input path is required" if Array(input_paths).empty?
42
+
43
+ @input_paths = Array(input_paths).map(&:to_s)
44
+ @output_path = output_path.to_s
45
+ @options = options
46
+ end
47
+
48
+ # Run the FFmpeg compositing command
49
+ #
50
+ # @yield [Float] Progress ratio from 0.0 to 1.0 (requires :duration option)
51
+ # @return [String] Path to the output file
52
+ # @raise [InvalidInputError] if a local input file does not exist
53
+ # @raise [TranscodingError] if FFmpeg exits with a non-zero status
54
+ def run(&block)
55
+ validate_inputs!
56
+ ensure_output_directory!
57
+
58
+ command = build_command
59
+ execute_command(command, &block)
60
+ end
61
+
62
+ private
63
+
64
+ def validate_inputs!
65
+ input_paths.each do |path|
66
+ next if %r{^(https?|rtmp|rtsp)://}.match?(path)
67
+
68
+ raise InvalidInputError, "Input file does not exist: #{path}" unless File.exist?(path)
69
+ end
70
+ end
71
+
72
+ def ensure_output_directory!
73
+ output_dir = File.dirname(output_path)
74
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
75
+ end
76
+
77
+ def build_command
78
+ cmd = [FFmpegCore.configuration.ffmpeg_binary]
79
+
80
+ input_paths.each { |path| cmd += ["-i", path] }
81
+
82
+ cmd += ["-c:v", options[:video_codec]] if options[:video_codec]
83
+ cmd += ["-c:a", options[:audio_codec]] if options[:audio_codec]
84
+ cmd += ["-b:v", normalize_bitrate(options[:video_bitrate])] if options[:video_bitrate]
85
+ cmd += ["-b:a", normalize_bitrate(options[:audio_bitrate])] if options[:audio_bitrate]
86
+
87
+ filter = options[:filter_complex] || options[:filter_graph]
88
+ if filter
89
+ cmd += ["-filter_complex", filter.is_a?(Array) ? filter.join(";") : filter]
90
+ end
91
+
92
+ maps = options[:maps] || options[:map]
93
+ Array(maps).each { |map| cmd += ["-map", map] } if maps
94
+
95
+ cmd += options[:custom] if options[:custom]
96
+ cmd += ["-y", output_path]
97
+ cmd
98
+ end
99
+
100
+ def normalize_bitrate(bitrate)
101
+ return bitrate.to_s if bitrate.to_s.match?(/\d+[kKmM]/)
102
+
103
+ "#{bitrate}k"
104
+ end
105
+
106
+ def execute_command(command, &block)
107
+ error_log = []
108
+
109
+ Open3.popen3(*command) do |_stdin, _stdout, stderr, wait_thr|
110
+ stderr.each_line("\r") do |line|
111
+ clean_line = line.strip
112
+ next if clean_line.empty?
113
+
114
+ error_log << clean_line
115
+ error_log.shift if error_log.size > 20
116
+ parse_progress(line, &block) if block
117
+ end
118
+
119
+ status = wait_thr.value
120
+ unless status.success?
121
+ raise TranscodingError.new(
122
+ "FFmpeg compositing failed",
123
+ command: command.join(" "),
124
+ exit_status: status.exitstatus,
125
+ stdout: "",
126
+ stderr: error_log.join("\n")
127
+ )
128
+ end
129
+ end
130
+
131
+ output_path
132
+ end
133
+
134
+ def parse_progress(line)
135
+ return unless line =~ /time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/
136
+
137
+ current_time = $1.to_i * 3600 + $2.to_i * 60 + $3.to_f
138
+ duration = options[:duration]
139
+ yield([current_time / duration.to_f, 1.0].min) if duration && duration.to_f > 0
140
+ end
141
+ end
142
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FFmpegCore
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/ffmpeg_core.rb CHANGED
@@ -26,4 +26,5 @@ require_relative "ffmpeg_core/transcoder"
26
26
  require_relative "ffmpeg_core/screenshot"
27
27
  require_relative "ffmpeg_core/clipper"
28
28
  require_relative "ffmpeg_core/audio_extractor"
29
+ require_relative "ffmpeg_core/compositor"
29
30
  require_relative "ffmpeg_core/movie"
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev
@@ -39,6 +39,7 @@ files:
39
39
  - lib/ffmpeg_core.rb
40
40
  - lib/ffmpeg_core/audio_extractor.rb
41
41
  - lib/ffmpeg_core/clipper.rb
42
+ - lib/ffmpeg_core/compositor.rb
42
43
  - lib/ffmpeg_core/configuration.rb
43
44
  - lib/ffmpeg_core/errors.rb
44
45
  - lib/ffmpeg_core/movie.rb