From 383a6ca3fcce097a23f861d2a4a9acbede91e1f1 Mon Sep 17 00:00:00 2001 From: bajankristof Date: Thu, 24 Apr 2025 19:15:33 +0200 Subject: [PATCH 1/4] feat: use standard frame rates in built-in presets --- lib/ffmpeg/command_args.rb | 7 ++++++- spec/ffmpeg/command_args_spec.rb | 32 ++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/ffmpeg/command_args.rb b/lib/ffmpeg/command_args.rb index f9e4403..2a9dd24 100644 --- a/lib/ffmpeg/command_args.rb +++ b/lib/ffmpeg/command_args.rb @@ -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. @@ -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. diff --git a/spec/ffmpeg/command_args_spec.rb b/spec/ffmpeg/command_args_spec.rb index fd00444..9922d79 100644 --- a/spec/ffmpeg/command_args_spec.rb +++ b/spec/ffmpeg/command_args_spec.rb @@ -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 @@ -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 From 4b277b7ce2a7c86244ead7c7120129012a042260 Mon Sep 17 00:00:00 2001 From: bajankristof Date: Thu, 24 Apr 2025 19:17:02 +0200 Subject: [PATCH 2/4] fix: resolve audio timeline issues in MPEG-DASH H.264 presets --- lib/ffmpeg/presets/dash/h264.rb | 3 +++ lib/ffmpeg/raw_command_args.rb | 18 +++++++++++++++--- spec/ffmpeg/raw_command_args_spec.rb | 1 + 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/ffmpeg/presets/dash/h264.rb b/lib/ffmpeg/presets/dash/h264.rb index 19873c5..4283c12 100644 --- a/lib/ffmpeg/presets/dash/h264.rb +++ b/lib/ffmpeg/presets/dash/h264.rb @@ -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 diff --git a/lib/ffmpeg/raw_command_args.rb b/lib/ffmpeg/raw_command_args.rb index 6bd1544..17ddd81 100644 --- a/lib/ffmpeg/raw_command_args.rb +++ b/lib/ffmpeg/raw_command_args.rb @@ -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). @@ -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] @@ -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). diff --git a/spec/ffmpeg/raw_command_args_spec.rb b/spec/ffmpeg/raw_command_args_spec.rb index 182004a..37aff1d 100644 --- a/spec/ffmpeg/raw_command_args_spec.rb +++ b/spec/ffmpeg/raw_command_args_spec.rb @@ -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', From 98b5215b7b26ef1ea468dd81ec84e1f9f819959f Mon Sep 17 00:00:00 2001 From: bajankristof Date: Thu, 24 Apr 2025 19:18:08 +0200 Subject: [PATCH 3/4] chore: reduce keyframe interval in MPEG-DASH H.264 presets * There is no perceivable difference between a keyframe interval of 1 and 2 in terms of playback, but the file size is significantly smaller with a keyframe interval of 2 (as one would expect). --- lib/ffmpeg/presets/dash/h264.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/ffmpeg/presets/dash/h264.rb b/lib/ffmpeg/presets/dash/h264.rb index 4283c12..e333978 100644 --- a/lib/ffmpeg/presets/dash/h264.rb +++ b/lib/ffmpeg/presets/dash/h264.rb @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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], & From 4bf487390a082fc1618ac00124dd62c73806fe89 Mon Sep 17 00:00:00 2001 From: bajankristof Date: Thu, 24 Apr 2025 20:32:09 +0200 Subject: [PATCH 4/4] chore: update version to 7.0.0-beta.14 and document changes --- CHANGELOG | 13 +++++++++++++ lib/ffmpeg/version.rb | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8a54179..9897497 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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: diff --git a/lib/ffmpeg/version.rb b/lib/ffmpeg/version.rb index 474234e..a2de057 100644 --- a/lib/ffmpeg/version.rb +++ b/lib/ffmpeg/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FFMPEG - VERSION = '7.0.0-beta.13' + VERSION = '7.0.0-beta.14' end