ffmpeg_core 0.4.0 → 0.5.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: afead7a6fce7f95748207153b521b35a223f2efd7add35664e12ba3e27f86b23
4
- data.tar.gz: 3052072fb4a3541518cc5d12bf333b58b77a77f2b3b4deec44d51b89ddbac678
3
+ metadata.gz: bb9323f5aae7b7f162d4b43f219f14506af5fa012e033451a2021e92bec14128
4
+ data.tar.gz: 7164b154e6b542e2179f430418cd3b42fb68cc48a5dff62a679b13eb135b3de7
5
5
  SHA512:
6
- metadata.gz: d725d25a1d211186ede000ac81d58f20c57fc8f46ba5a3897b006a8e92e128b37f5f9cfb9295633bfe0a7dd878a0f99793329c388c1c2a920bc585256b4740db
7
- data.tar.gz: 5e683ec8fb39dc1e669abaeaa8e3f7254de8f92ef4593ed6869ba7a5a4c02dab2d6ee8005243dbf31e24be27b11a689c89de736c0eca69e93be38b7d9e5fc531
6
+ metadata.gz: f97a26864b497d77ae8b862daa0b45154f038cbc94dfbf56703a4879ccbac680b294acdacf35d20538fb736d046dbd85c55514bc6d7e7a8c0d6c41030878f40d
7
+ data.tar.gz: 5235e82f757aa787cb76dbed3af50a5bd4eff074c9476918f90d9d8c8187f658979a6ca0b773d762569f631394326c27c45d45d265f1a0e13f9eb2890a1bba26
data/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ 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.5.0] - 2026-04-13
9
+
10
+ ### Added
11
+
12
+ - **Probe — extended metadata:**
13
+ - `subtitle_streams` — array of subtitle streams
14
+ - `chapters` — file chapters (requires ffprobe `-show_chapters`)
15
+ - `format_name` — container format name (mp4, mkv, avi…)
16
+ - `tags` — file-level tags (title, artist, etc.)
17
+ - `audio_sample_rate` — sample rate of the first audio stream
18
+ - `audio_channels` — channel count
19
+ - `audio_channel_layout` — channel layout string (stereo, 5.1…)
20
+ - `pixel_format` — video pixel format (yuv420p, etc.)
21
+ - `has_video?` / `has_audio?` — stream presence predicates
22
+ - `exif` — EXIF tags merged from format and video stream tags (FFmpeg 8.1)
23
+ - **Movie — new operations:**
24
+ - `cut(output, start_time:, duration:)` / `cut(output, start_time:, end_time:)` — lossless trim via `-c copy`
25
+ - `extract_audio(output, codec:)` — extract audio track to file
26
+ - `screenshots(output_dir, count:)` — extract multiple screenshots at equal intervals
27
+ - **Hardware acceleration — AV1 and D3D12 support (FFmpeg 8.0/8.1):**
28
+ - AV1 via `:nvenc` (`av1_nvenc`), `:vaapi` (`av1_vaapi`), `:vulkan` (`av1_vulkan`)
29
+ - D3D12 via `:d3d12` (`h264_d3d12va`) for Windows
30
+
31
+ ### Changed
32
+
33
+ - `Probe#probe!` now passes `-show_chapters` to ffprobe
34
+
35
+ ## [0.4.1] - 2026-04-09
36
+
37
+ ### Fixed
38
+
39
+ - Binary detection on Windows: `where` fallback now correctly resolves ffmpeg/ffprobe when not on PATH
40
+
41
+ ### Changed
42
+
43
+ - Simplified binary detection: removed redundant Ruby PATH scan, extracted lookup steps into focused private methods
44
+ - Expanded `Configuration` spec: ENV override, `BinaryNotFoundError`, known-path fallback, `reset_configuration!`
45
+
8
46
  ## [0.4.0] - 2026-01-26
9
47
 
10
48
  ### Added
@@ -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
@@ -40,23 +40,54 @@ module FFmpegCore
40
40
  end
41
41
 
42
42
  def detect_binary(name)
43
- # Check common locations
44
- paths = ENV["PATH"].split(File::PATH_SEPARATOR)
45
- paths.each do |path|
46
- binary = File.join(path, name)
47
- return binary if File.executable?(binary)
48
- end
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
49
54
 
50
- # Homebrew locations (macOS)
51
- homebrew_paths = [
52
- "/opt/homebrew/bin/#{name}", # Apple Silicon
53
- "/usr/local/bin/#{name}" # Intel
54
- ]
55
- homebrew_paths.each do |path|
56
- return path if File.executable?(path)
57
- end
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
60
+
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?
58
66
 
59
- raise BinaryNotFoundError, "#{name} binary not found. Please install FFmpeg: brew install ffmpeg"
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
60
91
  end
61
92
  end
62
93
 
@@ -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.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/ffmpeg_core.rb CHANGED
@@ -24,4 +24,6 @@ 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"
27
29
  require_relative "ffmpeg_core/movie"
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.0
4
+ version: 0.5.0
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:
@@ -23,6 +37,8 @@ files:
23
37
  - Rakefile
24
38
  - lefthook.yml
25
39
  - lib/ffmpeg_core.rb
40
+ - lib/ffmpeg_core/audio_extractor.rb
41
+ - lib/ffmpeg_core/clipper.rb
26
42
  - lib/ffmpeg_core/configuration.rb
27
43
  - lib/ffmpeg_core/errors.rb
28
44
  - lib/ffmpeg_core/movie.rb
@@ -53,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
69
  - !ruby/object:Gem::Version
54
70
  version: '0'
55
71
  requirements: []
56
- rubygems_version: 4.0.4
72
+ rubygems_version: 4.0.10
57
73
  specification_version: 4
58
74
  summary: Modern Ruby wrapper for FFmpeg
59
75
  test_files: []