ffmpeg_core 0.1.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +27 -1
- data/README.md +74 -22
- data/lib/ffmpeg_core/movie.rb +6 -2
- data/lib/ffmpeg_core/probe.rb +44 -2
- data/lib/ffmpeg_core/screenshot.rb +2 -6
- data/lib/ffmpeg_core/transcoder.rb +73 -33
- data/lib/ffmpeg_core/version.rb +2 -2
- data/sig/ffmpeg_core.rbs +1 -1
- metadata +1 -2
- data/ffmpeg_core-0.1.0.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 026e0c17db47148e554c7b08f77a3e21a7b78b6f7d65915d3bf5159d057aae08
|
|
4
|
+
data.tar.gz: d0e49b402f4dff9aa73944f3a6e1543260d970c1114b4faccf9d3c51bb3da670
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d6eee4ad41d752b13ce1aab69be77ca24a5368b60db93b468feb12c2340d7f26dec285d09924503237a176b6bed299d4d9b1801e375fb6c5953f5c030e8f957
|
|
7
|
+
data.tar.gz: 987108002867b7d622aeb57b2606e59edae50a4762e76bb7287f52f97de836b740d2a65258df1e7f6baf8f1596e288d3bdad1b666e3b1b01974046212cfea3bf
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,33 @@ 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
|
-
## [
|
|
8
|
+
## [0.3.0] - 2026-01-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Remote Input Support:** `Movie.new` now accepts HTTP, HTTPS, RTMP, and RTSP URLs.
|
|
13
|
+
- **Crop Support:** Added `crop` option to `transcode` (e.g., `crop: { width: 100, height: 100, x: 10, y: 10 }`).
|
|
14
|
+
- **Advanced Metadata:** Added `video_profile` and `video_level` to `Probe`.
|
|
15
|
+
- Rotation detection from `side_data_list` for better compatibility with newer video formats.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Rotation Geometry:** `Probe#width` and `Probe#height` now correctly swap values if the video is rotated 90 or 270 degrees.
|
|
20
|
+
|
|
21
|
+
## [0.2.0] - 2026-01-15
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Real-time progress reporting in `Movie#transcode` via block yielding (0.0 to 1.0)
|
|
26
|
+
- Support for video and audio filters (`video_filter`, `audio_filter`)
|
|
27
|
+
- Support for quality settings (`preset`, `crf`)
|
|
28
|
+
- Enhanced metadata in `Probe`: added `rotation`, `aspect_ratio`, and `audio_streams`
|
|
29
|
+
- Comprehensive modular test suite for `Transcoder` and `Screenshot`
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `Transcoder` now uses `Open3.popen3` for non-blocking execution and progress parsing
|
|
34
|
+
- Improved RSpec testing style (using `have_received` spies)
|
|
9
35
|
|
|
10
36
|
## [0.1.1] - 2026-01-14
|
|
11
37
|
|
data/README.md
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Modern Ruby wrapper for FFmpeg with clean API and proper error handling.
|
|
4
4
|
|
|
5
|
+
[](https://badge.fury.io/rb/ffmpeg_core)
|
|
6
|
+
[](https://github.com/alec-c4/ffmpeg_core/actions)
|
|
7
|
+
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
10
|
- Modern Ruby 3+ conventions
|
|
8
11
|
- Zero runtime dependencies
|
|
12
|
+
- **Real-time progress reporting**
|
|
13
|
+
- **Support for video/audio filters and quality presets**
|
|
14
|
+
- **Remote input support (HTTP/HTTPS/RTMP/RTSP)**
|
|
9
15
|
- Proper error handling with detailed context
|
|
10
16
|
- Thread-safe configuration
|
|
11
17
|
- Simple, intuitive API
|
|
@@ -36,17 +42,26 @@ bundle install
|
|
|
36
42
|
```ruby
|
|
37
43
|
require "ffmpeg_core"
|
|
38
44
|
|
|
39
|
-
# Load a video file
|
|
45
|
+
# Load a video file or remote URL
|
|
40
46
|
movie = FFmpegCore::Movie.new("input.mp4")
|
|
47
|
+
# movie = FFmpegCore::Movie.new("http://example.com/video.mp4")
|
|
41
48
|
|
|
42
49
|
# Get metadata
|
|
43
50
|
movie.duration # => 120.5 (seconds)
|
|
44
|
-
movie.resolution # => "1920x1080"
|
|
51
|
+
movie.resolution # => "1920x1080" (automatically swapped if rotated)
|
|
52
|
+
movie.width # => 1920
|
|
53
|
+
movie.height # => 1080
|
|
45
54
|
movie.video_codec # => "h264"
|
|
46
55
|
movie.audio_codec # => "aac"
|
|
47
56
|
movie.frame_rate # => 29.97
|
|
48
57
|
movie.bitrate # => 5000 (kb/s)
|
|
49
58
|
movie.valid? # => true
|
|
59
|
+
|
|
60
|
+
# Access detailed metadata via probe
|
|
61
|
+
movie.probe.rotation # => 90 (degrees)
|
|
62
|
+
movie.probe.aspect_ratio # => "16:9"
|
|
63
|
+
movie.probe.video_profile # => "High"
|
|
64
|
+
movie.probe.video_level # => 41
|
|
50
65
|
```
|
|
51
66
|
|
|
52
67
|
### Transcoding
|
|
@@ -54,23 +69,58 @@ movie.valid? # => true
|
|
|
54
69
|
```ruby
|
|
55
70
|
movie = FFmpegCore::Movie.new("input.mp4")
|
|
56
71
|
|
|
57
|
-
# Basic transcoding
|
|
58
|
-
movie.transcode("output.mp4", video_codec: "libx264")
|
|
72
|
+
# Basic transcoding with progress
|
|
73
|
+
movie.transcode("output.mp4", video_codec: "libx264") do |progress|
|
|
74
|
+
puts "Progress: #{(progress * 100).round(2)}%"
|
|
75
|
+
end
|
|
59
76
|
|
|
60
|
-
#
|
|
77
|
+
# Advanced options (Filters & Quality)
|
|
61
78
|
movie.transcode("output.mp4", {
|
|
62
79
|
video_codec: "libx264",
|
|
63
80
|
audio_codec: "aac",
|
|
64
81
|
video_bitrate: "1000k",
|
|
65
82
|
audio_bitrate: "128k",
|
|
66
83
|
resolution: "1280x720",
|
|
67
|
-
|
|
84
|
+
crop: { width: 500, height: 500, x: 10, y: 10 }, # Crop video
|
|
85
|
+
video_filter: "scale=1280:-1,transpose=1", # Resize and rotate
|
|
86
|
+
audio_filter: "volume=0.5", # Reduce volume
|
|
87
|
+
preset: "slow", # ffmpeg preset (ultrafast, fast, medium, slow, etc.)
|
|
88
|
+
crf: 23, # Constant Rate Factor (0-51)
|
|
89
|
+
custom: %w[-map 0:v -map 0:a] # Custom FFmpeg flags
|
|
68
90
|
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Using Filters
|
|
69
94
|
|
|
70
|
-
|
|
95
|
+
FFmpegCore supports raw FFmpeg filter strings for both video (`video_filter` or `-vf`) and audio (`audio_filter` or `-af`).
|
|
96
|
+
|
|
97
|
+
**Common Video Filters:**
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
71
100
|
movie.transcode("output.mp4", {
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
# Scale to width 1280, keep aspect ratio
|
|
102
|
+
video_filter: "scale=1280:-1",
|
|
103
|
+
|
|
104
|
+
# Crop 100x100 starting at position (10,10)
|
|
105
|
+
video_filter: "crop=100:100:10:10",
|
|
106
|
+
|
|
107
|
+
# Rotate 90 degrees clockwise
|
|
108
|
+
video_filter: "transpose=1",
|
|
109
|
+
|
|
110
|
+
# Chain multiple filters (Scale then Rotate)
|
|
111
|
+
video_filter: "scale=1280:-1,transpose=1"
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Common Audio Filters:**
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
movie.transcode("output.mp4", {
|
|
119
|
+
# Increase volume by 50%
|
|
120
|
+
audio_filter: "volume=1.5",
|
|
121
|
+
|
|
122
|
+
# Fade in first 5 seconds
|
|
123
|
+
audio_filter: "afade=t=in:ss=0:d=5"
|
|
74
124
|
})
|
|
75
125
|
```
|
|
76
126
|
|
|
@@ -82,7 +132,7 @@ movie = FFmpegCore::Movie.new("input.mp4")
|
|
|
82
132
|
# Extract screenshot at specific time
|
|
83
133
|
movie.screenshot("thumbnail.jpg", seek_time: 5)
|
|
84
134
|
|
|
85
|
-
# With resolution
|
|
135
|
+
# With resolution and quality
|
|
86
136
|
movie.screenshot("thumbnail.jpg", {
|
|
87
137
|
seek_time: 10,
|
|
88
138
|
resolution: "640x360",
|
|
@@ -102,7 +152,7 @@ end
|
|
|
102
152
|
|
|
103
153
|
## Error Handling
|
|
104
154
|
|
|
105
|
-
FFmpegCore provides specific error classes for different failure scenarios
|
|
155
|
+
FFmpegCore provides specific error classes for different failure scenarios. All execution errors (transcoding, probing, screenshots) inherit from `FFmpegCore::ExecutionError`, which provides access to the command, exit status, and stderr output.
|
|
106
156
|
|
|
107
157
|
```ruby
|
|
108
158
|
begin
|
|
@@ -111,9 +161,9 @@ begin
|
|
|
111
161
|
rescue FFmpegCore::InvalidInputError => e
|
|
112
162
|
# File doesn't exist or is not readable
|
|
113
163
|
puts "Input error: #{e.message}"
|
|
114
|
-
rescue FFmpegCore::
|
|
115
|
-
#
|
|
116
|
-
puts "
|
|
164
|
+
rescue FFmpegCore::ExecutionError => e
|
|
165
|
+
# Covers TranscodingError, ProbeError, and ScreenshotError
|
|
166
|
+
puts "Execution failed: #{e.message}"
|
|
117
167
|
puts "Command: #{e.command}"
|
|
118
168
|
puts "Exit status: #{e.exit_status}"
|
|
119
169
|
puts "Stderr: #{e.stderr}"
|
|
@@ -125,14 +175,16 @@ end
|
|
|
125
175
|
|
|
126
176
|
### Error Classes
|
|
127
177
|
|
|
128
|
-
| Error
|
|
129
|
-
|
|
130
|
-
| `FFmpegCore::Error`
|
|
131
|
-
| `FFmpegCore::BinaryNotFoundError` | FFmpeg/FFprobe not found |
|
|
132
|
-
| `FFmpegCore::InvalidInputError`
|
|
133
|
-
| `FFmpegCore::
|
|
134
|
-
| `FFmpegCore::
|
|
135
|
-
| `FFmpegCore::
|
|
178
|
+
| Error | Description | Parent |
|
|
179
|
+
| --------------------------------- | -------------------------------------- | ------ |
|
|
180
|
+
| `FFmpegCore::Error` | Base error class | StandardError |
|
|
181
|
+
| `FFmpegCore::BinaryNotFoundError` | FFmpeg/FFprobe not found | Error |
|
|
182
|
+
| `FFmpegCore::InvalidInputError` | Input file doesn't exist or unreadable | Error |
|
|
183
|
+
| `FFmpegCore::OutputError` | Output file cannot be written | Error |
|
|
184
|
+
| `FFmpegCore::ExecutionError` | Base for command execution errors | Error |
|
|
185
|
+
| `FFmpegCore::ProbeError` | Failed to extract metadata | ExecutionError |
|
|
186
|
+
| `FFmpegCore::TranscodingError` | FFmpeg transcoding failed | ExecutionError |
|
|
187
|
+
| `FFmpegCore::ScreenshotError` | Screenshot extraction failed | ExecutionError |
|
|
136
188
|
|
|
137
189
|
## Development
|
|
138
190
|
|
data/lib/ffmpeg_core/movie.rb
CHANGED
|
@@ -27,10 +27,14 @@ module FFmpegCore
|
|
|
27
27
|
# @option options [String] :resolution Resolution (e.g., "1280x720")
|
|
28
28
|
# @option options [Integer, Float] :frame_rate Frame rate (e.g., 30)
|
|
29
29
|
# @option options [Array<String>] :custom Custom FFmpeg flags
|
|
30
|
+
# @yield [Float] Progress ratio (0.0 to 1.0)
|
|
30
31
|
# @return [String] Path to transcoded file
|
|
31
|
-
def transcode(output_path, options = {})
|
|
32
|
+
def transcode(output_path, options = {}, &block)
|
|
33
|
+
# Inject duration for progress calculation if known
|
|
34
|
+
options[:duration] ||= duration
|
|
35
|
+
|
|
32
36
|
transcoder = Transcoder.new(path, output_path, options)
|
|
33
|
-
transcoder.run
|
|
37
|
+
transcoder.run(&block)
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
# Extract screenshot from video
|
data/lib/ffmpeg_core/probe.rb
CHANGED
|
@@ -39,16 +39,32 @@ module FFmpegCore
|
|
|
39
39
|
video_stream&.dig("codec_name")
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def video_profile
|
|
43
|
+
video_stream&.dig("profile")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def video_level
|
|
47
|
+
video_stream&.dig("level")
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
def audio_codec
|
|
43
51
|
audio_stream&.dig("codec_name")
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
def width
|
|
47
|
-
video_stream&.dig("width")
|
|
55
|
+
val = video_stream&.dig("width")
|
|
56
|
+
return val unless val
|
|
57
|
+
return val if (rotation || 0) % 180 == 0
|
|
58
|
+
|
|
59
|
+
video_stream&.dig("height")
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
def height
|
|
51
|
-
video_stream&.dig("height")
|
|
63
|
+
val = video_stream&.dig("height")
|
|
64
|
+
return val unless val
|
|
65
|
+
return val if (rotation || 0) % 180 == 0
|
|
66
|
+
|
|
67
|
+
video_stream&.dig("width")
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
def frame_rate
|
|
@@ -68,9 +84,33 @@ module FFmpegCore
|
|
|
68
84
|
|
|
69
85
|
def resolution
|
|
70
86
|
return nil unless width && height
|
|
87
|
+
|
|
71
88
|
"#{width}x#{height}"
|
|
72
89
|
end
|
|
73
90
|
|
|
91
|
+
def rotation
|
|
92
|
+
return nil unless video_stream
|
|
93
|
+
|
|
94
|
+
# Try to find rotation in tags (common in MP4/MOV)
|
|
95
|
+
tags = video_stream.fetch("tags", {})
|
|
96
|
+
return tags["rotate"].to_i if tags["rotate"]
|
|
97
|
+
|
|
98
|
+
# Try side_data_list (common in some newer formats)
|
|
99
|
+
side_data = video_stream.fetch("side_data_list", []).find { |sd| sd.key?("rotation") }
|
|
100
|
+
return side_data["rotation"].to_i if side_data
|
|
101
|
+
|
|
102
|
+
# Default to 0 if not found
|
|
103
|
+
0
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def aspect_ratio
|
|
107
|
+
video_stream&.dig("display_aspect_ratio")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def audio_streams
|
|
111
|
+
streams.select { |s| s["codec_type"] == "audio" }
|
|
112
|
+
end
|
|
113
|
+
|
|
74
114
|
def valid?
|
|
75
115
|
!video_stream.nil?
|
|
76
116
|
end
|
|
@@ -82,6 +122,8 @@ module FFmpegCore
|
|
|
82
122
|
end
|
|
83
123
|
|
|
84
124
|
def validate_file!
|
|
125
|
+
return if path =~ %r{^(https?|rtmp|rtsp)://}
|
|
126
|
+
|
|
85
127
|
raise InvalidInputError, "File does not exist: #{path}" unless File.exist?(path)
|
|
86
128
|
raise InvalidInputError, "File is not readable: #{path}" unless File.readable?(path)
|
|
87
129
|
end
|
|
@@ -36,9 +36,7 @@ module FFmpegCore
|
|
|
36
36
|
cmd = [FFmpegCore.configuration.ffmpeg_binary]
|
|
37
37
|
|
|
38
38
|
# Seek to timestamp (before input for faster processing)
|
|
39
|
-
if options[:seek_time]
|
|
40
|
-
cmd += ["-ss", options[:seek_time].to_s]
|
|
41
|
-
end
|
|
39
|
+
cmd += ["-ss", options[:seek_time].to_s] if options[:seek_time]
|
|
42
40
|
|
|
43
41
|
# Input file
|
|
44
42
|
cmd += ["-i", input_path]
|
|
@@ -47,9 +45,7 @@ module FFmpegCore
|
|
|
47
45
|
cmd += ["-vframes", "1"]
|
|
48
46
|
|
|
49
47
|
# Resolution
|
|
50
|
-
if options[:resolution]
|
|
51
|
-
cmd += ["-s", options[:resolution]]
|
|
52
|
-
end
|
|
48
|
+
cmd += ["-s", options[:resolution]] if options[:resolution]
|
|
53
49
|
|
|
54
50
|
# Quality (2-31, lower is better, default: 2)
|
|
55
51
|
quality = options[:quality] || 2
|
|
@@ -14,17 +14,19 @@ module FFmpegCore
|
|
|
14
14
|
@options = options
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def run
|
|
17
|
+
def run(&block)
|
|
18
18
|
validate_input!
|
|
19
19
|
ensure_output_directory!
|
|
20
20
|
|
|
21
21
|
command = build_command
|
|
22
|
-
execute_command(command)
|
|
22
|
+
execute_command(command, &block)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
def validate_input!
|
|
28
|
+
return if input_path =~ %r{^(https?|rtmp|rtsp)://}
|
|
29
|
+
|
|
28
30
|
raise InvalidInputError, "Input file does not exist: #{input_path}" unless File.exist?(input_path)
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -40,39 +42,45 @@ module FFmpegCore
|
|
|
40
42
|
cmd += ["-i", input_path]
|
|
41
43
|
|
|
42
44
|
# Video codec
|
|
43
|
-
if options[:video_codec]
|
|
44
|
-
cmd += ["-c:v", options[:video_codec]]
|
|
45
|
-
end
|
|
45
|
+
cmd += ["-c:v", options[:video_codec]] if options[:video_codec]
|
|
46
46
|
|
|
47
47
|
# Audio codec
|
|
48
|
-
if options[:audio_codec]
|
|
49
|
-
cmd += ["-c:a", options[:audio_codec]]
|
|
50
|
-
end
|
|
48
|
+
cmd += ["-c:a", options[:audio_codec]] if options[:audio_codec]
|
|
51
49
|
|
|
52
50
|
# Video bitrate
|
|
53
|
-
if options[:video_bitrate]
|
|
54
|
-
cmd += ["-b:v", normalize_bitrate(options[:video_bitrate])]
|
|
55
|
-
end
|
|
51
|
+
cmd += ["-b:v", normalize_bitrate(options[:video_bitrate])] if options[:video_bitrate]
|
|
56
52
|
|
|
57
53
|
# Audio bitrate
|
|
58
|
-
if options[:audio_bitrate]
|
|
59
|
-
cmd += ["-b:a", normalize_bitrate(options[:audio_bitrate])]
|
|
60
|
-
end
|
|
54
|
+
cmd += ["-b:a", normalize_bitrate(options[:audio_bitrate])] if options[:audio_bitrate]
|
|
61
55
|
|
|
62
56
|
# Resolution
|
|
63
|
-
if options[:resolution]
|
|
64
|
-
cmd += ["-s", options[:resolution]]
|
|
65
|
-
end
|
|
57
|
+
cmd += ["-s", options[:resolution]] if options[:resolution]
|
|
66
58
|
|
|
67
59
|
# Frame rate
|
|
68
|
-
if options[:frame_rate]
|
|
69
|
-
|
|
60
|
+
cmd += ["-r", options[:frame_rate].to_s] if options[:frame_rate]
|
|
61
|
+
|
|
62
|
+
# Video filters
|
|
63
|
+
video_filters = []
|
|
64
|
+
video_filters << options[:video_filter] if options[:video_filter]
|
|
65
|
+
|
|
66
|
+
if options[:crop]
|
|
67
|
+
crop = options[:crop]
|
|
68
|
+
video_filters << "crop=#{crop[:width]}:#{crop[:height]}:#{crop[:x]}:#{crop[:y]}"
|
|
70
69
|
end
|
|
70
|
+
|
|
71
|
+
cmd += ["-vf", video_filters.join(",")] unless video_filters.empty?
|
|
72
|
+
|
|
73
|
+
# Audio filter
|
|
74
|
+
cmd += ["-af", options[:audio_filter]] if options[:audio_filter]
|
|
75
|
+
|
|
76
|
+
# Quality preset
|
|
77
|
+
cmd += ["-preset", options[:preset]] if options[:preset]
|
|
78
|
+
|
|
79
|
+
# Constant Rate Factor (CRF)
|
|
80
|
+
cmd += ["-crf", options[:crf].to_s] if options[:crf]
|
|
71
81
|
|
|
72
82
|
# Custom options (array of strings)
|
|
73
|
-
if options[:custom]
|
|
74
|
-
cmd += options[:custom]
|
|
75
|
-
end
|
|
83
|
+
cmd += options[:custom] if options[:custom]
|
|
76
84
|
|
|
77
85
|
# Overwrite output file
|
|
78
86
|
cmd += ["-y"]
|
|
@@ -89,23 +97,55 @@ module FFmpegCore
|
|
|
89
97
|
# 1000 -> "1000k"
|
|
90
98
|
# "1M" -> "1M"
|
|
91
99
|
return bitrate.to_s if bitrate.to_s.match?(/\d+[kKmM]/)
|
|
100
|
+
|
|
92
101
|
"#{bitrate}k"
|
|
93
102
|
end
|
|
94
103
|
|
|
95
|
-
def execute_command(command)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
def execute_command(command, &block)
|
|
105
|
+
# Keep track of last few lines for error reporting
|
|
106
|
+
error_log = []
|
|
107
|
+
|
|
108
|
+
Open3.popen3(*command) do |_stdin, _stdout, stderr, wait_thr|
|
|
109
|
+
# FFmpeg uses \r to update progress line, so we split by \r
|
|
110
|
+
# We also handle \n just in case
|
|
111
|
+
stderr.each_line("\r") do |line|
|
|
112
|
+
clean_line = line.strip
|
|
113
|
+
next if clean_line.empty?
|
|
114
|
+
|
|
115
|
+
error_log << clean_line
|
|
116
|
+
error_log.shift if error_log.size > 20 # Keep last 20 lines
|
|
117
|
+
|
|
118
|
+
parse_progress(clean_line, &block) if block_given?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
status = wait_thr.value
|
|
122
|
+
|
|
123
|
+
unless status.success?
|
|
124
|
+
raise TranscodingError.new(
|
|
125
|
+
"FFmpeg transcoding failed",
|
|
126
|
+
command: command.join(" "),
|
|
127
|
+
exit_status: status.exitstatus,
|
|
128
|
+
stdout: "",
|
|
129
|
+
stderr: error_log.join("\n")
|
|
130
|
+
)
|
|
131
|
+
end
|
|
106
132
|
end
|
|
107
133
|
|
|
108
134
|
output_path
|
|
109
135
|
end
|
|
136
|
+
|
|
137
|
+
def parse_progress(line)
|
|
138
|
+
# Match time=HH:MM:SS.ms
|
|
139
|
+
if line =~ /time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/
|
|
140
|
+
hours, minutes, seconds = $1.to_i, $2.to_i, $3.to_f
|
|
141
|
+
current_time = (hours * 3600) + (minutes * 60) + seconds
|
|
142
|
+
|
|
143
|
+
duration = options[:duration]
|
|
144
|
+
if duration && duration.to_f > 0
|
|
145
|
+
progress = [current_time / duration.to_f, 1.0].min
|
|
146
|
+
yield(progress)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
110
150
|
end
|
|
111
151
|
end
|
data/lib/ffmpeg_core/version.rb
CHANGED
data/sig/ffmpeg_core.rbs
CHANGED
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
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexey Poimtsev
|
|
@@ -21,7 +21,6 @@ files:
|
|
|
21
21
|
- LICENSE.txt
|
|
22
22
|
- README.md
|
|
23
23
|
- Rakefile
|
|
24
|
-
- ffmpeg_core-0.1.0.gem
|
|
25
24
|
- lib/ffmpeg_core.rb
|
|
26
25
|
- lib/ffmpeg_core/configuration.rb
|
|
27
26
|
- lib/ffmpeg_core/errors.rb
|
data/ffmpeg_core-0.1.0.gem
DELETED
|
Binary file
|