Skip to content

Commit cdad933

Browse files
committed
feat: add new modular way to compose command arguments
Refs: ARC-10735
1 parent 4f170c3 commit cdad933

8 files changed

Lines changed: 314 additions & 0 deletions

File tree

lib/ffmpeg.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
require 'shellwords'
77

88
require_relative 'ffmpeg/command_args'
9+
require_relative 'ffmpeg/command_args/color_space_injection'
10+
require_relative 'ffmpeg/command_args/composable'
11+
require_relative 'ffmpeg/command_args/network_streaming'
912
require_relative 'ffmpeg/errors'
1013
require_relative 'ffmpeg/filter'
1114
require_relative 'ffmpeg/filters/format'
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'composable'
4+
5+
module FFMPEG
6+
class CommandArgs
7+
# The ColorSpaceInjection composable contains logic for injecting
8+
# color space metadata into video streams by taking a wild guess at the
9+
# color space (uses bt709 for H.264 and HEVC).
10+
# This composable is best used as an input argument composer.
11+
# See https://trac.ffmpeg.org/ticket/11020 for more information.
12+
module ColorSpaceInjection
13+
include FFMPEG::CommandArgs::Composable
14+
15+
compose do
16+
next unless media.video_streams?
17+
next unless %w[h264 hevc].include?(media.video_codec_name)
18+
next unless media.color_space == 'reserved'
19+
20+
bitstream_filter FFMPEG::Filter.new(
21+
:video,
22+
"#{media.video_codec_name}_metadata",
23+
colour_primaries: 1,
24+
transfer_characteristics: 1,
25+
matrix_coefficients: 1
26+
)
27+
end
28+
end
29+
end
30+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require 'securerandom'
4+
5+
require_relative '../command_args'
6+
7+
module FFMPEG
8+
class CommandArgs
9+
# The Composable module allows for composing command arguments in a modular way.
10+
module Composable
11+
module ClassMethods # rubocop:disable Style/Documentation
12+
attr_reader :blocks
13+
14+
# Defines a block of code that can be composed into command arguments.
15+
# Multiple blocks can be defined with different names,
16+
# and they can be used to compose command arguments in a modular way.
17+
#
18+
# @param name [Object] The name of the block.
19+
# @param block [Proc] The block of code to be executed in context of the command arguments.
20+
# @return [self]
21+
#
22+
# @example
23+
# module MyCommandArgs
24+
# include FFMPEG::CommandArgs::Composable
25+
#
26+
# compose :h264 do
27+
# video_codec_name 'libx264'
28+
# end
29+
#
30+
# compose :aac do
31+
# audio_codec_name 'aac'
32+
# end
33+
# end
34+
#
35+
# args = FFMPEG::RawCommandArgs.compose do
36+
# use MyCommandArgs, only: %i[h264]
37+
# end
38+
# args.to_s # "-c:v libx264"
39+
def compose(name = SecureRandom.hex(4), &block)
40+
return unless block_given?
41+
42+
@blocks ||= {}
43+
@blocks[name] = block
44+
45+
self
46+
end
47+
end
48+
49+
class << self
50+
def included(base)
51+
base.extend(ClassMethods)
52+
end
53+
end
54+
end
55+
end
56+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'composable'
4+
5+
module FFMPEG
6+
class CommandArgs
7+
# The NetworkStreaming composable contains some defaults
8+
# for network streaming operations.
9+
# This composable is best used as an input argument composer.
10+
module NetworkStreaming
11+
include FFMPEG::CommandArgs::Composable
12+
13+
compose do
14+
next unless media.remote?
15+
16+
reconnect 1
17+
reconnect_at_eof 1
18+
reconnect_streamed 1
19+
reconnect_delay_max 30
20+
reconnect_on_network_error 1
21+
reconnect_on_http_error '500,502,503,504'
22+
reconnect_on_timeout 1
23+
end
24+
end
25+
end
26+
end

lib/ffmpeg/raw_command_args.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,44 @@ def to_a
9696
@args
9797
end
9898

99+
# Adds a composable to the command arguments.
100+
#
101+
# @param composable [Module] The composable to add.
102+
# @param only [Array] The names of the blocks to include.
103+
# @param except [Array] The names of the blocks to exclude.
104+
# @return [self]
105+
#
106+
# @example
107+
# module MyCommandArgs
108+
# include FFMPEG::CommandArgs::Composable
109+
#
110+
# compose :h264 do
111+
# video_codec_name 'libx264'
112+
# end
113+
#
114+
# compose :aac do
115+
# audio_codec_name 'aac'
116+
# end
117+
# end
118+
#
119+
# args = FFMPEG::RawCommandArgs.compose do
120+
# use MyCommandArgs, only: %i[h264]
121+
# end
122+
# args.to_s # "-c:v libx264"
123+
def use(composable, only: nil, except: nil)
124+
only = [only].compact unless only.is_a?(Array)
125+
except = [except].compact unless except.is_a?(Array)
126+
127+
composable.blocks&.each do |name, block|
128+
next if !only.empty? && !only.include?(name)
129+
next if !except.empty? && except.include?(name)
130+
131+
instance_exec(&block)
132+
end
133+
134+
self
135+
end
136+
99137
# ==================== #
100138
# === COMMON UTILS === #
101139
# ==================== #
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../spec_helper'
4+
5+
module FFMPEG
6+
class CommandArgs
7+
describe ColorSpaceInjection do
8+
let(:media) { instance_double(FFMPEG::Media) }
9+
10+
subject(:args) do
11+
CommandArgs.compose(media) do
12+
use ColorSpaceInjection
13+
end.to_a
14+
end
15+
16+
context 'when the media has no video streams' do
17+
before { allow(media).to receive(:video_streams?).and_return(false) }
18+
19+
it 'does not apply color space injection arguments' do
20+
expect(args).to be_empty
21+
end
22+
end
23+
24+
context 'when the media video codec is not H.264 or HEVC' do
25+
before do
26+
allow(media).to receive(:video_streams?).and_return(true)
27+
allow(media).to receive(:video_codec_name).and_return('vp9')
28+
end
29+
30+
it 'does not apply color space injection arguments' do
31+
expect(args).to be_empty
32+
end
33+
end
34+
35+
context 'when the media color space is not reserved' do
36+
before do
37+
allow(media).to receive(:video_streams?).and_return(true)
38+
allow(media).to receive(:video_codec_name).and_return('h264')
39+
allow(media).to receive(:color_space).and_return('bt709')
40+
end
41+
42+
it 'does not apply color space injection arguments' do
43+
expect(args).to be_empty
44+
end
45+
end
46+
47+
context 'when the media meets all conditions for color space injection' do
48+
before do
49+
allow(media).to receive(:video_streams?).and_return(true)
50+
allow(media).to receive(:video_codec_name).and_return('h264')
51+
allow(media).to receive(:color_space).and_return('reserved')
52+
end
53+
54+
it 'applies color space injection arguments' do
55+
expect(args).to eq(
56+
%w[
57+
-bsf:v
58+
h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1
59+
]
60+
)
61+
end
62+
end
63+
end
64+
end
65+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../spec_helper'
4+
5+
module FFMPEG
6+
class CommandArgs
7+
describe NetworkStreaming do
8+
let(:path) { fixture_media_file('napoleon.mp3', remote: true) }
9+
let(:media) { FFMPEG::Media.new(path) }
10+
11+
subject(:args) do
12+
CommandArgs.compose(media) do
13+
use NetworkStreaming
14+
end.to_a
15+
end
16+
17+
before { start_web_server }
18+
after { stop_web_server }
19+
20+
context 'when the media is not remote' do
21+
let(:path) { fixture_media_file('napoleon.mp3') }
22+
23+
it 'does not apply network streaming arguments' do
24+
expect(args).to be_empty
25+
end
26+
end
27+
28+
context 'when the media is remote' do
29+
it 'applies network streaming arguments' do
30+
expect(args).to(
31+
eq(
32+
%w[
33+
-reconnect 1
34+
-reconnect_at_eof 1
35+
-reconnect_streamed 1
36+
-reconnect_delay_max 30
37+
-reconnect_on_network_error 1
38+
-reconnect_on_http_error 500,502,503,504
39+
-reconnect_on_timeout 1
40+
]
41+
)
42+
)
43+
end
44+
end
45+
end
46+
end
47+
end

spec/ffmpeg/raw_command_args_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,55 @@ module FFMPEG
66
describe RawCommandArgs do
77
subject { RawCommandArgs.new }
88

9+
describe '#use' do
10+
let(:composable) do
11+
Module.new do
12+
include FFMPEG::CommandArgs::Composable
13+
14+
compose :foo do
15+
foo 1
16+
end
17+
18+
compose :bar do
19+
bar 1
20+
end
21+
end
22+
end
23+
24+
it 'uses a composable to generate arguments in a modular way' do
25+
subject.use composable
26+
expect(subject.to_a).to eq(%w[-foo 1 -bar 1])
27+
end
28+
29+
context 'when given a non-array `only` keyword argument' do
30+
it 'uses the composable to generate arguments with the specified only keyword' do
31+
subject.use composable, only: :foo
32+
expect(subject.to_a).to eq(%w[-foo 1])
33+
end
34+
end
35+
36+
context 'when given an array `only` keyword argument' do
37+
it 'uses the composable to generate arguments with the specified only keyword' do
38+
subject.use composable, only: %i[foo bar]
39+
expect(subject.to_a).to eq(%w[-foo 1 -bar 1])
40+
end
41+
end
42+
43+
context 'when given a non-array `except` keyword argument' do
44+
it 'uses the composable to generate arguments with the specified except keyword' do
45+
subject.use composable, except: :foo
46+
expect(subject.to_a).to eq(%w[-bar 1])
47+
end
48+
end
49+
50+
context 'when given an array `except` keyword argument' do
51+
it 'uses the composable to generate arguments with the specified except keyword' do
52+
subject.use composable, except: %i[foo bar]
53+
expect(subject.to_a).to eq([])
54+
end
55+
end
56+
end
57+
958
describe '#arg' do
1059
it 'adds the argument' do
1160
subject.arg('foo', 'bar')

0 commit comments

Comments
 (0)