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
7 changes: 7 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
== 7.0.0-beta.11 2025-04-10

Improvements:
* Better mimic the output of FFmpeg with the stream overview methods.
* Added support for using zlib with the Scale filter.
* Updated the H.264 presets to use zlib for scaling by default.

== 7.0.0-beta.10 2025-04-08

Fixes:
Expand Down
137 changes: 109 additions & 28 deletions lib/ffmpeg/filters/scale.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,42 @@
module FFMPEG
module Filters # rubocop:disable Style/Documentation
class << self
def scale(width: nil, height: nil, force_original_aspect_ratio: nil, flags: nil)
Scale.new(width:, height:, force_original_aspect_ratio:, flags:)
def scale(
zlib: false,
width: nil,
height: nil,
algorithm: nil,
in_color_space: nil,
out_color_space: nil,
in_color_range: nil,
out_color_range: nil,
in_color_primaries: nil,
out_color_primaries: nil,
in_color_transfer: nil,
out_color_transfer: nil,
in_chroma_location: nil,
out_chroma_location: nil
)
Scale.new(
zlib:,
width:,
height:,
algorithm:,
in_color_space:,
out_color_space:,
in_color_range:,
out_color_range:,
in_color_primaries:,
out_color_primaries:,
in_color_transfer:,
out_color_transfer:,
in_chroma_location:,
out_chroma_location:
)
end
end

# The Scale class uses the scale filter
# The Scale class uses the scale (or zscale) filter
# to resize a multimedia stream.
class Scale < Filter
NEAREST_DIMENSION = -1
Expand All @@ -25,7 +55,7 @@ class << self
# @param max_width [Numeric] The maximum width to fit.
# @param max_height [Numeric] The maximum height to fit.
# @return [FFMPEG::Filters::Scale] The scale filter.
def contained(media, max_width: nil, max_height: nil)
def contained(media, max_width: nil, max_height: nil, **kwargs)
unless media.is_a?(FFMPEG::Media)
raise ArgumentError,
"Unknown media format #{media.class}, expected #{FFMPEG::Media}"
Expand All @@ -52,18 +82,38 @@ def contained(media, max_width: nil, max_height: nil)
end

if width.negative? || height.negative?
Filters.scale(width:, height:)
new(width:, height:, **kwargs)
elsif media.calculated_aspect_ratio > Rational(width, height)
Filters.scale(width:, height: -2)
new(width:, height: -2, **kwargs)
else
Filters.scale(width: -2, height:)
new(width: -2, height:, **kwargs)
end
end
end

attr_reader :width, :height, :force_original_aspect_ratio, :flags

def initialize(width: nil, height: nil, force_original_aspect_ratio: nil, flags: nil)
attr_reader :width, :height, :algorithm,
:in_color_space, :out_color_space,
:in_color_range, :out_color_range,
:in_color_primaries, :out_color_primaries,
:in_color_transfer, :out_color_transfer,
:in_chroma_location, :out_chroma_location

def initialize(
zlib: false,
width: nil,
height: nil,
algorithm: nil,
in_color_space: nil,
out_color_space: nil,
in_color_range: nil,
out_color_range: nil,
in_color_primaries: nil,
out_color_primaries: nil,
in_color_transfer: nil,
out_color_transfer: nil,
in_chroma_location: nil,
out_chroma_location: nil
)
if !width.nil? && !width.is_a?(Numeric) && !width.is_a?(String)
raise ArgumentError, "Unknown width format #{width.class}, expected #{Numeric} or #{String}"
end
Expand All @@ -72,32 +122,63 @@ def initialize(width: nil, height: nil, force_original_aspect_ratio: nil, flags:
raise ArgumentError, "Unknown height format #{height.class}, expected #{Numeric} or #{String}"
end

if !force_original_aspect_ratio.nil? && !force_original_aspect_ratio.is_a?(String)
raise ArgumentError,
"Unknown force_original_aspect_ratio format #{force_original_aspect_ratio.class}, expected #{String}"
end

if !flags.nil? && !flags.is_a?(Array)
raise ArgumentError, "Unknown flags format #{flags.class}, expected #{Array}"
end

@width = width
@height = height
@force_original_aspect_ratio = force_original_aspect_ratio
@flags = flags
@algorithm = algorithm
@in_color_space = in_color_space
@out_color_space = out_color_space
@in_color_range = in_color_range
@out_color_range = out_color_range
@in_color_primaries = in_color_primaries
@out_color_primaries = out_color_primaries
@in_color_transfer = in_color_transfer
@out_color_transfer = out_color_transfer
@in_chroma_location = in_chroma_location
@out_chroma_location = out_chroma_location

super(:video, zlib ? 'zscale' : 'scale')
end

super(:video, 'scale')
def zlib?
@name.start_with?('z')
end

protected

def format_kwargs
super(
w: @width,
h: @height,
force_original_aspect_ratio: @force_original_aspect_ratio,
flags: @flags
)
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
end
end
end
Expand Down
16 changes: 16 additions & 0 deletions lib/ffmpeg/media.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ def local?
@default_video_stream = video_streams.find(&:default?) || video_streams.first
end

# Whether the media is HDR (High Dynamic Range).
#
# @return [Boolean]
autoload def hdr?
default_video_stream&.color_primaries == 'bt2020' &&
default_video_stream&.color_space == 'bt2020nc' &&
%w[smpte2084 arib-std-b67].include?(default_video_stream&.color_transfer)
end

# Whether the media is rotated (based on the default video stream).
# (e.g. 90°, 180°, 270°)
#
Expand Down Expand Up @@ -282,6 +291,13 @@ def local?
default_video_stream&.calculated_pixel_aspect_ratio
end

# Returns the pixel format of the default video stream (if any).
#
# @return [String, nil]
autoload def pixel_format
default_video_stream&.pixel_format
end

# Returns the color range of the default video stream (if any).
#
# @return [String, nil]
Expand Down
21 changes: 20 additions & 1 deletion lib/ffmpeg/presets/h264.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def h264_144p(
frame_rate: 30,
constant_rate_factor: 28,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -34,6 +35,7 @@ def h264_144p(
pixel_format:,
max_width: 256,
max_height: 144,
zlib:,
&
)
end
Expand All @@ -49,6 +51,7 @@ def h264_240p(
frame_rate: 30,
constant_rate_factor: 28,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -64,6 +67,7 @@ def h264_240p(
pixel_format:,
max_width: 426,
max_height: 240,
zlib:,
&
)
end
Expand All @@ -79,6 +83,7 @@ def h264_360p(
frame_rate: 30,
constant_rate_factor: 28,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -94,6 +99,7 @@ def h264_360p(
pixel_format:,
max_width: 640,
max_height: 360,
zlib:,
&
)
end
Expand All @@ -109,6 +115,7 @@ def h264_480p(
frame_rate: 30,
constant_rate_factor: 27,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -124,6 +131,7 @@ def h264_480p(
pixel_format:,
max_width: 854,
max_height: 480,
zlib:,
&
)
end
Expand All @@ -139,6 +147,7 @@ def h264_720p(
frame_rate: 60,
constant_rate_factor: 27,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -154,6 +163,7 @@ def h264_720p(
pixel_format:,
max_width: 1280,
max_height: 720,
zlib:,
&
)
end
Expand All @@ -169,6 +179,7 @@ def h264_1080p(
frame_rate: 60,
constant_rate_factor: 27,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -184,6 +195,7 @@ def h264_1080p(
pixel_format:,
max_width: 1920,
max_height: 1080,
zlib:,
&
)
end
Expand All @@ -199,6 +211,7 @@ def h264_1440p(
frame_rate: 60,
constant_rate_factor: 26,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -214,6 +227,7 @@ def h264_1440p(
pixel_format:,
max_width: 2560,
max_height: 1440,
zlib:,
&
)
end
Expand All @@ -229,6 +243,7 @@ def h264_4k(
frame_rate: 60,
constant_rate_factor: 26,
pixel_format: 'yuv420p',
zlib: true,
&
)
H264.new(
Expand All @@ -244,6 +259,7 @@ def h264_4k(
pixel_format:,
max_width: 3840,
max_height: 2160,
zlib:,
&
)
end
Expand All @@ -266,6 +282,7 @@ 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,
Expand All @@ -280,6 +297,7 @@ def initialize(
pixel_format: 'yuv420p',
max_width: nil,
max_height: nil,
zlib: true,
&
)
if max_width && !max_width.is_a?(Numeric)
Expand All @@ -299,6 +317,7 @@ def initialize(
@pixel_format = pixel_format
@max_width = max_width
@max_height = max_height
@zlib = zlib
preset = self

super(name:, filename:, metadata:) do
Expand Down Expand Up @@ -350,7 +369,7 @@ def format_filter
def scale_filter(media)
return unless @max_width || @max_height

Filters::Scale.contained(media, max_width: @max_width, max_height: @max_height)
Filters::Scale.contained(media, zlib: @zlib, max_width: @max_width, max_height: @max_height)
end
end
end
Expand Down
Loading