ruby-ffmpeg 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0c5608dc7cc071bd2df362e31ed56e9a71994ac7ad0ffcd1e3f5cc160d2cb39c
4
+ data.tar.gz: 29adefaaca271fde91c4c2c58fb49c3c7b234b31a25667862d7f78475e5471cb
5
+ SHA512:
6
+ metadata.gz: a9461b5c17c61c53feb3619b60fa8d17e76851afa2f22e2f8b752d8db4e2a703fc8173ae825414e5bd0a5b63b7d62e658894075f3a67fb6cb708e188e3ff9c57
7
+ data.tar.gz: 0d6b09652b3b8d5f62b112092377ece5ebce22a7c50104053dcb802eade131faec9b062028f3bb7027e6f186624de99192aae7ea0fd1a7dc4f26470e21f35c89
data/README.md ADDED
@@ -0,0 +1,336 @@
1
+ # ruby-ffmpeg
2
+
3
+ A modern Ruby wrapper for FFmpeg with zero dependencies. Provides clean APIs for metadata extraction, transcoding, scene detection, and keyframe extraction.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/ruby-ffmpeg.svg)](https://badge.fury.io/rb/ruby-ffmpeg)
6
+ [![Ruby](https://github.com/activeagents/ruby-ffmpeg/actions/workflows/main.yml/badge.svg)](https://github.com/activeagents/ruby-ffmpeg/actions/workflows/main.yml)
7
+
8
+ ## Features
9
+
10
+ - **Zero Dependencies** - Pure Ruby, no gem dependencies
11
+ - **Modern Ruby** - Requires Ruby 3.1+, uses Data classes and modern patterns
12
+ - **FFmpeg 4-7 Support** - Tested against FFmpeg versions 4, 5, 6, and 7
13
+ - **Scene Detection** - Detect scene changes for video analysis
14
+ - **Keyframe Extraction** - Extract frames at intervals, timestamps, or I-frames
15
+ - **Progress Reporting** - Real-time transcoding progress callbacks
16
+ - **Rails Integration** - Active Storage analyzer and previewer included
17
+
18
+ ## Installation
19
+
20
+ Add to your Gemfile:
21
+
22
+ ```ruby
23
+ gem 'ruby-ffmpeg'
24
+ ```
25
+
26
+ Or install directly:
27
+
28
+ ```bash
29
+ gem install ruby-ffmpeg
30
+ ```
31
+
32
+ **Note:** FFmpeg must be installed on your system. Install with:
33
+ - macOS: `brew install ffmpeg`
34
+ - Ubuntu: `sudo apt install ffmpeg`
35
+ - Windows: `choco install ffmpeg`
36
+
37
+ ## Quick Start
38
+
39
+ ```ruby
40
+ require 'ffmpeg'
41
+
42
+ # Load a video
43
+ media = FFMPEG::Media.new("/path/to/video.mp4")
44
+
45
+ # Get metadata
46
+ media.duration # => 120.5
47
+ media.resolution # => "1920x1080"
48
+ media.video_codec # => "h264"
49
+ media.audio_codec # => "aac"
50
+ media.frame_rate # => 29.97
51
+ media.bit_rate # => 5000000
52
+
53
+ # Check properties
54
+ media.valid? # => true
55
+ media.video? # => true
56
+ media.audio? # => true
57
+ media.hd? # => true
58
+ media.portrait? # => false
59
+ ```
60
+
61
+ ## Transcoding
62
+
63
+ ```ruby
64
+ # Basic transcoding
65
+ media.transcode("/output.mp4")
66
+
67
+ # With options
68
+ media.transcode("/output.webm",
69
+ video_codec: "libvpx-vp9",
70
+ audio_codec: "libopus",
71
+ resolution: "1280x720",
72
+ video_bitrate: "2M"
73
+ )
74
+
75
+ # With progress callback
76
+ media.transcode("/output.mp4") do |progress|
77
+ puts "Progress: #{progress.round(1)}%"
78
+ end
79
+
80
+ # Copy streams (no re-encoding)
81
+ media.transcode("/output.mp4",
82
+ copy_video: true,
83
+ copy_audio: true
84
+ )
85
+
86
+ # Trim video
87
+ media.transcode("/clip.mp4",
88
+ seek: 30, # Start at 30 seconds
89
+ duration: 10 # 10 second clip
90
+ )
91
+ ```
92
+
93
+ ## Scene Detection
94
+
95
+ Detect scene changes for video analysis, chapter generation, or highlight extraction:
96
+
97
+ ```ruby
98
+ # Detect scenes with default threshold
99
+ scenes = media.detect_scenes
100
+ # => [
101
+ # { timestamp: 0.0, score: 1.0 },
102
+ # { timestamp: 5.23, score: 0.45 },
103
+ # { timestamp: 12.8, score: 0.38 }
104
+ # ]
105
+
106
+ # Custom threshold (lower = more sensitive)
107
+ scenes = media.detect_scenes(threshold: 0.2)
108
+
109
+ # Using SceneDetector directly for more options
110
+ detector = FFMPEG::SceneDetector.new(media)
111
+ scenes = detector.detect(
112
+ threshold: 0.3,
113
+ min_scene_length: 2.0, # Minimum 2 seconds between scenes
114
+ max_scenes: 20 # Maximum 20 scenes
115
+ )
116
+
117
+ # Detect scenes and extract keyframes
118
+ scenes = detector.detect_with_keyframes(
119
+ threshold: 0.3,
120
+ output_dir: "/tmp/scenes"
121
+ )
122
+ # Each scene now has :keyframe_path
123
+ ```
124
+
125
+ ## Keyframe Extraction
126
+
127
+ Extract frames for thumbnails, video analysis, or sprite sheets:
128
+
129
+ ```ruby
130
+ # Extract at regular intervals
131
+ frames = media.extract_keyframes(
132
+ output_dir: "/tmp/frames",
133
+ interval: 5.0 # Every 5 seconds
134
+ )
135
+ # => ["/tmp/frames/frame_0000.jpg", "/tmp/frames/frame_0005.jpg", ...]
136
+
137
+ # Extract specific number of frames
138
+ frames = media.extract_keyframes(
139
+ output_dir: "/tmp/frames",
140
+ count: 10 # 10 evenly-distributed frames
141
+ )
142
+
143
+ # Extract at specific timestamps
144
+ frames = media.extract_keyframes(
145
+ output_dir: "/tmp/frames",
146
+ timestamps: [0, 30, 60, 90, 120]
147
+ )
148
+
149
+ # Using KeyframeExtractor directly for more options
150
+ extractor = FFMPEG::KeyframeExtractor.new(media)
151
+
152
+ # Extract actual I-frames (keyframes) from video
153
+ iframes = extractor.extract_iframes(
154
+ output_dir: "/tmp/iframes",
155
+ max_frames: 50
156
+ )
157
+
158
+ # Create a thumbnail sprite sheet (for video scrubbing)
159
+ sprite = extractor.create_sprite(
160
+ columns: 10,
161
+ rows: 10,
162
+ width: 160,
163
+ output_path: "/tmp/sprite.jpg"
164
+ )
165
+ # => { path: "/tmp/sprite.jpg", columns: 10, rows: 10, ... }
166
+ ```
167
+
168
+ ## Stream Information
169
+
170
+ Access detailed stream information:
171
+
172
+ ```ruby
173
+ # Video stream
174
+ video = media.video
175
+ video.codec # => "h264"
176
+ video.width # => 1920
177
+ video.height # => 1080
178
+ video.frame_rate # => 29.97
179
+ video.bit_rate # => 4500000
180
+ video.pixel_format # => "yuv420p"
181
+ video.rotation # => 0
182
+
183
+ # Audio stream
184
+ audio = media.audio
185
+ audio.codec # => "aac"
186
+ audio.sample_rate # => 48000
187
+ audio.channels # => 2
188
+ audio.channel_layout # => "stereo"
189
+
190
+ # All streams
191
+ media.streams.each do |stream|
192
+ puts stream.to_s
193
+ end
194
+ ```
195
+
196
+ ## Configuration
197
+
198
+ ```ruby
199
+ FFMPEG.configure do |config|
200
+ # Custom binary paths
201
+ config.ffmpeg_binary = "/usr/local/bin/ffmpeg"
202
+ config.ffprobe_binary = "/usr/local/bin/ffprobe"
203
+
204
+ # Default timeout (seconds)
205
+ config.timeout = 600
206
+
207
+ # Enable logging
208
+ config.logger = Logger.new(STDOUT)
209
+
210
+ # Default codecs
211
+ config.default_video_codec = "libx264"
212
+ config.default_audio_codec = "aac"
213
+
214
+ # Number of threads (0 = auto)
215
+ config.threads = 0
216
+
217
+ # Temporary directory
218
+ config.temp_dir = "/tmp/ffmpeg"
219
+
220
+ # Overwrite existing files
221
+ config.overwrite_output = true
222
+ end
223
+ ```
224
+
225
+ ## Rails Integration
226
+
227
+ ### Active Storage Analyzer
228
+
229
+ Automatically extract video metadata for Active Storage:
230
+
231
+ ```ruby
232
+ # config/initializers/active_storage.rb
233
+ Rails.application.config.active_storage.analyzers.prepend(
234
+ FFMPEG::ActiveStorage::Analyzer
235
+ )
236
+ ```
237
+
238
+ Now video blobs will have metadata like:
239
+ ```ruby
240
+ video.metadata
241
+ # => { width: 1920, height: 1080, duration: 120.5, video_codec: "h264", ... }
242
+ ```
243
+
244
+ ### Active Storage Previewer
245
+
246
+ Generate video previews:
247
+
248
+ ```ruby
249
+ # config/initializers/active_storage.rb
250
+ Rails.application.config.active_storage.previewers.prepend(
251
+ FFMPEG::ActiveStorage::Previewer
252
+ )
253
+ ```
254
+
255
+ ### Rake Tasks
256
+
257
+ ```bash
258
+ # Check FFmpeg installation
259
+ rake ffmpeg:check
260
+
261
+ # Analyze a video
262
+ rake ffmpeg:analyze[/path/to/video.mp4]
263
+
264
+ # Extract keyframes
265
+ rake ffmpeg:keyframes[/path/to/video.mp4,output_dir,5]
266
+
267
+ # Detect scenes
268
+ rake ffmpeg:scenes[/path/to/video.mp4,0.3]
269
+ ```
270
+
271
+ ## Error Handling
272
+
273
+ ```ruby
274
+ begin
275
+ media = FFMPEG::Media.new("/path/to/video.mp4")
276
+ media.transcode("/output.mp4")
277
+ rescue FFMPEG::MediaNotFound => e
278
+ puts "File not found: #{e.message}"
279
+ rescue FFMPEG::InvalidMedia => e
280
+ puts "Invalid media: #{e.message}"
281
+ rescue FFMPEG::TranscodingError => e
282
+ puts "Transcoding failed: #{e.message}"
283
+ puts "Command: #{e.command}"
284
+ puts "Output: #{e.output}"
285
+ rescue FFMPEG::CommandTimeout => e
286
+ puts "Command timed out: #{e.message}"
287
+ end
288
+ ```
289
+
290
+ ## Comparison with streamio-ffmpeg
291
+
292
+ | Feature | ruby-ffmpeg | streamio-ffmpeg |
293
+ |---------|-------------|-----------------|
294
+ | Ruby Version | 3.1+ | 2.0+ |
295
+ | FFmpeg Version | 4-7 | 2.8.4 |
296
+ | Dependencies | None | None |
297
+ | Scene Detection | Built-in | No |
298
+ | Keyframe Extraction | Built-in | No |
299
+ | Active Storage | Built-in | No |
300
+ | Progress Callbacks | Yes | Yes |
301
+ | Last Updated | 2026 | 2016 |
302
+
303
+ ## Development
304
+
305
+ ```bash
306
+ # Clone the repo
307
+ git clone https://github.com/activeagents/ruby-ffmpeg.git
308
+ cd ruby-ffmpeg
309
+
310
+ # Install dependencies
311
+ bin/setup
312
+
313
+ # Run tests
314
+ rake test
315
+
316
+ # Run linter
317
+ rake rubocop
318
+ ```
319
+
320
+ ## Contributing
321
+
322
+ 1. Fork it
323
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
324
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
325
+ 4. Push to the branch (`git push origin feature/my-new-feature`)
326
+ 5. Create a new Pull Request
327
+
328
+ ## License
329
+
330
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
331
+
332
+ ## Credits
333
+
334
+ Developed and maintained by [Active Agents](https://activeagents.ai).
335
+
336
+ Inspired by [streamio-ffmpeg](https://github.com/streamio/streamio-ffmpeg) and [instructure/ruby-ffmpeg](https://github.com/instructure/ruby-ffmpeg).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFMPEG
4
+ module ActiveStorage
5
+ # Active Storage Analyzer for video files using FFMPEG
6
+ #
7
+ # This analyzer extracts video metadata that Active Storage can use
8
+ # for displaying video information and generating previews.
9
+ #
10
+ # @example Add to Active Storage analyzers
11
+ # # config/initializers/active_storage.rb
12
+ # Rails.application.config.active_storage.analyzers.prepend(
13
+ # FFMPEG::ActiveStorage::Analyzer
14
+ # )
15
+ #
16
+ class Analyzer < ::ActiveStorage::Analyzer
17
+ class << self
18
+ # Check if this analyzer can process the blob
19
+ # @param blob [ActiveStorage::Blob]
20
+ # @return [Boolean]
21
+ def accept?(blob)
22
+ blob.video?
23
+ end
24
+ end
25
+
26
+ # Analyze the video and return metadata
27
+ # @return [Hash] video metadata
28
+ def metadata
29
+ download_blob_to_tempfile do |file|
30
+ media = FFMPEG::Media.new(file.path)
31
+
32
+ {
33
+ width: media.width,
34
+ height: media.height,
35
+ duration: media.duration,
36
+ angle: media.rotation,
37
+ display_aspect_ratio: calculate_display_aspect_ratio(media),
38
+ audio: media.audio?,
39
+ video: media.video?,
40
+ video_codec: media.video_codec,
41
+ audio_codec: media.audio_codec,
42
+ frame_rate: media.frame_rate,
43
+ bit_rate: media.bit_rate
44
+ }.compact
45
+ end
46
+ rescue FFMPEG::Error => e
47
+ Rails.logger.error "[FFMPEG] Analysis failed: #{e.message}" if defined?(Rails)
48
+ {}
49
+ end
50
+
51
+ private
52
+
53
+ def calculate_display_aspect_ratio(media)
54
+ return nil unless media.width && media.height && media.height.positive?
55
+
56
+ ratio = media.width.to_f / media.height
57
+
58
+ # Common aspect ratios
59
+ common_ratios = {
60
+ [16, 9] => 1.778,
61
+ [4, 3] => 1.333,
62
+ [21, 9] => 2.333,
63
+ [1, 1] => 1.0,
64
+ [9, 16] => 0.5625,
65
+ [3, 4] => 0.75
66
+ }
67
+
68
+ # Find closest common ratio
69
+ closest = common_ratios.min_by { |_, r| (r - ratio).abs }
70
+
71
+ if (closest[1] - ratio).abs < 0.05
72
+ closest[0].join(":")
73
+ else
74
+ "#{media.width}:#{media.height}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFMPEG
4
+ module ActiveStorage
5
+ # Active Storage Previewer for video files using FFMPEG
6
+ #
7
+ # Generates video preview images (thumbnails) for Active Storage.
8
+ #
9
+ # @example Add to Active Storage previewers
10
+ # # config/initializers/active_storage.rb
11
+ # Rails.application.config.active_storage.previewers.prepend(
12
+ # FFMPEG::ActiveStorage::Previewer
13
+ # )
14
+ #
15
+ class Previewer < ::ActiveStorage::Previewer
16
+ class << self
17
+ # Check if this previewer can process the blob
18
+ # @param blob [ActiveStorage::Blob]
19
+ # @return [Boolean]
20
+ def accept?(blob)
21
+ blob.video?
22
+ end
23
+ end
24
+
25
+ # Generate a preview image
26
+ # @yield [io, options] the generated preview image
27
+ def preview(**options)
28
+ download_blob_to_tempfile do |input|
29
+ media = FFMPEG::Media.new(input.path)
30
+
31
+ # Extract frame at 10% into the video (avoid black frames at start)
32
+ timestamp = (media.duration || 10) * 0.1
33
+
34
+ draw_frame(media, timestamp) do |output|
35
+ yield io: output, filename: "#{blob.filename.base}.jpg",
36
+ content_type: "image/jpeg"
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def draw_frame(media, timestamp)
44
+ Dir.mktmpdir("ffmpeg_preview") do |dir|
45
+ output_path = File.join(dir, "preview.jpg")
46
+
47
+ extractor = FFMPEG::KeyframeExtractor.new(media)
48
+ extractor.extract_at_timestamps(
49
+ [timestamp],
50
+ output_dir: dir,
51
+ format: "jpg",
52
+ quality: 2
53
+ )
54
+
55
+ # Find the generated file
56
+ frame_path = Dir.glob(File.join(dir, "frame_*.jpg")).first
57
+
58
+ if frame_path && File.exist?(frame_path)
59
+ yield File.open(frame_path, "rb")
60
+ else
61
+ raise FFMPEG::KeyframeExtractionError, "Failed to generate preview"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end