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 +7 -0
- data/README.md +336 -0
- data/Rakefile +12 -0
- data/lib/ffmpeg/active_storage/analyzer.rb +79 -0
- data/lib/ffmpeg/active_storage/previewer.rb +67 -0
- data/lib/ffmpeg/command.rb +218 -0
- data/lib/ffmpeg/configuration.rb +113 -0
- data/lib/ffmpeg/errors.rb +81 -0
- data/lib/ffmpeg/keyframe_extractor.rb +213 -0
- data/lib/ffmpeg/media.rb +385 -0
- data/lib/ffmpeg/railtie.rb +31 -0
- data/lib/ffmpeg/scene_detector.rb +170 -0
- data/lib/ffmpeg/stream.rb +229 -0
- data/lib/ffmpeg/tasks/ffmpeg.rake +108 -0
- data/lib/ffmpeg/transcoder.rb +139 -0
- data/lib/ffmpeg/version.rb +11 -0
- data/lib/ffmpeg.rb +96 -0
- data/ruby-ffmpeg.gemspec +49 -0
- metadata +123 -0
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
|
+
[](https://badge.fury.io/rb/ruby-ffmpeg)
|
|
6
|
+
[](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,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
|