ffmpeg_core 0.4.1 → 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: a3a1322c33ef390363223c0f3a307f269e84ed3484d2491e2d7340111d33d7a6
4
- data.tar.gz: d98fcf0cf9881e837783cda58d1d43784c87310db8e22468e5b0623c9a8d6716
3
+ metadata.gz: 83185cde1635bd7362c737cba6da5c723be5407cd7457ea5bbac2e0b43206365
4
+ data.tar.gz: bf4dea2ee0d08d1e8af7b8193cef697f2b8dc1fcda8f5d86efed89e1d23c8f4e
5
5
  SHA512:
6
- metadata.gz: 5e50f4fb86e61b7d7e858ed90a047f37fa92d7ec9c9216cf8238b7aaf7f48b7c78aa237c36a8c154e864976e8b65c0ac78a93697c1047342df113037fed6f035
7
- data.tar.gz: 621b5d98bd486b260a4c208c091f18113484d29df66cb005f8e005204dedd324fc5d737e22d662bbf2dbda788f8cdba002a5a313ba18abcc28d926c948c2cf11
6
+ metadata.gz: fbe0cf4fd2b09c72c143aeeb1281889f7291e816d7602c6d12852503f647fbc71100e106cf41c33c5e76b9728258d3ecc547b1407632748b6512e92086376265
7
+ data.tar.gz: 056ea62e71626ac7969cee7242fb14936b15aa9098311b631c36f14c87c61289f6da48a41e99e7745fb7032965a3550d9f1c59e56969b43bba6af33f4b25977f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ 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
+
21
+ ## [0.5.0] - 2026-04-13
22
+
23
+ ### Added
24
+
25
+ - **Probe — extended metadata:**
26
+ - `subtitle_streams` — array of subtitle streams
27
+ - `chapters` — file chapters (requires ffprobe `-show_chapters`)
28
+ - `format_name` — container format name (mp4, mkv, avi…)
29
+ - `tags` — file-level tags (title, artist, etc.)
30
+ - `audio_sample_rate` — sample rate of the first audio stream
31
+ - `audio_channels` — channel count
32
+ - `audio_channel_layout` — channel layout string (stereo, 5.1…)
33
+ - `pixel_format` — video pixel format (yuv420p, etc.)
34
+ - `has_video?` / `has_audio?` — stream presence predicates
35
+ - `exif` — EXIF tags merged from format and video stream tags (FFmpeg 8.1)
36
+ - **Movie — new operations:**
37
+ - `cut(output, start_time:, duration:)` / `cut(output, start_time:, end_time:)` — lossless trim via `-c copy`
38
+ - `extract_audio(output, codec:)` — extract audio track to file
39
+ - `screenshots(output_dir, count:)` — extract multiple screenshots at equal intervals
40
+ - **Hardware acceleration — AV1 and D3D12 support (FFmpeg 8.0/8.1):**
41
+ - AV1 via `:nvenc` (`av1_nvenc`), `:vaapi` (`av1_vaapi`), `:vulkan` (`av1_vulkan`)
42
+ - D3D12 via `:d3d12` (`h264_d3d12va`) for Windows
43
+
44
+ ### Changed
45
+
46
+ - `Probe#probe!` now passes `-show_chapters` to ffprobe
47
+
8
48
  ## [0.4.1] - 2026-04-09
9
49
 
10
50
  ### Fixed
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,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module FFmpegCore
6
+ # Extract audio track from video files
7
+ class AudioExtractor
8
+ attr_reader :input_path, :output_path, :options
9
+
10
+ def initialize(input_path, output_path, options = {})
11
+ @input_path = input_path.to_s
12
+ @output_path = output_path.to_s
13
+ @options = options
14
+ end
15
+
16
+ def run
17
+ validate_input!
18
+ ensure_output_directory!
19
+
20
+ command = build_command
21
+ execute_command(command)
22
+ end
23
+
24
+ private
25
+
26
+ def validate_input!
27
+ return if %r{^(https?|rtmp|rtsp)://}.match?(input_path)
28
+
29
+ raise InvalidInputError, "Input file does not exist: #{input_path}" unless File.exist?(input_path)
30
+ end
31
+
32
+ def ensure_output_directory!
33
+ output_dir = File.dirname(output_path)
34
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
35
+ end
36
+
37
+ def build_command
38
+ cmd = [FFmpegCore.configuration.ffmpeg_binary, "-i", input_path]
39
+ cmd += ["-vn"]
40
+ cmd += ["-c:a", options[:codec]] if options[:codec]
41
+ cmd += ["-y", output_path]
42
+ cmd
43
+ end
44
+
45
+ def execute_command(command)
46
+ _stdout, stderr, status = Open3.capture3(*command)
47
+
48
+ unless status.success?
49
+ raise TranscodingError.new(
50
+ "FFmpeg audio extraction failed",
51
+ command: command.join(" "),
52
+ exit_status: status.exitstatus,
53
+ stdout: "",
54
+ stderr: stderr
55
+ )
56
+ end
57
+
58
+ output_path
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module FFmpegCore
6
+ # Cut/trim video segments
7
+ class Clipper
8
+ attr_reader :input_path, :output_path, :options
9
+
10
+ def initialize(input_path, output_path, options = {})
11
+ @input_path = input_path.to_s
12
+ @output_path = output_path.to_s
13
+ @options = options
14
+ end
15
+
16
+ def run
17
+ validate_input!
18
+ ensure_output_directory!
19
+
20
+ command = build_command
21
+ execute_command(command)
22
+ end
23
+
24
+ private
25
+
26
+ def validate_input!
27
+ return if %r{^(https?|rtmp|rtsp)://}.match?(input_path)
28
+
29
+ raise InvalidInputError, "Input file does not exist: #{input_path}" unless File.exist?(input_path)
30
+ end
31
+
32
+ def ensure_output_directory!
33
+ output_dir = File.dirname(output_path)
34
+ FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
35
+ end
36
+
37
+ def build_command
38
+ cmd = [FFmpegCore.configuration.ffmpeg_binary]
39
+
40
+ cmd += ["-ss", options[:start_time].to_s] if options[:start_time]
41
+ cmd += ["-i", input_path]
42
+
43
+ if options[:duration]
44
+ cmd += ["-t", options[:duration].to_s]
45
+ elsif options[:end_time]
46
+ cmd += ["-to", options[:end_time].to_s]
47
+ end
48
+
49
+ cmd += ["-c", "copy", "-y", output_path]
50
+ cmd
51
+ end
52
+
53
+ def execute_command(command)
54
+ _stdout, stderr, status = Open3.capture3(*command)
55
+
56
+ unless status.success?
57
+ raise TranscodingError.new(
58
+ "FFmpeg cut failed",
59
+ command: command.join(" "),
60
+ exit_status: status.exitstatus,
61
+ stdout: "",
62
+ stderr: stderr
63
+ )
64
+ end
65
+
66
+ output_path
67
+ end
68
+ end
69
+ end
@@ -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
@@ -52,5 +52,47 @@ module FFmpegCore
52
52
  screenshotter = Screenshot.new(path, output_path, options)
53
53
  screenshotter.extract
54
54
  end
55
+
56
+ # Cut/trim a segment from video
57
+ #
58
+ # @param output_path [String] Path to output file
59
+ # @param options [Hash] Cut options
60
+ # @option options [Integer, Float] :start_time Start time in seconds
61
+ # @option options [Integer, Float] :duration Duration in seconds
62
+ # @option options [Integer, Float] :end_time End time in seconds (alternative to :duration)
63
+ # @return [String] Path to output file
64
+ def cut(output_path, options = {})
65
+ clipper = Clipper.new(path, output_path, options)
66
+ clipper.run
67
+ end
68
+
69
+ # Extract audio track from video
70
+ #
71
+ # @param output_path [String] Path to output audio file
72
+ # @param options [Hash] Extraction options
73
+ # @option options [String] :codec Audio codec (e.g., "libmp3lame", "aac")
74
+ # @return [String] Path to output file
75
+ def extract_audio(output_path, options = {})
76
+ extractor = AudioExtractor.new(path, output_path, options)
77
+ extractor.run
78
+ end
79
+
80
+ # Extract multiple screenshots at equal intervals
81
+ #
82
+ # @param output_dir [String] Directory to save screenshots
83
+ # @param count [Integer] Number of screenshots to extract (default: 5)
84
+ # @return [Array<String>] Paths to screenshot files
85
+ def screenshots(output_dir, count: 5)
86
+ FileUtils.mkdir_p(output_dir)
87
+ total = duration || 0
88
+ interval = total / (count + 1).to_f
89
+
90
+ (1..count).map do |i|
91
+ seek = (interval * i).round(2)
92
+ output_path = File.join(output_dir, format("screenshot_%03d.jpg", i))
93
+ Screenshot.new(path, output_path, seek_time: seek).extract
94
+ output_path
95
+ end
96
+ end
55
97
  end
56
98
  end
@@ -111,6 +111,53 @@ module FFmpegCore
111
111
  streams.select { |s| s["codec_type"] == "audio" }
112
112
  end
113
113
 
114
+ def subtitle_streams
115
+ streams.select { |s| s["codec_type"] == "subtitle" }
116
+ end
117
+
118
+ def chapters
119
+ @metadata.fetch("chapters", [])
120
+ end
121
+
122
+ def format_name
123
+ @metadata.dig("format", "format_name")
124
+ end
125
+
126
+ def tags
127
+ @metadata.dig("format", "tags") || {}
128
+ end
129
+
130
+ def audio_sample_rate
131
+ audio_stream&.dig("sample_rate")&.to_i
132
+ end
133
+
134
+ def audio_channels
135
+ audio_stream&.dig("channels")
136
+ end
137
+
138
+ def audio_channel_layout
139
+ audio_stream&.dig("channel_layout")
140
+ end
141
+
142
+ def pixel_format
143
+ video_stream&.dig("pix_fmt")
144
+ end
145
+
146
+ def has_video?
147
+ !video_stream.nil?
148
+ end
149
+
150
+ def has_audio?
151
+ !audio_stream.nil?
152
+ end
153
+
154
+ # EXIF metadata: merges format-level and video stream tags (FFmpeg 8.1+)
155
+ def exif
156
+ format_tags = @metadata.dig("format", "tags") || {}
157
+ stream_tags = video_stream&.dig("tags") || {}
158
+ format_tags.merge(stream_tags)
159
+ end
160
+
114
161
  def valid?
115
162
  !video_stream.nil?
116
163
  end
@@ -135,6 +182,7 @@ module FFmpegCore
135
182
  "-print_format", "json",
136
183
  "-show_format",
137
184
  "-show_streams",
185
+ "-show_chapters",
138
186
  path
139
187
  ]
140
188
 
@@ -189,25 +189,35 @@ module FFmpegCore
189
189
  :h264
190
190
  when /x265|hevc/i
191
191
  :hevc
192
+ when /av1|libaom/i
193
+ :av1
192
194
  end
193
195
  end
194
196
 
195
197
  HW_FLAGS = {
196
198
  vaapi: ["-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi"],
197
199
  qsv: ["-hwaccel", "qsv"],
198
- nvenc: ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"]
200
+ nvenc: ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"],
201
+ vulkan: ["-hwaccel", "vulkan"],
202
+ d3d12: ["-hwaccel", "d3d12va"]
199
203
  }.freeze
200
204
 
201
205
  HW_ENCODERS = {
202
206
  h264: {
203
207
  nvenc: "h264_nvenc",
204
208
  vaapi: "h264_vaapi",
205
- qsv: "h264_qsv"
209
+ qsv: "h264_qsv",
210
+ d3d12: "h264_d3d12va"
206
211
  },
207
212
  hevc: {
208
213
  nvenc: "hevc_nvenc",
209
214
  vaapi: "hevc_vaapi",
210
215
  qsv: "hevc_qsv"
216
+ },
217
+ av1: {
218
+ nvenc: "av1_nvenc",
219
+ vaapi: "av1_vaapi",
220
+ vulkan: "av1_vulkan"
211
221
  }
212
222
  }.freeze
213
223
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FFmpegCore
4
- VERSION = "0.4.1"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/ffmpeg_core.rb CHANGED
@@ -24,4 +24,7 @@ require_relative "ffmpeg_core/configuration"
24
24
  require_relative "ffmpeg_core/probe"
25
25
  require_relative "ffmpeg_core/transcoder"
26
26
  require_relative "ffmpeg_core/screenshot"
27
+ require_relative "ffmpeg_core/clipper"
28
+ require_relative "ffmpeg_core/audio_extractor"
29
+ require_relative "ffmpeg_core/compositor"
27
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.4.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev
@@ -37,6 +37,9 @@ files:
37
37
  - Rakefile
38
38
  - lefthook.yml
39
39
  - lib/ffmpeg_core.rb
40
+ - lib/ffmpeg_core/audio_extractor.rb
41
+ - lib/ffmpeg_core/clipper.rb
42
+ - lib/ffmpeg_core/compositor.rb
40
43
  - lib/ffmpeg_core/configuration.rb
41
44
  - lib/ffmpeg_core/errors.rb
42
45
  - lib/ffmpeg_core/movie.rb
@@ -67,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
70
  - !ruby/object:Gem::Version
68
71
  version: '0'
69
72
  requirements: []
70
- rubygems_version: 4.0.6
73
+ rubygems_version: 4.0.10
71
74
  specification_version: 4
72
75
  summary: Modern Ruby wrapper for FFmpeg
73
76
  test_files: []