Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
== 7.0.0-beta.14 2025-04-25

Improvements:
* Built-in presets now use the closest standard frame rate to the input file
when the input file has lower frame rate than the preset.
* Increased the keyframe interval of the MPEG-DASH H.264 preset to 2 seconds by default.
This results in a more efficient encoding process and smaller file sizes, while
still maintaining a good quality.

Fixes:
* Fixed some audio timeline shifting issues with the MPEG-DASH H.264 preset due to which
some players would skip to the end of the video upon loading.

== 7.0.0-beta.13 2025-04-23

Fixes:
Expand Down
7 changes: 6 additions & 1 deletion lib/ffmpeg/command_args.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ module FFMPEG
# end
# args.to_s # "-map 0:v:0 -c:v:0 libx264 -r 30"
class CommandArgs < RawCommandArgs
STANDARD_FRAME_RATES = [12, 24, 25, 30, 50, 60, 90, 120, 240].freeze

class << self
# Composes a new instance of CommandArgs with the given media.
# The block is evaluated in the context of the new instance.
Expand Down Expand Up @@ -96,7 +98,10 @@ def audio_bit_rate(target_value, **kwargs)
# @param target_value [Integer, Float] The target frame rate.
# @return [Numeric]
def adjusted_frame_rate(target_value)
[media.frame_rate, target_value].compact.min
return target_value if media.frame_rate.nil?
return target_value if media.frame_rate > target_value

STANDARD_FRAME_RATES.min_by { (_1 - media.frame_rate).abs }
end

# Returns the minimum of the current video bit rate and the target value.
Expand Down
17 changes: 10 additions & 7 deletions lib/ffmpeg/presets/dash/h264.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def h264_360p(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
keyframe_interval: 1,
keyframe_interval: 2,
audio_bit_rate: '128k',
frame_rate: 30,
ld_frame_rate: 24,
Expand Down Expand Up @@ -48,7 +48,7 @@ def h264_480p(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
keyframe_interval: 1,
keyframe_interval: 2,
audio_bit_rate: '128k',
frame_rate: 30,
ld_frame_rate: 24,
Expand Down Expand Up @@ -79,7 +79,7 @@ def h264_720p(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
keyframe_interval: 1,
keyframe_interval: 2,
audio_bit_rate: '128k',
ld_frame_rate: 24,
sd_frame_rate: 30,
Expand Down Expand Up @@ -112,7 +112,7 @@ def h264_1080p(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
keyframe_interval: 1,
keyframe_interval: 2,
audio_bit_rate: '128k',
ld_frame_rate: 24,
sd_frame_rate: 30,
Expand Down Expand Up @@ -146,7 +146,7 @@ def h264_1440p(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
keyframe_interval: 1,
keyframe_interval: 2,
audio_bit_rate: '128k',
ld_frame_rate: 24,
sd_frame_rate: 30,
Expand Down Expand Up @@ -181,7 +181,7 @@ def h264_4k(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
keyframe_interval: 1,
keyframe_interval: 2,
audio_bit_rate: '128k',
ld_frame_rate: 24,
sd_frame_rate: 30,
Expand Down Expand Up @@ -231,7 +231,7 @@ def initialize(
metadata: nil,
threads: FFMPEG.threads,
segment_duration: 4,
keyframe_interval: 1,
keyframe_interval: 2,
h264_presets: [Presets.h264_1080p, Presets.h264_720p, Presets.h264_480p, Presets.h264_360p],
ld_h264_presets: [Presets.h264_240p, Presets.h264_144p],
&
Expand Down Expand Up @@ -292,11 +292,14 @@ def initialize(
constant_rate_factor h264_preset.constant_rate_factor, stream_type: 'v', stream_index: index
min_keyframe_interval preset.keyframe_interval * frame_rate, stream_index: index
max_keyframe_interval preset.keyframe_interval * frame_rate, stream_index: index
force_keyframes "expr:gte(t,n_forced*#{preset.keyframe_interval})", stream_index: index
end
end
end

map media.audio_mapping_id do
# Reset the audio stream's timestamps to start from 0.
filter Filter.new(:audio, 'asetpts', expr: 'PTS-STARTPTS')
audio_bit_rate h264_presets.first.audio_bit_rate
end
end
Expand Down
18 changes: 15 additions & 3 deletions lib/ffmpeg/raw_command_args.rb
Original file line number Diff line number Diff line change
Expand Up @@ -580,8 +580,22 @@ def aspect_ratio(value, **kwargs)
stream_arg('aspect', value, stream_type: 'v', **kwargs)
end

# Sets a force keyframe interval in the command arguments.
#
# @param value [String, Numeric] The force keyframe interval to set.
# @param kwargs [Hash] The stream specific arguments to use (see stream_arg).
# @return [self]
#
# @example
# args = FFMPEG::RawCommandArgs.compose do
# force_keyframes 'expr:gte(t,n_forced*2)'
# end
# args.to_s # "-force_key_frames expr:gte(t,n_forced*2)"
def force_keyframes(value, **kwargs)
stream_arg('force_key_frames', value, **kwargs)
end

# Sets a minimum keyframe interval in the command arguments.
# This is used for adaptive streaming.
#
# @param value [String, Numeric] The minimum keyframe interval to set.
# @param kwargs [Hash] The stream specific arguments to use (see stream_arg).
Expand All @@ -598,7 +612,6 @@ def min_keyframe_interval(value, **kwargs)

# Sets a maximum keyframe interval in the command arguments.
#
# This is used for adaptive streaming.
# @param value [String, Numeric] The maximum keyframe interval to set.
# @param kwargs [Hash] The stream specific arguments to use (see stream_arg).
# @return [self]
Expand All @@ -613,7 +626,6 @@ def max_keyframe_interval(value, **kwargs)
end

# Sets a scene change threshold in the command arguments.
# This is used for adaptive streaming.
#
# @param value [String, Numeric] The scene change threshold to set.
# @param kwargs [Hash] The stream specific arguments to use (see stream_arg).
Expand Down
2 changes: 1 addition & 1 deletion lib/ffmpeg/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module FFMPEG
VERSION = '7.0.0-beta.13'
VERSION = '7.0.0-beta.14'
end
32 changes: 24 additions & 8 deletions spec/ffmpeg/command_args_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@
module FFMPEG
describe CommandArgs do
describe '#frame_rate' do
context 'when the media frame rate is lower than the target value' do
it 'sets the frame rate to the media frame rate' do
context 'when the target value is nil' do
it 'does not set the frame rate' do
media = instance_double(Media, frame_rate: 30)
args = CommandArgs.compose(media) { frame_rate nil }
expect(args.to_a).to eq(%w[])
end
end

context 'when the media frame rate is nil' do
it 'sets the frame rate to the target value' do
media = instance_double(Media, frame_rate: nil)
args = CommandArgs.compose(media) { frame_rate 60 }
expect(args.to_a).to eq(%w[-r 30])
expect(args.to_a).to eq(%w[-r 60])
end
end

Expand All @@ -21,11 +29,19 @@ module FFMPEG
end
end

context 'when the target value is nil' do
it 'does not set the frame rate' do
media = instance_double(Media, frame_rate: 30)
args = CommandArgs.compose(media) { frame_rate nil }
expect(args.to_a).to eq(%w[])
context 'when the media frame rate is lower than the target value' do
it 'sets the frame rate to the closest standard value' do
{
0 => 12,
21 => 24,
26 => 25,
29.94 => 30,
480 => 240
}.each do |media_frame_rate, expected_value|
media = instance_double(Media, frame_rate: media_frame_rate)
args = CommandArgs.compose(media) { frame_rate 1000 }
expect(args.to_a).to eq(%W[-r #{expected_value}])
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions spec/ffmpeg/raw_command_args_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ module FFMPEG
frame_rate: 'r',
pixel_format: 'pix_fmt',
resolution: 's',
force_keyframes: 'force_key_frames',
min_keyframe_interval: 'keyint_min',
max_keyframe_interval: 'g',
scene_change_threshold: 'sc_threshold',
Expand Down
Loading