From 6b59ede7fa216ec83cf8dea1ab9cb9bf05f6c45e Mon Sep 17 00:00:00 2001 From: bajankristof Date: Tue, 15 Apr 2025 09:45:58 +0200 Subject: [PATCH 1/6] feat: add color information helpers to media --- lib/ffmpeg/media.rb | 14 ++++++++++++++ spec/ffmpeg/media_spec.rb | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/ffmpeg/media.rb b/lib/ffmpeg/media.rb index 6478f5a..bf0b6c8 100644 --- a/lib/ffmpeg/media.rb +++ b/lib/ffmpeg/media.rb @@ -312,6 +312,20 @@ def local? default_video_stream&.color_space end + # Returns the color primaries of the default video stream (if any). + # + # @return [String, nil] + autoload def color_primaries + default_video_stream&.color_primaries + end + + # Returns the color transfer of the default video stream (if any). + # + # @return [String, nil] + autoload def color_transfer + default_video_stream&.color_transfer + end + # Returns the frame rate (avg_frame_rate) of the default video stream (if any). # # @return [Float, nil] diff --git a/spec/ffmpeg/media_spec.rb b/spec/ffmpeg/media_spec.rb index bfd75d8..065e8b7 100644 --- a/spec/ffmpeg/media_spec.rb +++ b/spec/ffmpeg/media_spec.rb @@ -446,9 +446,11 @@ module FFMPEG { calculated_pixel_aspect_ratio: Rational(1), - color_range: 'pc', pixel_format: 'yuvj420p', + color_range: 'pc', color_space: 'bt709', + color_primaries: 'bt709', + color_transfer: 'bt709', frame_rate: Rational(60 / 1), frames: 213, video_index: 0, From e090fe4dc6bab3b9f3b6d1f854b5883831da47ec Mon Sep 17 00:00:00 2001 From: bajankristof Date: Tue, 15 Apr 2025 09:47:33 +0200 Subject: [PATCH 2/6] fix: resolve certain aspect ratio issues in MPEG-DASH presets --- lib/ffmpeg/presets/dash/h264.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/ffmpeg/presets/dash/h264.rb b/lib/ffmpeg/presets/dash/h264.rb index 5b99ece..410e5d9 100644 --- a/lib/ffmpeg/presets/dash/h264.rb +++ b/lib/ffmpeg/presets/dash/h264.rb @@ -311,6 +311,9 @@ def initialize( # Force keyframes at the specified interval. force_key_frames "expr:gte(t,n_forced*#{preset.keyframe_interval})" + # Force aspect ratio to the calculated aspect ratio. + aspect media.calculated_aspect_ratio if media.calculated_aspect_ratio + # Map the scaled video streams with the desired H.264 parameters. h264_presets.each_with_index do |h264_preset, index| map "[v#{index}out]" do From a4a4223f9890e95c3854762c069e0d15fdd5e715 Mon Sep 17 00:00:00 2001 From: bajankristof Date: Tue, 15 Apr 2025 09:48:39 +0200 Subject: [PATCH 3/6] fix: rollback zlib scale support --- lib/ffmpeg/filters/scale.rb | 79 +++++++++++++------------------ lib/ffmpeg/presets/h264.rb | 21 +------- spec/ffmpeg/filters/scale_spec.rb | 3 -- 3 files changed, 34 insertions(+), 69 deletions(-) diff --git a/lib/ffmpeg/filters/scale.rb b/lib/ffmpeg/filters/scale.rb index 886d81b..5b42e5f 100644 --- a/lib/ffmpeg/filters/scale.rb +++ b/lib/ffmpeg/filters/scale.rb @@ -6,7 +6,6 @@ module FFMPEG module Filters # rubocop:disable Style/Documentation class << self def scale( - zlib: false, width: nil, height: nil, algorithm: nil, @@ -19,10 +18,11 @@ def scale( in_color_transfer: nil, out_color_transfer: nil, in_chroma_location: nil, - out_chroma_location: nil + out_chroma_location: nil, + force_original_aspect_ratio: nil, + force_divisible_by: nil ) Scale.new( - zlib:, width:, height:, algorithm:, @@ -35,12 +35,14 @@ def scale( in_color_transfer:, out_color_transfer:, in_chroma_location:, - out_chroma_location: + out_chroma_location:, + force_original_aspect_ratio:, + force_divisible_by: ) end end - # The Scale class uses the scale (or zscale) filter + # The Scale class uses the scale filter # to resize a multimedia stream. class Scale < Filter NEAREST_DIMENSION = -1 @@ -54,6 +56,7 @@ class << self # @param media [FFMPEG::Media] The media to fit. # @param max_width [Numeric] The maximum width to fit. # @param max_height [Numeric] The maximum height to fit. + # @param kwargs [Hash] Additional options for the scale filter. # @return [FFMPEG::Filters::Scale] The scale filter. def contained(media, max_width: nil, max_height: nil, **kwargs) unless media.is_a?(FFMPEG::Media) @@ -96,10 +99,10 @@ def contained(media, max_width: nil, max_height: nil, **kwargs) :in_color_range, :out_color_range, :in_color_primaries, :out_color_primaries, :in_color_transfer, :out_color_transfer, - :in_chroma_location, :out_chroma_location + :in_chroma_location, :out_chroma_location, + :force_original_aspect_ratio, :force_divisible_by def initialize( - zlib: false, width: nil, height: nil, algorithm: nil, @@ -112,7 +115,9 @@ def initialize( in_color_transfer: nil, out_color_transfer: nil, in_chroma_location: nil, - out_chroma_location: nil + out_chroma_location: nil, + force_original_aspect_ratio: nil, + force_divisible_by: nil ) if !width.nil? && !width.is_a?(Numeric) && !width.is_a?(String) raise ArgumentError, "Unknown width format #{width.class}, expected #{Numeric} or #{String}" @@ -135,50 +140,32 @@ def initialize( @out_color_transfer = out_color_transfer @in_chroma_location = in_chroma_location @out_chroma_location = out_chroma_location + @force_original_aspect_ratio = force_original_aspect_ratio + @force_divisible_by = force_divisible_by - super(:video, zlib ? 'zscale' : 'scale') - end - - def zlib? - @name.start_with?('z') + super(:video, 'scale') end protected def format_kwargs - if zlib? - super( - w: @width, - h: @height, - f: @algorithm, - min: @in_color_space, - m: @out_color_space, - rin: @in_color_range, - r: @out_color_range, - pin: @in_color_primaries, - p: @out_color_primaries, - tin: @in_color_transfer, - t: @out_color_transfer, - cin: @in_chroma_location, - c: @out_chroma_location - ) - else - super( - w: @width, - h: @height, - flags: @algorithm && [@algorithm], - in_color_matrix: @in_color_space, - out_color_matrix: @out_color_space, - in_range: @in_color_range, - out_range: @out_color_range, - in_primaries: @in_color_primaries, - out_primaries: @out_color_primaries, - in_transfer: @in_color_transfer, - out_transfer: @out_color_transfer, - in_chroma_loc: @in_chroma_location, - out_chroma_loc: @out_chroma_location - ) - end + super( + w: @width, + h: @height, + flags: @algorithm && [@algorithm], + in_color_matrix: @in_color_space, + out_color_matrix: @out_color_space, + in_range: @in_color_range, + out_range: @out_color_range, + in_primaries: @in_color_primaries, + out_primaries: @out_color_primaries, + in_transfer: @in_color_transfer, + out_transfer: @out_color_transfer, + in_chroma_loc: @in_chroma_location, + out_chroma_loc: @out_chroma_location, + force_original_aspect_ratio: @force_original_aspect_ratio, + force_divisible_by: @force_divisible_by + ) end end end diff --git a/lib/ffmpeg/presets/h264.rb b/lib/ffmpeg/presets/h264.rb index fb58ae4..e220009 100644 --- a/lib/ffmpeg/presets/h264.rb +++ b/lib/ffmpeg/presets/h264.rb @@ -19,7 +19,6 @@ def h264_144p( frame_rate: 30, constant_rate_factor: 28, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -35,7 +34,6 @@ def h264_144p( pixel_format:, max_width: 256, max_height: 144, - zlib:, & ) end @@ -51,7 +49,6 @@ def h264_240p( frame_rate: 30, constant_rate_factor: 28, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -67,7 +64,6 @@ def h264_240p( pixel_format:, max_width: 426, max_height: 240, - zlib:, & ) end @@ -83,7 +79,6 @@ def h264_360p( frame_rate: 30, constant_rate_factor: 28, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -99,7 +94,6 @@ def h264_360p( pixel_format:, max_width: 640, max_height: 360, - zlib:, & ) end @@ -115,7 +109,6 @@ def h264_480p( frame_rate: 30, constant_rate_factor: 27, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -131,7 +124,6 @@ def h264_480p( pixel_format:, max_width: 854, max_height: 480, - zlib:, & ) end @@ -147,7 +139,6 @@ def h264_720p( frame_rate: 60, constant_rate_factor: 27, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -163,7 +154,6 @@ def h264_720p( pixel_format:, max_width: 1280, max_height: 720, - zlib:, & ) end @@ -179,7 +169,6 @@ def h264_1080p( frame_rate: 60, constant_rate_factor: 27, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -195,7 +184,6 @@ def h264_1080p( pixel_format:, max_width: 1920, max_height: 1080, - zlib:, & ) end @@ -211,7 +199,6 @@ def h264_1440p( frame_rate: 60, constant_rate_factor: 26, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -227,7 +214,6 @@ def h264_1440p( pixel_format:, max_width: 2560, max_height: 1440, - zlib:, & ) end @@ -243,7 +229,6 @@ def h264_4k( frame_rate: 60, constant_rate_factor: 26, pixel_format: 'yuv420p', - zlib: true, & ) H264.new( @@ -259,7 +244,6 @@ def h264_4k( pixel_format:, max_width: 3840, max_height: 2160, - zlib:, & ) end @@ -282,7 +266,6 @@ class H264 < Preset # @param pixel_format [String] The pixel format to use. # @param max_width [Integer] The maximum width of the video. # @param max_height [Integer] The maximum height of the video. - # @param zlib [Boolean] Whether to use zlib for the scale filter. # @yield The block to execute to compose the command arguments. def initialize( name: nil, @@ -297,7 +280,6 @@ def initialize( pixel_format: 'yuv420p', max_width: nil, max_height: nil, - zlib: true, & ) if max_width && !max_width.is_a?(Numeric) @@ -317,7 +299,6 @@ def initialize( @pixel_format = pixel_format @max_width = max_width @max_height = max_height - @zlib = zlib preset = self super(name:, filename:, metadata:) do @@ -369,7 +350,7 @@ def format_filter def scale_filter(media) return unless @max_width || @max_height - Filters::Scale.contained(media, zlib: @zlib, max_width: @max_width, max_height: @max_height) + Filters::Scale.contained(media, max_width: @max_width, max_height: @max_height) end end end diff --git a/spec/ffmpeg/filters/scale_spec.rb b/spec/ffmpeg/filters/scale_spec.rb index 9698735..11ecb74 100644 --- a/spec/ffmpeg/filters/scale_spec.rb +++ b/spec/ffmpeg/filters/scale_spec.rb @@ -75,9 +75,6 @@ module Filters describe '#to_s' do it 'returns the filter as a string' do - filter = described_class.new(zlib: true, width: 640, height: 480, algorithm: 'lanczos') - expect(filter.to_s).to eq('zscale=w=640:h=480:f=lanczos') - filter = described_class.new(width: 'iw/2', height: 'ih/2', algorithm: 'lanczos') expect(filter.to_s).to eq('scale=w=iw/2:h=ih/2:flags=lanczos') From c09b07c28618e9591dcc052931d065ac5d2c1aec Mon Sep 17 00:00:00 2001 From: bajankristof Date: Tue, 15 Apr 2025 23:03:27 +0200 Subject: [PATCH 4/6] feat!: various API and MPEG-DASH preset improvements BREAKING CHANGE: - The display_aspect_ratio and sample_aspect_ratio methods have been renamed to raw_display_aspect_ratio and raw_sample_aspect_ratio respectively on both the FFMPEG::Media and FFMPEG::Stream classes. - The calculated_aspect_ratio and calculated_pixel_aspect_ratio methods have been renamed to display_aspect_ratio and sample_aspect_ratio respectively on both the FFMPEG::Media and FFMPEG::Stream classes. - More composer methods support stream specifiers in the RawCommandArgs (and by extension the CommandArgs) class. - The MPEG-DASH H.264 preset have been greatly simplified to achieve the same exact results. --- lib/ffmpeg/filters/scale.rb | 2 +- lib/ffmpeg/media.rb | 18 ++--- lib/ffmpeg/presets/dash.rb | 2 + lib/ffmpeg/presets/dash/h264.rb | 50 +++--------- lib/ffmpeg/presets/h264.rb | 6 ++ lib/ffmpeg/raw_command_args.rb | 112 +++++++++++++++------------ lib/ffmpeg/stream.rb | 51 +++++------- spec/ffmpeg/filters/scale_spec.rb | 2 +- spec/ffmpeg/media_spec.rb | 23 ++---- spec/ffmpeg/raw_command_args_spec.rb | 6 +- spec/ffmpeg/stream_spec.rb | 14 ++-- spec/ffmpeg/transcoder_spec.rb | 2 +- 12 files changed, 129 insertions(+), 159 deletions(-) diff --git a/lib/ffmpeg/filters/scale.rb b/lib/ffmpeg/filters/scale.rb index 5b42e5f..744941b 100644 --- a/lib/ffmpeg/filters/scale.rb +++ b/lib/ffmpeg/filters/scale.rb @@ -86,7 +86,7 @@ def contained(media, max_width: nil, max_height: nil, **kwargs) if width.negative? || height.negative? new(width:, height:, **kwargs) - elsif media.calculated_aspect_ratio > Rational(width, height) + elsif media.display_aspect_ratio > Rational(width, height) new(width:, height: -2, **kwargs) else new(width: -2, height:, **kwargs) diff --git a/lib/ffmpeg/media.rb b/lib/ffmpeg/media.rb index bf0b6c8..56b6d54 100644 --- a/lib/ffmpeg/media.rb +++ b/lib/ffmpeg/media.rb @@ -270,25 +270,25 @@ def local? default_video_stream&.display_aspect_ratio end - # Returns the sample aspect ratio of the default video stream (if any). + # Returns the raw display aspect ratio of the default video stream (if any). # # @return [String, nil] - autoload def sample_aspect_ratio - default_video_stream&.sample_aspect_ratio + autoload def raw_display_aspect_ratio + default_video_stream&.raw_display_aspect_ratio end - # Returns the calculated aspect ratio of the default video stream (if any). + # Returns the sample aspect ratio of the default video stream (if any). # # @return [String, nil] - autoload def calculated_aspect_ratio - default_video_stream&.calculated_aspect_ratio + autoload def sample_aspect_ratio + default_video_stream&.sample_aspect_ratio end - # Returns the calculated pixel aspect ratio of the default video stream (if any). + # Returns the raw sample aspect ratio of the default video stream (if any). # # @return [String, nil] - autoload def calculated_pixel_aspect_ratio - default_video_stream&.calculated_pixel_aspect_ratio + autoload def raw_sample_aspect_ratio + default_video_stream&.raw_sample_aspect_ratio end # Returns the pixel format of the default video stream (if any). diff --git a/lib/ffmpeg/presets/dash.rb b/lib/ffmpeg/presets/dash.rb index 05cef99..7ac7f84 100644 --- a/lib/ffmpeg/presets/dash.rb +++ b/lib/ffmpeg/presets/dash.rb @@ -28,6 +28,8 @@ def initialize( super(name:, filename:, metadata:) do threads preset.threads if preset.threads format_name 'dash' + use_template 1 + use_timeline 1 segment_duration preset.segment_duration muxing_flags 'frag_keyframe+empty_moov+default_base_moof' diff --git a/lib/ffmpeg/presets/dash/h264.rb b/lib/ffmpeg/presets/dash/h264.rb index 410e5d9..19873c5 100644 --- a/lib/ffmpeg/presets/dash/h264.rb +++ b/lib/ffmpeg/presets/dash/h264.rb @@ -278,48 +278,20 @@ def initialize( h264_presets = preset.usable_h264_presets(media) if media.video_streams? - # Split the default video stream into multiple streams, - # one for each usable H.264 preset (e.g.: [v:0]split=2[v0][v1]). - split_filter = - Filters - .split(h264_presets.length) - .with_input_link!(media.video_mapping_id) - .with_output_links!(*h264_presets.each_with_index.map { |_, index| "v#{index}" }) - - # Scale the split video streams to the desired resolutions - # and frame rates (e.g.: [v0]scale=640:360,fps=30[v0out]). - # We also apply the desired pixel format to the video stream, - # as well as set the display aspect ratio to the calculated aspect ratio - # to resolve potential issues with different aspect ratios. - stream_filter_graphs = - h264_presets.each_with_index.map do |h264_preset, index| - fps_filter = Filters.fps(adjusted_frame_rate(h264_preset.frame_rate)) - format_filter = h264_preset.format_filter - scale_filter = h264_preset.scale_filter(media) - dar_filter = Filters.set_dar(media.calculated_aspect_ratio) if media.calculated_aspect_ratio - - stream_filters = [fps_filter, format_filter, scale_filter, dar_filter].compact - stream_filters.first.with_input_link!("v#{index}") - stream_filters.last.with_output_link!("v#{index}out") - - Filter.join(*stream_filters) - end - - # Apply the generated filter complex to the output. - filter_complex split_filter, *stream_filter_graphs - - # Force keyframes at the specified interval. - force_key_frames "expr:gte(t,n_forced*#{preset.keyframe_interval})" - - # Force aspect ratio to the calculated aspect ratio. - aspect media.calculated_aspect_ratio if media.calculated_aspect_ratio - - # Map the scaled video streams with the desired H.264 parameters. + # Use the default video stream for all representations. h264_presets.each_with_index do |h264_preset, index| - map "[v#{index}out]" do + map media.video_mapping_id do + frame_rate = adjusted_frame_rate(h264_preset.frame_rate) + filters Filters.fps(frame_rate), + h264_preset.format_filter, + h264_preset.scale_filter(media), + h264_preset.dar_filter(media), + stream_index: index video_preset h264_preset.video_preset, stream_index: index video_profile h264_preset.video_profile, stream_index: index - constant_rate_factor h264_preset.constant_rate_factor, stream_id: "v:#{index}" + 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 end end end diff --git a/lib/ffmpeg/presets/h264.rb b/lib/ffmpeg/presets/h264.rb index e220009..3a7a087 100644 --- a/lib/ffmpeg/presets/h264.rb +++ b/lib/ffmpeg/presets/h264.rb @@ -352,6 +352,12 @@ def scale_filter(media) Filters::Scale.contained(media, max_width: @max_width, max_height: @max_height) end + + def dar_filter(media) + return unless media.display_aspect_ratio + + Filters.set_dar(media.display_aspect_ratio) + end end end end diff --git a/lib/ffmpeg/raw_command_args.rb b/lib/ffmpeg/raw_command_args.rb index 14c1b52..6bd1544 100644 --- a/lib/ffmpeg/raw_command_args.rb +++ b/lib/ffmpeg/raw_command_args.rb @@ -207,6 +207,8 @@ def map(stream_id) # Adds a new filter to the command arguments. # # @param filter [FFMPEG::Filter] The filter to add. + # @param stream_id [String] The stream ID to target (see stream_arg). + # @param stream_index [String] The stream index to target (see stream_arg). # @return [self] # # @example @@ -214,26 +216,30 @@ def map(stream_id) # filter FFMPEG::Filters.scale(width: -2, height: 1080) # end # args.to_s # "-vf scale=w=-2:h=1080" - def filter(filter) - filters(filter) + def filter(filter, stream_id: nil, stream_index: nil) + filters(filter, stream_id:, stream_index:) end # Adds multiple filters to the command arguments # in a single filter chain. # # @param filters [Array] The filters to add. + # @param stream_id [String] The stream ID to target (see stream_arg). + # @param stream_index [String] The stream index to target (see stream_arg). # @return [self] # + # @note Make sure not to mix video and audio filters when using stream_id or stream_index. + # # @example # args = FFMPEG::RawCommandArgs.compose do # filters FFMPEG::Filters.scale(width: -2, height: 1080), # FFMPEG::Filters.fps(24), # FFMPEG::Filters.silence_detect # end - # args.to_s # "-vf scale=w=-2:h=1080,fps=24 -af silencedetect" - def filters(*filters) + # args.to_s # "-filter:v scale=w=-2:h=1080,fps=24 -filter:a silencedetect" + def filters(*filters, stream_id: nil, stream_index: nil) filters.compact.group_by(&:type).each do |type, group| - arg("#{type.to_s[0]}f", Filter.join(*group)) + stream_arg('filter', Filter.join(*group), stream_type: type.to_s[0], stream_id:, stream_index:) end self @@ -242,6 +248,8 @@ def filters(*filters) # Adds a new bitstream filter to the command arguments. # # @param filter [FFMPEG::Filter] The bitstream filter to add. + # @param stream_id [String] The stream ID to target (see stream_arg). + # @param stream_index [String] The stream index to target (see stream_arg). # @return [self] # # @example @@ -249,24 +257,28 @@ def filters(*filters) # bitstream_filter FFMPEG::Filter.new(:video, 'h264_mp4toannexb') # end # args.to_s # "-bsf:v h264_mp4toannexb" - def bitstream_filter(filter) - bitstream_filters(filter) + def bitstream_filter(filter, stream_id: nil, stream_index: nil) + bitstream_filters(filter, stream_id:, stream_index:) end # Adds multiple bitstream filters to the command arguments. # # @param filters [Array] The bitstream filters to add. + # @param stream_id [String] The stream ID to target (see stream_arg). + # @param stream_index [String] The stream index to target (see stream_arg). # @return [self] # + # @note Make sure not to mix video and audio filters when using stream_id or stream_index. + # # @example # args = FFMPEG::RawCommandArgs.compose do # bitstream_filters FFMPEG::Filter.new(:video, 'h264_mp4toannexb'), # FFMPEG::Filter.new(:audio, 'aac_adtstoasc') # end # args.to_s # "-bsf:v h264_mp4toannexb -bsf:a aac_adtstoasc" - def bitstream_filters(*filters) + def bitstream_filters(*filters, stream_id: nil, stream_index: nil) filters.compact.group_by(&:type).each do |type, group| - arg("bsf:#{type.to_s[0]}", Filter.join(*group)) + stream_arg('bsf', Filter.join(*group), stream_type: type.to_s[0], stream_id:, stream_index:) end self @@ -381,7 +393,7 @@ def constant_rate_factor(value, **kwargs) # Sets a video codec in the command arguments. # # @param value [String] The video codec to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -395,14 +407,14 @@ def constant_rate_factor(value, **kwargs) # video_codec_name 'libx264', stream_index: 0 # end # args.to_s # "-c:v:0 libx264" - def video_codec_name(value, stream_index: nil) - stream_arg('c', value, stream_type: 'v', stream_index:) + def video_codec_name(value, **kwargs) + stream_arg('c', value, stream_type: 'v', **kwargs) end # Sets a video bit rate in the command arguments. # # @param value [String, Numeric] The video bit rate to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -416,8 +428,8 @@ def video_codec_name(value, stream_index: nil) # video_bit_rate '128k', stream_index: 0 # end # args.to_s # "-b:v:0 128k" - def video_bit_rate(value, stream_index: nil) - stream_arg('b', value, stream_type: 'v', stream_index:) + def video_bit_rate(value, **kwargs) + stream_arg('b', value, stream_type: 'v', **kwargs) end # Sets a minimum video bit rate in the command arguments. @@ -451,7 +463,7 @@ def max_video_bit_rate(value) # Sets a video preset in the command arguments. # # @param value [String] The video preset to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -465,14 +477,14 @@ def max_video_bit_rate(value) # video_preset 'fast', stream_index: 0 # end # args.to_s # "-preset:v:0 fast" - def video_preset(value, stream_index: nil) - stream_arg('preset', value, stream_type: 'v', stream_index:) + def video_preset(value, **kwargs) + stream_arg('preset', value, stream_type: 'v', **kwargs) end # Sets a video profile in the command arguments. # # @param value [String] The video profile to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -486,14 +498,14 @@ def video_preset(value, stream_index: nil) # video_profile 'high', stream_index: 0 # end # args.to_s # "-profile:v:0 high" - def video_profile(value, stream_index: nil) - stream_arg('profile', value, stream_type: 'v', stream_index:) + def video_profile(value, **kwargs) + stream_arg('profile', value, stream_type: 'v', **kwargs) end # Sets a video quality in the command arguments. # # @param value [String] The video quality to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -507,8 +519,8 @@ def video_profile(value, stream_index: nil) # video_quality '2', stream_index: 0 # end # args.to_s # "-q:v:0 2" - def video_quality(value, stream_index: nil) - stream_arg('q', value, stream_type: 'v', stream_index:) + def video_quality(value, **kwargs) + stream_arg('q', value, stream_type: 'v', **kwargs) end # Sets a frame rate in the command arguments. @@ -556,6 +568,7 @@ def resolution(value) # Sets an aspect ratio in the command arguments. # # @param value [String] The aspect ratio to set. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -563,14 +576,15 @@ def resolution(value) # aspect_ratio '16:9' # end # args.to_s # "-aspect 16:9" - def aspect_ratio(value) - arg('aspect', value) + def aspect_ratio(value, **kwargs) + stream_arg('aspect', value, stream_type: 'v', **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). # @return [self] # # @example @@ -578,14 +592,15 @@ def aspect_ratio(value) # min_keyframe_interval 48 # end # args.to_s # "-keyint_min 48" - def min_keyframe_interval(value) - arg('keyint_min', value) + def min_keyframe_interval(value, **kwargs) + stream_arg('keyint_min', value, **kwargs) end # 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] # # @example @@ -593,14 +608,15 @@ def min_keyframe_interval(value) # max_keyframe_interval 48 # end # args.to_s # "-g 48" - def max_keyframe_interval(value) - arg('g', value) + def max_keyframe_interval(value, **kwargs) + stream_arg('g', 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). # @return [self] # # @example @@ -608,8 +624,8 @@ def max_keyframe_interval(value) # scene_change_threshold 0 # end # args.to_s # "-sc_threshold 0" - def scene_change_threshold(value) - arg('sc_threshold', value) + def scene_change_threshold(value, **kwargs) + stream_arg('sc_threshold', value, **kwargs) end # =================== # @@ -619,7 +635,7 @@ def scene_change_threshold(value) # Sets an audio codec in the command arguments. # # @param value [String] The audio codec to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -633,14 +649,14 @@ def scene_change_threshold(value) # audio_codec_name 'aac', stream_index: 0 # end # args.to_s # "-c:a:0 aac" - def audio_codec_name(value, stream_index: nil) - stream_arg('c', value, stream_type: 'a', stream_index:) + def audio_codec_name(value, **kwargs) + stream_arg('c', value, stream_type: 'a', **kwargs) end # Sets an audio bit rate in the command arguments. # # @param value [String, Numeric] The audio bit rate to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -654,8 +670,8 @@ def audio_codec_name(value, stream_index: nil) # audio_bit_rate '128k', stream_index: 0 # end # args.to_s # "-b:a:0 128k" - def audio_bit_rate(value, stream_index: nil) - stream_arg('b', value, stream_type: 'a', stream_index:) + def audio_bit_rate(value, **kwargs) + stream_arg('b', value, stream_type: 'a', **kwargs) end # Sets an audio sample rate in the command arguments. @@ -689,7 +705,7 @@ def audio_channels(value) # Sets an audio preset in the command arguments. # # @param value [String] The audio preset to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -703,14 +719,14 @@ def audio_channels(value) # audio_preset 'aac_low', stream_index: 0 # end # args.to_s # "-profile:a:0 aac_low" - def audio_preset(value, stream_index: nil) - stream_arg('preset', value, stream_type: 'a', stream_index:) + def audio_preset(value, **kwargs) + stream_arg('preset', value, stream_type: 'a', **kwargs) end # Sets an audio profile in the command arguments. # # @param value [String] The audio profile to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -724,14 +740,14 @@ def audio_preset(value, stream_index: nil) # audio_profile 'aac_low', stream_index: 0 # end # args.to_s # "-profile:a:0 aac_low" - def audio_profile(value, stream_index: nil) - stream_arg('profile', value, stream_type: 'a', stream_index:) + def audio_profile(value, **kwargs) + stream_arg('profile', value, stream_type: 'a', **kwargs) end # Sets an audio quality in the command arguments. # # @param value [String] The audio quality to set. - # @param stream_index [String] The stream index to target. + # @param kwargs [Hash] The stream specific arguments to use (see stream_arg). # @return [self] # # @example @@ -745,8 +761,8 @@ def audio_profile(value, stream_index: nil) # audio_quality '2', stream_index: 0 # end # args.to_s # "-q:a:0 2" - def audio_quality(value, stream_index: nil) - stream_arg('q', value, stream_type: 'a', stream_index:) + def audio_quality(value, **kwargs) + stream_arg('q', value, stream_type: 'a', **kwargs) end # Sets the audio sync in the command arguments. @@ -771,7 +787,7 @@ def respond_to_missing? end def method_missing(name, *args) - arg(name, args.first) + arg(name.to_s, args.first) end end end diff --git a/lib/ffmpeg/stream.rb b/lib/ffmpeg/stream.rb index 1014918..d21a43f 100644 --- a/lib/ffmpeg/stream.rb +++ b/lib/ffmpeg/stream.rb @@ -6,7 +6,8 @@ class Stream attr_reader :metadata, :id, :index, :profile, :tags, :codec_name, :codec_long_name, :codec_tag, :codec_tag_string, :codec_type, - :coded_width, :coded_height, :sample_aspect_ratio, :display_aspect_ratio, :rotation, + :raw_width, :raw_height, :coded_width, :coded_height, + :raw_sample_aspect_ratio, :raw_display_aspect_ratio, :rotation, :pixel_format, :color_range, :color_space, :color_primaries, :color_transfer, :field_order, :frame_rate, :sample_rate, :sample_fmt, :channels, :channel_layout, :start_time, :bit_rate, :duration, :frames, :overview @@ -25,12 +26,12 @@ def initialize(metadata, stderr = '') @codec_tag_string = metadata[:codec_tag_string] @codec_type = metadata[:codec_type]&.to_sym - @width = metadata[:width]&.to_i - @height = metadata[:height]&.to_i + @raw_width = metadata[:width]&.to_i + @raw_height = metadata[:height]&.to_i @coded_width = metadata[:coded_width]&.to_i @coded_height = metadata[:coded_height]&.to_i - @sample_aspect_ratio = metadata[:sample_aspect_ratio] - @display_aspect_ratio = metadata[:display_aspect_ratio] + @raw_sample_aspect_ratio = metadata[:sample_aspect_ratio] + @raw_display_aspect_ratio = metadata[:display_aspect_ratio] @rotation = if metadata.dig(:tags, :rotate) @@ -73,7 +74,7 @@ def initialize(metadata, stderr = '') "#{color_space || 'unknown'}/#{color_transfer || 'unknown'}/#{color_primaries || 'unknown'}, " \ "#{field_order || 'unknown'}), " \ "#{resolution} " \ - "[SAR #{sample_aspect_ratio} DAR #{display_aspect_ratio}]" + "[SAR #{raw_sample_aspect_ratio} DAR #{raw_display_aspect_ratio}]" elsif audio? @overview = "#{codec_name} " \ "(#{codec_tag_string} / #{codec_tag}), " \ @@ -159,15 +160,7 @@ def landscape? # # @return [Integer] def width - rotated? ? @height : @width - end - - # The raw width of the stream. - # This is the width of the stream without considering rotation. - # - # @return [Integer] - def raw_width - @width + rotated? ? @raw_height : @raw_width end # The height of the stream. @@ -175,15 +168,7 @@ def raw_width # # @return [Integer] def height - rotated? ? @width : @height - end - - # The raw height of the stream. - # This is the height of the stream without considering rotation. - # - # @return [Integer] - def raw_height - @height + rotated? ? @raw_width : @raw_height end # The resolution of the stream. @@ -202,13 +187,13 @@ def resolution # If the stream is rotated, the inverted aspect ratio is returned. # # @return [Rational, nil] - def calculated_aspect_ratio - return @calculated_aspect_ratio unless @calculated_aspect_ratio.nil? + def display_aspect_ratio + return @display_aspect_ratio unless @display_aspect_ratio.nil? - @calculated_aspect_ratio = calculate_aspect_ratio(display_aspect_ratio) - @calculated_aspect_ratio ||= Rational(width, height) if width && height + @display_aspect_ratio = calculate_aspect_ratio(@raw_display_aspect_ratio) + @display_aspect_ratio ||= Rational(width, height) if width && height - @calculated_aspect_ratio + @display_aspect_ratio end # The calculated pixel aspect ratio of the stream. @@ -217,11 +202,11 @@ def calculated_aspect_ratio # If the stream is rotated, the inverted aspect ratio is returned. # # @return [Rational] - def calculated_pixel_aspect_ratio - return @calculated_pixel_aspect_ratio unless @calculated_pixel_aspect_ratio.nil? + def sample_aspect_ratio + return @sample_aspect_ratio unless @sample_aspect_ratio.nil? - @calculated_pixel_aspect_ratio = calculate_aspect_ratio(sample_aspect_ratio) - @calculated_pixel_aspect_ratio ||= Rational(1) + @sample_aspect_ratio = calculate_aspect_ratio(@raw_sample_aspect_ratio) + @sample_aspect_ratio ||= Rational(1) end protected diff --git a/spec/ffmpeg/filters/scale_spec.rb b/spec/ffmpeg/filters/scale_spec.rb index 11ecb74..e5be5e9 100644 --- a/spec/ffmpeg/filters/scale_spec.rb +++ b/spec/ffmpeg/filters/scale_spec.rb @@ -51,7 +51,7 @@ module Filters context 'when the aspect ratio is higher than the max_width and max_height' do it 'returns a contained scale filter that scales to width' do - expect(media).to receive(:calculated_aspect_ratio).and_return(2) + expect(media).to receive(:display_aspect_ratio).and_return(2) expect(described_class.contained(media, max_width: 640, max_height: 480).to_s).to eq('scale=w=640:h=-2') end end diff --git a/spec/ffmpeg/media_spec.rb b/spec/ffmpeg/media_spec.rb index 065e8b7..14d21af 100644 --- a/spec/ffmpeg/media_spec.rb +++ b/spec/ffmpeg/media_spec.rb @@ -415,23 +415,9 @@ module FFMPEG end describe '#display_aspect_ratio' do - let(:path) { fixture_media_file('portrait@4k60.mp4') } - - it 'returns the display aspect ratio of the default video stream' do - expect(subject.display_aspect_ratio).to eq('16:9') - end - end - - describe '#sample_aspect_ratio' do - it 'returns the sample aspect ratio of the default video stream' do - expect(subject.sample_aspect_ratio).to eq('1:1') - end - end - - describe '#calculated_aspect_ratio' do context 'when the default video stream is not rotated' do it 'returns the aspect ratio of the default video stream' do - expect(subject.calculated_aspect_ratio).to eq(Rational(16, 9)) + expect(subject.display_aspect_ratio).to eq(Rational(16, 9)) end end @@ -439,13 +425,16 @@ module FFMPEG let(:path) { fixture_media_file('portrait@4k60.mp4') } it 'returns the inverted aspect ratio of the default video stream' do - expect(subject.calculated_aspect_ratio).to eq(Rational(9, 16)) + expect(subject.display_aspect_ratio).to eq(Rational(9, 16)) end end end { - calculated_pixel_aspect_ratio: Rational(1), + raw_sample_aspect_ratio: '1:1', + sample_aspect_ratio: Rational(1), + raw_display_aspect_ratio: '16:9', + display_aspect_ratio: Rational(16, 9), pixel_format: 'yuvj420p', color_range: 'pc', color_space: 'bt709', diff --git a/spec/ffmpeg/raw_command_args_spec.rb b/spec/ffmpeg/raw_command_args_spec.rb index 61eae16..182004a 100644 --- a/spec/ffmpeg/raw_command_args_spec.rb +++ b/spec/ffmpeg/raw_command_args_spec.rb @@ -88,7 +88,7 @@ module FFMPEG it 'adds the correct filter argument' do filter = Filters.fps(30) subject.filter(filter) - expect(subject.to_a).to eq(['-vf', filter.to_s]) + expect(subject.to_a).to eq(['-filter:v', filter.to_s]) end end @@ -97,7 +97,7 @@ module FFMPEG video_filters = [Filters.fps(30), Filters.grayscale] audio_filters = [Filters.silence_detect] subject.filters(*video_filters[0..1], *audio_filters, *video_filters[2..]) - expect(subject.to_a).to eq(['-vf', Filter.join(*video_filters), '-af', Filter.join(*audio_filters)]) + expect(subject.to_a).to eq(['-filter:v', Filter.join(*video_filters), '-filter:a', Filter.join(*audio_filters)]) end end @@ -169,7 +169,6 @@ module FFMPEG frame_rate: 'r', pixel_format: 'pix_fmt', resolution: 's', - aspect_ratio: 'aspect', min_keyframe_interval: 'keyint_min', max_keyframe_interval: 'g', scene_change_threshold: 'sc_threshold', @@ -187,6 +186,7 @@ module FFMPEG end { + aspect_ratio: 'aspect:v', video_codec_name: 'c:v', video_bit_rate: 'b:v', video_preset: 'preset:v', diff --git a/spec/ffmpeg/stream_spec.rb b/spec/ffmpeg/stream_spec.rb index c4e1015..9452af0 100644 --- a/spec/ffmpeg/stream_spec.rb +++ b/spec/ffmpeg/stream_spec.rb @@ -168,12 +168,12 @@ module FFMPEG end end - describe '#calculated_aspect_ratio' do + describe '#display_aspect_ratio' do context 'when the display_aspect_ratio is nil' do let(:metadata) { { width: 100, height: 200 } } it 'returns the aspect ratio from the width and height' do - expect(subject.calculated_aspect_ratio).to eq(Rational(1, 2)) + expect(subject.display_aspect_ratio).to eq(Rational(1, 2)) end end @@ -181,25 +181,25 @@ module FFMPEG let(:metadata) { { width: 100, height: 200, display_aspect_ratio: '16:9' } } it 'returns the aspect ratio from the display_aspect_ratio' do - expect(subject.calculated_aspect_ratio).to eq(Rational(16, 9)) + expect(subject.display_aspect_ratio).to eq(Rational(16, 9)) end context 'and the stream is rotated' do let(:metadata) { { width: 100, height: 200, display_aspect_ratio: '16:9', tags: { rotate: 90 } } } it 'returns the aspect ratio from the display_aspect_ratio' do - expect(subject.calculated_aspect_ratio).to eq(Rational(9, 16)) + expect(subject.display_aspect_ratio).to eq(Rational(9, 16)) end end end end - describe '#calculated_pixel_aspect_ratio' do + describe '#sample_aspect_ratio' do context 'when the sample_aspect_ratio is nil' do let(:metadata) { { sample_aspect_ratio: nil } } it 'returns 1' do - expect(subject.calculated_pixel_aspect_ratio).to eq(Rational(1)) + expect(subject.sample_aspect_ratio).to eq(Rational(1)) end end @@ -207,7 +207,7 @@ module FFMPEG let(:metadata) { { sample_aspect_ratio: '16:9' } } it 'returns the aspect ratio from the sample_aspect_ratio' do - expect(subject.calculated_pixel_aspect_ratio).to eq(Rational(16, 9)) + expect(subject.sample_aspect_ratio).to eq(Rational(16, 9)) end end end diff --git a/spec/ffmpeg/transcoder_spec.rb b/spec/ffmpeg/transcoder_spec.rb index d56bd95..26f5192 100644 --- a/spec/ffmpeg/transcoder_spec.rb +++ b/spec/ffmpeg/transcoder_spec.rb @@ -45,7 +45,7 @@ module FFMPEG expect(args).to eq( %W[ -c:v libx264 -c:a aac - -map v:0 -vf scale=w=-2:h=360 -crf 28 + -map v:0 -filter:v scale=w=-2:h=360 -crf 28 -map a:0 -b:a 96k #{output_path}.mp4 -c:a aac From d2689666cf15fecfe524ec97998ae3803892f04d Mon Sep 17 00:00:00 2001 From: bajankristof Date: Tue, 15 Apr 2025 09:51:37 +0200 Subject: [PATCH 5/6] ci: allow workflows to be triggered manually --- .github/workflows/ci.lint.yml | 1 + .github/workflows/ci.test.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.lint.yml b/.github/workflows/ci.lint.yml index 7da7846..7a04303 100644 --- a/.github/workflows/ci.lint.yml +++ b/.github/workflows/ci.lint.yml @@ -5,6 +5,7 @@ on: branches: [ 'main' ] pull_request: branches: [ 'main' ] + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/ci.test.yml b/.github/workflows/ci.test.yml index 5904646..c2a044f 100644 --- a/.github/workflows/ci.test.yml +++ b/.github/workflows/ci.test.yml @@ -5,6 +5,7 @@ on: branches: [ 'main' ] pull_request: branches: [ 'main' ] + workflow_dispatch: permissions: contents: read From eaeecb673b1342585aa17b5d8997693231633a3c Mon Sep 17 00:00:00 2001 From: bajankristof Date: Tue, 15 Apr 2025 10:00:25 +0200 Subject: [PATCH 6/6] chore: update version to 7.0.0-beta.12 and document changes --- CHANGELOG | 17 +++++++++++++++++ lib/ffmpeg/version.rb | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6c06f2d..d132eb3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,20 @@ +== 7.0.0-beta.12 2025-04-15 + +Breaking Changes: +* The `display_aspect_ratio` and `sample_aspect_ratio` methods have been renamed to + `raw_display_aspect_ratio` and `raw_sample_aspect_ratio` respectively on both + the Media and Stream classes. +* The `calculated_aspect_ratio` and `calculated_pixel_aspect_ratio` methods have been + renamed to `display_aspect_ratio` and `sample_aspect_ratio` respectively on both + the Media and Stream classes. +* Rolled back zlib scale changes (they caused more errors than what they resolved). + +Improvements: +* Added more getters for color information to the Media class. +* Added more options to the scale filter. +* Greatly simplified the MPEG-DASH H.264 preset. +* Added support for stream specifiers in many RawCommandArgs methods. + == 7.0.0-beta.11 2025-04-10 Improvements: diff --git a/lib/ffmpeg/version.rb b/lib/ffmpeg/version.rb index 396ccc2..ce82eac 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.11' + VERSION = '7.0.0-beta.12' end