-
1
module Ffmprb
-
-
1
File.image_extname_regex = /^\.(jpe?g|a?png|y4m)$/i
-
1
File.sound_extname_regex = /^\.(mp3|wav)$/i
-
1
File.movie_extname_regex = /^\.(mp4|flv|mov)$/i
-
-
1
Filter.silence_noise_max_db = -40
-
-
# NOTE ducking is currently not for streams
-
1
Process.duck_audio_silent_min = 3
-
1
Process.duck_audio_transition_length = 1
-
1
Process.duck_audio_transition_in_start = -0.4
-
1
Process.duck_audio_transition_out_start = -0.6
-
1
Process.duck_audio_volume_hi = 0.9
-
1
Process.duck_audio_volume_lo = 0.1
-
1
Process.timeout = 30
-
-
1
Process.input_video_auto_rotate = false
-
1
Process.input_video_fps = nil # NOTE the documented ffmpeg default is 25
-
-
1
Process.output_video_resolution = CGA
-
1
Process.output_video_fps = 16 # NOTE the documented ffmpeg default is 25
-
1
Process.output_audio_encoder = 'libmp3lame'
-
1
Process.output_audio_sampling_freq = nil # NOTE Use ffmpeg default by default, specify otherwise e.g. 44100
-
-
1
Util.cmd_timeout = 30
-
1
Util.ffmpeg_cmd = %w[ffmpeg -y]
-
1
Util.ffmpeg_inputs_max = 31
-
1
Util.ffprobe_cmd = ['ffprobe']
-
-
1
Util::ThreadedIoBuffer.blocks_max = 1024
-
1
Util::ThreadedIoBuffer.block_size = 64*1024
-
1
Util::ThreadedIoBuffer.timeout = 1
-
1
Util::ThreadedIoBuffer.timeout_limit = 15
-
# NOTE all this effectively sets the minimum throughput: blocks_max * blocks_size / timeout * timeout_limit
-
1
Util::ThreadedIoBuffer.io_wait_timeout = 1
-
-
1
Util::Thread.timeout = 15
-
-
-
# NOTE http://12factor.net etc
-
-
1
Ffmprb.log_level = Logger::INFO
-
1
Ffmprb.ffmpeg_debug = ENV.fetch('FFMPRB_FFMPEG_DEBUG', '') !~ Ffmprb::ENV_VAR_FALSE_REGEX
-
1
Ffmprb.debug = ENV.fetch('FFMPRB_DEBUG', '') !~ Ffmprb::ENV_VAR_FALSE_REGEX
-
-
1
proc_vis_firebase = ENV['FFMPRB_PROC_VIS_FIREBASE']
-
1
if Ffmprb::FIREBASE_AVAILABLE
-
1
fail Error, "Please provide just the name of the firebase in FFMPRB_PROC_VIS_FIREBASE (e.g. my-proc-vis-io for https://my-proc-vis-io.firebaseio.com/proc/)" if proc_vis_firebase =~ /\//
-
1
Ffmprb.proc_vis_firebase = proc_vis_firebase
-
elsif proc_vis_firebase
-
Ffmprb.logger.warn "Firebase unavailable (have firebase gem installed or unset FFMPRB_PROC_VIS_FIREBASE to get rid of this warning)"
-
end
-
-
end
-
1
require 'logger'
-
1
require 'ostruct'
-
1
require 'timeout'
-
-
# IMPORTANT NOTE ffmprb uses threads internally, however, it is not "thread-safe"
-
-
1
require_relative 'ffmprb/version'
-
1
require_relative 'ffmprb/util' # NOTE utils are like (micro-)gem candidates, errors are also there
-
-
1
module Ffmprb
-
-
1
ENV_VAR_FALSE_REGEX = /^(0|no?|false)?$/i
-
-
1
CGA = '320x200'
-
1
QVGA = '320x240'
-
1
HD_720p = '1280x720'
-
1
HD_1080p = '1920x1080'
-
-
1
class << self
-
-
# TODO limit:
-
1
def process(*args, name: nil, **opts, &blk)
-
56
fail Error, "process: nothing ;( gimme a block!" unless blk
-
-
168
name ||= blk.source_location.map(&:to_s).map{ |s| ::File.basename s.to_s, ::File.extname(s) }.join(':')
-
56
process = Process.new(name: name, **opts)
-
55
proc_vis_node process if respond_to? :proc_vis_node # XXX simply include the ProcVis if it makes into a gem
-
55
logger.debug{"Starting process with #{args} #{opts} in #{blk.source_location}"}
-
-
55
process.instance_exec *args, &blk
-
54
logger.debug{"Initialized process with #{args} #{opts} in #{blk.source_location}"}
-
-
54
process.run.tap do
-
48
logger.debug{"Finished process with #{args} #{opts} in #{blk.source_location}"}
-
end
-
end
-
1
alias :action! :process # ;)
-
-
1
attr_accessor :debug, :ffmpeg_debug, :log_level
-
-
1
def logger
-
@logger ||= Logger.new(STDERR).tap do |logger|
-
1
logger.level = debug ? Logger::DEBUG : Ffmprb.log_level
-
454979
end
-
end
-
-
1
def logger=(logger)
-
@logger.close if @logger
-
@logger = logger
-
end
-
-
end
-
-
1
include Util::ProcVis if FIREBASE_AVAILABLE
-
end
-
-
-
1
require_relative 'ffmprb/execution'
-
1
require_relative 'ffmprb/file'
-
1
require_relative 'ffmprb/filter'
-
1
require_relative 'ffmprb/find_silence'
-
1
require_relative 'ffmprb/process'
-
-
1
require_relative 'defaults'
-
1
require 'thor'
-
-
1
module Ffmprb
-
-
1
class Execution < Thor
-
-
1
def self.exit_on_failure?; true; end
-
-
1
class_option :debug, :type => :boolean, :default => false
-
1
class_option :verbose, :aliases => '-v', :type => :boolean, :default => false
-
1
class_option :quiet, :aliases => '-q', :type => :boolean, :default => false
-
-
1
default_task :process
-
-
1
desc :process, "Reads an ffmprb script from STDIN and carries it out. See #{GEM_GITHUB_URL}"
-
1
def process(*ios)
-
script = eval("lambda{#{STDIN.read}}")
-
Ffmprb.log_level =
-
if options[:debug]
-
Logger::DEBUG
-
elsif options[:verbose]
-
Logger::INFO
-
elsif options[:quiet]
-
Logger::ERROR
-
else
-
Logger::WARN
-
end
-
Ffmprb.process *ios, ignore_broken_pipes: false, &script
-
end
-
-
# NOTE a hack from http://stackoverflow.com/a/23955971/714287
-
1
def method_missing(method, *args)
-
args = [:process, method.to_s] + args
-
self.class.start(args)
-
end
-
-
end
-
-
end
-
1
require 'json'
-
1
require 'mkfifo'
-
1
require 'tempfile'
-
-
1
module Ffmprb
-
-
1
class File # NOTE I would rather rename it to Stream at the moment
-
1
include Util::ProcVis::Node
-
-
1
class << self
-
-
# NOTE careful when subclassing, it doesn't inherit the attr values
-
1
attr_accessor :image_extname_regex, :sound_extname_regex, :movie_extname_regex
-
-
# NOTE must be timeout-safe
-
1
def opener(file, mode=nil)
-
221
->{
-
279
path = file.respond_to?(:path)? file.path : file
-
279
mode ||= file.respond_to?(mode)? file.mode.to_s[0] : 'r'
-
279
Ffmprb.logger.debug{"Trying to open #{path} (for #{mode}-buffering or something)"}
-
279
::File.open(path, mode)
-
}
-
end
-
-
1
def create(path)
-
521
new(path: path, mode: :write).tap do |file|
-
521
Ffmprb.logger.debug{"Created file with path: #{file.path}"}
-
end
-
end
-
-
1
def open(path)
-
3
new(path: path, mode: :read).tap do |file|
-
3
Ffmprb.logger.debug{"Opened file with path: #{file.path}"}
-
end
-
end
-
-
1
def temp(extname)
-
292
file = create(Tempfile.new(['', extname]))
-
292
path = file.path
-
292
Ffmprb.logger.debug{"Created temp file with path: #{path}"}
-
-
292
return file unless block_given?
-
-
130
begin
-
130
yield file
-
ensure
-
130
begin
-
130
file.unlink
-
rescue
-
Ffmprb.logger.warn "#{$!.class.name} removing temp file with path #{path}: #{$!.message}"
-
end
-
130
Ffmprb.logger.debug{"Removed temp file with path: #{path}"}
-
end
-
end
-
-
1
def temp_fifo(extname='.tmp', &blk)
-
227
path = temp_fifo_path(extname)
-
227
::File.mkfifo path
-
227
fifo_file = create(path)
-
-
227
return fifo_file unless block_given?
-
-
3
path = fifo_file.path
-
3
begin
-
3
yield fifo_file
-
ensure
-
3
begin
-
3
fifo_file.unlink
-
rescue
-
Ffmprb.logger.warn "#{$!.class.name} removing temp file with path #{path}: #{$!.message}"
-
end
-
3
Ffmprb.logger.debug{"Removed temp file with path: #{path}"}
-
end
-
end
-
-
1
def temp_fifo_path(extname)
-
227
::File.join Dir.tmpdir, "#{rand(2**222)}p#{extname}"
-
end
-
-
1
def image?(extname)
-
880
!!(extname =~ File.image_extname_regex)
-
end
-
-
1
def sound?(extname)
-
960
!!(extname =~ File.sound_extname_regex)
-
end
-
-
1
def movie?(extname)
-
1394
!!(extname =~ File.movie_extname_regex)
-
end
-
-
end
-
-
1
attr_reader :mode
-
-
1
def initialize(path:, mode:)
-
524
@mode = mode.to_sym
-
524
fail Error, "Open for read, create for write, ??? for #{@mode}" unless %i[read write].include?(@mode)
-
524
@path = path
-
524
@path.close if @path && @path.respond_to?(:close) # NOTE we operate on closed files
-
524
path! # NOTE early (exception) raiser
-
end
-
-
1
def label
-
basename
-
end
-
-
1
def path
-
2353
path!
-
end
-
-
# Info
-
-
1
def exist?
-
5
::File.exist? path
-
end
-
-
1
def basename
-
@basename ||= ::File.basename(path)
-
end
-
-
1
def extname
-
3248
@extname ||= ::File.extname(path)
-
end
-
-
1
def channel?(medium)
-
1840
case medium
-
when :video
-
880
self.class.image?(extname) || self.class.movie?(extname)
-
when :audio
-
960
self.class.sound?(extname) || self.class.movie?(extname)
-
end
-
end
-
-
1
def length(force=false)
-
40
@duration = nil if force
-
40
return @duration if @duration
-
-
# NOTE first attempt
-
37
@duration = probe(force)['format']['duration']
-
37
@duration &&= @duration.to_f
-
37
return @duration if @duration
-
-
# NOTE a harder try
-
@duration = probe(true)['frames'].reduce(0) do |sum, frame|
-
sum + frame['pkt_duration_time'].to_f
-
end
-
end
-
-
1
def resolution
-
5
v_stream = probe['streams'].first
-
5
"#{v_stream['width']}x#{v_stream['height']}"
-
end
-
-
-
# Manipulation
-
-
1
def read
-
::File.read path
-
end
-
1
def write(s)
-
2
::File.write path, s
-
end
-
-
1
def unlink
-
306
if path.respond_to? :unlink
-
path.unlink
-
else
-
306
FileUtils.remove_entry path
-
end
-
306
Ffmprb.logger.debug{"Removed file with path: #{path}"}
-
306
@path = nil
-
end
-
-
1
private
-
-
1
def path!
-
(
-
2877
@path.respond_to?(:path)? @path.path : @path
-
2877
).tap do |path|
-
# TODO ensure readabilty/writability/readiness
-
2877
fail Error, "'#{path}' is un#{@mode.to_s[0..3]}able" unless path && !path.empty?
-
end
-
end
-
-
1
def probe(harder=false)
-
42
return @probe unless !@probe || harder
-
37
cmd = ['-v', 'quiet', '-i', path, '-print_format', 'json', '-show_format', '-show_streams']
-
37
cmd << '-show_frames' if harder
-
37
@probe = JSON.parse(Util::ffprobe *cmd).tap do |probe|
-
37
fail Error, "This doesn't look like a ffprobable file" unless probe['streams']
-
end
-
end
-
-
end
-
-
end
-
-
1
require_relative 'file/sample'
-
1
require_relative 'file/threaded_buffered'
-
1
module Ffmprb
-
-
1
class File
-
-
1
def sample(
-
at: 0.01,
-
video: true,
-
audio: true,
-
&blk
-
)
-
92
audio = File.temp('.wav') if audio == true
-
92
video = File.temp('.png') if video == true
-
-
92
Ffmprb.logger.debug{"Snap shooting files, video path: #{video ? video.path : 'NONE'}, audio path: #{audio ? audio.path : 'NONE'}"}
-
-
92
fail Error, "Incorrect output extname (must be image)" unless !video || video.channel?(:video) && !video.channel?(:audio)
-
92
fail Error, "Incorrect audio extname (must be sound)" unless !audio || audio.channel?(:audio) && !audio.channel?(:video)
-
92
fail Error, "Can sample either video OR audio UNLESS a block is given" unless block_given? || !!audio != !!video
-
-
92
cmd = %W[-i #{path}]
-
92
cmd.concat %W[-deinterlace -an -ss #{at} -vframes 1 #{video.path}] if video
-
92
cmd.concat %W[-vn -ss #{at} -t 1 #{audio.path}] if audio
-
92
Util.ffmpeg *cmd
-
-
79
return video || audio unless block_given?
-
-
63
begin
-
63
yield *[video || nil, audio || nil].compact
-
ensure
-
63
begin
-
63
video.unlink if video
-
63
audio.unlink if audio
-
63
Ffmprb.logger.debug{"Removed sample files"}
-
rescue
-
Ffmprb.logger.warn "#{$!.class.name} removing sample files: #{$!.message}"
-
end
-
end
-
end
-
1
def sample_video(*video, at: 0.01, &blk)
-
sample at: at, video: (video.first || true), audio: false, &blk
-
end
-
1
def sample_audio(*audio, at: 0.01, &blk)
-
12
sample at: at, video: false, audio: (audio.first || true), &blk
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class File # NOTE I would rather rename it to Stream at the moment
-
-
1
class << self
-
-
1
def threaded_buffered_fifo(extname='.tmp', reader_open_on_writer_idle_limit: nil, proc_vis: nil)
-
14
input_fifo_file = temp_fifo(extname)
-
14
output_fifo_file = temp_fifo(extname)
-
14
Ffmprb.logger.debug{"Opening #{input_fifo_file.path}>#{output_fifo_file.path} for buffering"}
-
14
Util::Thread.new do
-
14
begin
-
14
io_buff = Util::ThreadedIoBuffer.new(opener(input_fifo_file, 'r'), opener(output_fifo_file, 'w'), keep_outputs_open_on_input_idle_limit: reader_open_on_writer_idle_limit)
-
14
if proc_vis
-
9
proc_vis.proc_vis_edge input_fifo_file, io_buff
-
9
proc_vis.proc_vis_edge io_buff, output_fifo_file
-
end
-
14
begin
-
# yield input_fifo_file, output_fifo_file, io_buff if block_given?
-
ensure
-
14
Util::Thread.join_children!
-
end
-
13
Ffmprb.logger.debug{"IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} ended"}
-
ensure
-
14
input_fifo_file.unlink if input_fifo_file
-
14
output_fifo_file.unlink if output_fifo_file
-
end
-
end
-
14
Ffmprb.logger.debug{"IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} started"}
-
-
14
[input_fifo_file, output_fifo_file]
-
end
-
-
end
-
-
1
def threaded_buffered_copy_to(*dsts)
-
Util::ThreadedIoBuffer.new(
-
self.class.opener(self, 'r'),
-
178
*dsts.map{|io| self.class.opener io, 'w'}
-
15
).tap do |io_buff|
-
15
proc_vis_edge self, io_buff
-
193
dsts.each{ |dst| proc_vis_edge io_buff, dst }
-
end
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
module Filter
-
-
1
class Error < Ffmprb::Error; end
-
-
1
class << self
-
-
1
attr_accessor :silence_noise_max_db
-
-
1
def alphamerge(inputs, output=nil)
-
6
inout "alphamerge", inputs, output
-
end
-
-
1
def afade_in(duration, input=nil, output=nil)
-
7
inout "afade=in:d=%{duration}:curve=hsin", input, output, duration: duration
-
end
-
-
1
def afade_out(duration, input=nil, output=nil)
-
7
inout "afade=out:d=%{duration}:curve=hsin", input, output, duration: duration
-
end
-
-
1
def amix_to_first_same_volume(inputs, output=nil)
-
12
filters = []
-
12
new_inputs = inputs.map do |input|
-
24
if input == inputs.first
-
12
input
-
else
-
12
"apd#{input}".tap do |lbl_aux|
-
12
filters +=
-
inout("apad", input, lbl_aux) # NOTE we'll see if we really need this filter separate
-
end
-
end
-
end
-
filters +
-
inout("amix=%{inputs_count}:duration=shortest:dropout_transition=0, volume=%{inputs_count}",
-
12
new_inputs, output, inputs_count: (inputs.empty?? nil : inputs.size))
-
end
-
-
1
def anull(input=nil, output=nil)
-
235
inout "anull", input, output
-
end
-
-
1
def anullsink(input=nil)
-
inout "anullsink", input, nil
-
end
-
-
1
def asplit(inputs=nil, outputs=nil)
-
2
inout "asplit", inputs, outputs
-
end
-
-
1
def atrim(st, en=nil, input=nil, output=nil)
-
42
inout "atrim=%{start_end}, asetpts=PTS-STARTPTS", input, output,
-
start_end: [st, en].compact.join(':')
-
end
-
-
1
def blank_source(duration, resolution, fps, output=nil)
-
35
color_source '0x000000@0', duration, resolution, fps, output
-
end
-
-
1
def color_source(color, duration, resolution, fps, output=nil)
-
41
inout "color=%{color}:d=%{duration}:s=%{resolution}:r=%{fps}", nil, output,
-
color: color, duration: duration, resolution: resolution, fps: fps
-
end
-
-
1
def fade_out_alpha(duration, input=nil, output=nil)
-
6
inout "fade=out:d=%{duration}:alpha=1", input, output, duration: duration
-
end
-
-
1
def fps(fps, input=nil, output=nil)
-
52
inout "fps=fps=%{fps}", input, output, fps: fps
-
end
-
-
1
def concat_v(inputs, output=nil)
-
52
return copy(inputs, output) if inputs.size == 1
-
52
inout "concat=#{inputs.size}:v=1:a=0", inputs, output
-
end
-
-
1
def concat_a(inputs, output=nil)
-
56
return anull(inputs, output) if inputs.size == 1
-
56
inout "concat=#{inputs.size}:v=0:a=1", inputs, output
-
end
-
-
1
def concat_av(inputs, output=nil)
-
fail Error, "must be given an even number of inputs" unless inputs.size.even?
-
inout "concat=#{inputs.size/2}:v=1:a=1", inputs, output
-
end
-
-
1
def copy(input=nil, output=nil)
-
109
inout "copy", input, output
-
end
-
-
# TODO unused at the moment
-
1
def crop(crop, input=nil, output=nil)
-
inout "crop=x=%{left}:y=%{top}:w=%{width}:h=%{height}", input, output, crop
-
end
-
-
1
def crop_prop(crop, input=nil, output=nil)
-
8
inout "crop=%{crop_exp}", input, output,
-
crop_exp: crop_prop_exps(crop).join(':')
-
end
-
-
1
def crop_prop_exps(crop)
-
8
exps = []
-
-
8
if crop[:left]
-
7
exps << "x=in_w*#{crop[:left]}"
-
end
-
-
8
if crop[:top]
-
7
exps << "y=in_h*#{crop[:top]}"
-
end
-
-
8
if crop[:right] && crop[:left]
-
5
fail Error, "Must specify two of {left, right, width} at most" if crop[:width]
-
5
crop[:width] = 1 - crop[:right] - crop[:left]
-
3
elsif crop[:width]
-
3
if !crop[:left] && crop[:right]
-
crop[:left] = 1 - crop[:width] - crop[:right]
-
exps << "x=in_w*#{crop[:left]}"
-
end
-
end
-
8
exps << "w=in_w*#{crop[:width]}"
-
-
8
if crop[:bottom] && crop[:top]
-
5
fail Error, "Must specify two of {top, bottom, height} at most" if crop[:height]
-
5
crop[:height] = 1 - crop[:bottom] - crop[:top]
-
3
elsif crop[:height]
-
3
if !crop[:top] && crop[:bottom]
-
crop[:top] = 1 - crop[:height] - crop[:bottom]
-
exps << "y=in_h*#{crop[:top]}"
-
end
-
end
-
8
exps << "h=in_h*#{crop[:height]}"
-
-
8
exps
-
end
-
-
# NOTE might be very useful with UGC: def cropdetect
-
-
1
def nullsink(input=nil)
-
inout "nullsink", input, nil
-
end
-
-
1
def overlay(x=0, y=0, inputs=nil, output=nil)
-
6
inout "overlay=x=%{x}:y=%{y}:eof_action=pass", inputs, output, x: x, y: y
-
end
-
-
1
def pad(resolution, input=nil, output=nil)
-
52
width, height = resolution.to_s.split('x')
-
52
inout [
-
inout("pad=%{width}:%{height}:(%{width}-iw*min(%{width}/iw\\,%{height}/ih))/2:(%{height}-ih*min(%{width}/iw\\,%{height}/ih))/2",
-
width: width, height: height),
-
*setsar(1) # NOTE the scale & pad formulae damage SAR a little, unfortunately
-
].join(', '), input, output
-
end
-
-
1
def setsar(ratio, input=nil, output=nil)
-
104
inout "setsar=%{ratio}", input, output, ratio: ratio
-
end
-
-
1
def scale(resolution, input=nil, output=nil)
-
52
width, height = resolution.to_s.split('x')
-
52
inout [
-
inout("scale=iw*min(%{width}/iw\\,%{height}/ih):ih*min(%{width}/iw\\,%{height}/ih)", width: width, height: height),
-
*setsar(1) # NOTE the scale & pad formulae damage SAR a little, unfortunately
-
].join(', '), input, output
-
end
-
-
1
def scale_pad(resolution, input=nil, output=nil)
-
52
inout [
-
*scale(resolution),
-
*pad(resolution)
-
].join(', '), input, output
-
end
-
-
1
def scale_pad_fps(resolution, _fps, input=nil, output=nil)
-
44
inout [
-
*scale_pad(resolution),
-
*fps(_fps)
-
].join(', '), input, output
-
end
-
-
1
def silencedetect(input=nil, output=nil)
-
4
inout "silencedetect=d=1:n=%{silence_noise_max_db}dB", input, output,
-
silence_noise_max_db: silence_noise_max_db
-
end
-
-
1
def silent_source(duration, output=nil)
-
38
inout "aevalsrc=0:d=%{duration}", nil, output, duration: duration
-
end
-
-
# NOTE might be very useful with transitions: def smartblur
-
-
1
def split(inputs=nil, outputs=nil)
-
2
inout "split", inputs, outputs
-
end
-
-
1
def blend_v(duration, resolution, fps, inputs, output=nil)
-
6
fail Error, "must be given 2 inputs" unless inputs.size == 2
-
-
6
aux_lbl = "blnd#{inputs[0]}"
-
6
auxx_lbl = "x#{aux_lbl}"
-
[
-
*white_source(duration, resolution, fps, aux_lbl),
-
*inout([
-
*alphamerge([inputs[0], aux_lbl]),
-
*fade_out_alpha(duration)
-
].join(', '), nil, auxx_lbl),
-
6
*overlay(0, 0, [inputs[1], auxx_lbl], output),
-
]
-
end
-
-
1
def blend_a(duration, inputs, output=nil)
-
7
fail Error, "must be given 2 inputs" unless inputs.size == 2
-
-
7
aux_lbl = "blnd#{inputs[0]}"
-
7
auxx_lbl = "x#{aux_lbl}"
-
[
-
*afade_out(duration, inputs[0], aux_lbl),
-
*afade_in(duration, inputs[1], auxx_lbl),
-
7
*amix_to_first_same_volume([auxx_lbl, aux_lbl], output)
-
]
-
end
-
-
1
def trim(st, en=nil, input=nil, output=nil)
-
38
inout "trim=%{start_end}, setpts=PTS-STARTPTS", input, output,
-
start_end: [st, en].compact.join(':')
-
end
-
-
1
def volume(volume, input=nil, output=nil)
-
10
inout "volume='%{volume_exp}':eval=frame", input, output,
-
volume_exp: volume_exp(volume)
-
end
-
-
# NOTE supposedly volume list is sorted
-
1
def volume_exp(volume)
-
10
return volume unless volume.is_a?(Hash)
-
-
6
fail Error, "volume cannot be empty" if volume.empty?
-
-
6
prev_at = 0.0
-
6
prev_vol = volume[prev_at] || 1.0
-
6
exp = "#{volume[volume.keys.last]}"
-
6
volume.each do |at, vol|
-
30
next if at == 0.0
-
26
vol_exp =
-
26
if (vol - prev_vol).abs < 0.001
-
10
vol
-
else
-
16
"(#{vol - prev_vol}*t + #{prev_vol*at - vol*prev_at})/#{at - prev_at}"
-
end
-
26
exp = "if(between(t, #{prev_at}, #{at}), #{vol_exp}, #{exp})"
-
26
prev_at = at
-
26
prev_vol = vol
-
end
-
6
exp
-
end
-
-
1
def white_source(duration, resolution, fps, output=nil)
-
6
color_source '0xFFFFFF@1', duration, resolution, fps, output
-
end
-
-
1
def complex_args(*filters)
-
78
[].tap do |args|
-
args << '-filter_complex' << filters.join('; ') unless
-
78
filters.empty?
-
end
-
end
-
-
1
private
-
-
1
def inout(filter, inputs=nil, outputs=nil, **values)
-
1159
values.each do |key, value|
-
712
fail Error, "#{filter} needs #{key}" if value.to_s.empty?
-
end
-
1159
filter = filter % values
-
2193
filter = "#{[*inputs].map{|s| "[#{s}]"}.join ' '} " + filter if inputs
-
1906
filter = filter + " #{[*outputs].map{|s| "[#{s}]"}.join ' '}" if outputs
-
1159
[filter]
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class << self
-
-
# NOTE not for streaming just yet
-
1
def find_silence(input_file, output_file)
-
4
path = "#{input_file.path}->#{output_file.path}"
-
4
logger.debug{"Finding silence (#{path})"}
-
4
silence = []
-
Util.ffmpeg('-i', input_file.path, *find_silence_detect_args, output_file.path).
-
4
scan(SILENCE_DETECT_REGEX).each do |mark, time|
-
12
time = time.to_f
-
-
12
case mark
-
when 'start'
-
7
silence << OpenStruct.new(start_at: time)
-
when 'end'
-
5
if silence.empty?
-
silence << OpenStruct.new(start_at: 0.0, end_at: time)
-
else
-
5
fail Error, "ffmpeg is being stupid: silence_end with no silence_start" if silence.last.end_at
-
5
silence.last.end_at = time
-
end
-
else
-
Ffmprb.warn "Unknown silence mark: #{mark}"
-
end
-
end
-
4
logger.debug{
-
silence_map = silence.map{|t,v| "#{t}: #{v}"}
-
"Found silence (#{path}): [#{silence_map}]"
-
}
-
4
silence
-
end
-
-
1
private
-
-
1
SILENCE_DETECT_REGEX = /\[silencedetect\s.*\]\s*silence_(\w+):\s*(\d+\.?\d*)/
-
-
1
def find_silence_detect_args
-
4
Filter.complex_args Filter.silencedetect
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
1
include Util::ProcVis::Node
-
-
1
class << self
-
-
1
attr_accessor :duck_audio_volume_hi, :duck_audio_volume_lo,
-
:duck_audio_silent_min
-
1
attr_accessor :duck_audio_transition_length,
-
:duck_audio_transition_in_start, :duck_audio_transition_out_start
-
-
1
attr_accessor :input_video_auto_rotate
-
1
attr_accessor :input_video_fps
-
-
1
attr_accessor :output_video_resolution
-
1
attr_accessor :output_video_fps
-
1
attr_accessor :output_audio_encoder
-
1
attr_accessor :output_audio_sampling_freq
-
-
1
attr_accessor :timeout
-
-
1
def intermediate_channel_extname(video:, audio:)
-
12
if video
-
6
if audio
-
6
'.flv' # TODO optimise this by using http://superuser.com/a/522853 or something
-
else
-
'.y4m'
-
end
-
else
-
6
if audio
-
6
'.wav'
-
else
-
fail Error, "I don't know how to channel [#{media.join ', '}]"
-
end
-
end
-
end
-
-
1
def input_video_options
-
{
-
auto_rotate: input_video_auto_rotate,
-
fps: input_video_fps # TODO seen failing on apng (w/ffmpeg v4.x)
-
393
}
-
end
-
1
def input_audio_options
-
{
-
469
}
-
end
-
1
def output_video_options
-
{
-
fps: output_video_fps,
-
resolution: output_video_resolution
-
98
}
-
end
-
1
def output_audio_options
-
{
-
encoder: output_audio_encoder,
-
sampling_freq: output_audio_sampling_freq
-
118
}
-
end
-
-
# NOTE Temporarily, av_main_i/o and not a_main_i/o
-
1
def duck_audio(av_main_i, a_overlay_i, silence, av_main_o,
-
volume_lo: duck_audio_volume_lo,
-
volume_hi: duck_audio_volume_hi,
-
silent_min: duck_audio_silent_min,
-
process_options: {},
-
video:, # NOTE Temporarily, video should not be here
-
audio:
-
)
-
3
Ffmprb.process **process_options do
-
-
3
in_main = input(av_main_i)
-
3
in_over = input(a_overlay_i)
-
3
output(av_main_o, video: video, audio: audio) do
-
3
roll in_main
-
-
3
ducked_overlay_volume = {0.0 => volume_lo}
-
3
silence.each do |silent|
-
5
next if silent.end_at && silent.start_at && (silent.end_at - silent.start_at) < silent_min
-
-
5
if silent.start_at
-
5
transition_in_start = silent.start_at + Process.duck_audio_transition_in_start
-
5
ducked_overlay_volume.merge!(
-
[transition_in_start, 0.0].max => volume_lo,
-
5
(transition_in_start + Process.duck_audio_transition_length) => volume_hi
-
)
-
end
-
-
5
if silent.end_at
-
4
transition_out_start = silent.end_at + Process.duck_audio_transition_out_start
-
4
ducked_overlay_volume.merge!(
-
[transition_out_start, 0.0].max => volume_hi,
-
4
(transition_out_start + Process.duck_audio_transition_length) => volume_lo
-
)
-
end
-
end
-
3
overlay in_over.volume ducked_overlay_volume
-
-
3
Ffmprb.logger.debug{
-
ducked_overlay_volume_map = ducked_overlay_volume.map{|t,v| "#{t}: #{v}"}
-
"Ducking audio with volumes: {#{ducked_overlay_volume_map.join ', '}}"
-
}
-
end
-
-
end
-
end
-
-
end
-
-
1
attr_accessor :timeout
-
1
attr_accessor :name
-
1
attr_reader :parent
-
1
attr_accessor :ignore_broken_pipes
-
-
1
def initialize(*args, **opts)
-
56
self.timeout = opts.delete(:timeout) || Process.timeout
-
56
@name = opts.delete(:name)
-
56
@parent = opts.delete(:parent)
-
56
parent.proc_vis_node self if parent
-
56
self.ignore_broken_pipes = opts.delete(:ignore_broken_pipes)
-
56
Util.assert_options_empty! opts
-
55
@inputs, @outputs = [], []
-
end
-
-
1
def input(io, video: true, audio: true)
-
Input.new(io, self,
-
video: channel_params(video, Process.input_video_options),
-
audio: channel_params(audio, Process.input_audio_options)
-
253
).tap do |inp|
-
253
fail Error, "Too many inputs to the process, try breaking it down somehow" if @inputs.size > Util.ffmpeg_inputs_max
-
252
@inputs << inp
-
252
proc_vis_edge inp.io, self
-
end
-
end
-
-
1
def input_label(input)
-
240
@inputs.index input
-
end
-
-
1
def output(io, video: true, audio: true, &blk)
-
Output.new(io, self,
-
video: channel_params(video, Process.output_video_options),
-
audio: channel_params(audio, Process.output_audio_options)
-
55
).tap do |outp|
-
55
@outputs << outp
-
55
proc_vis_edge self, outp.io
-
55
outp.instance_exec &blk if blk
-
end
-
end
-
-
1
def output_index(output)
-
55
@outputs.index output
-
end
-
-
# NOTE the one and the only entry-point processing function which spawns threads etc
-
1
def run(limit: nil) # TODO (async: false)
-
# NOTE this is both for the future async: option and according to
-
# the threading policy (a parent death will be noticed and handled by children)
-
54
thr = Util::Thread.new main: !parent do
-
54
proc_vis_node Thread.current
-
# NOTE yes, an exception can occur anytime, and we'll just die, it's ok, see above
-
54
cmd = command
-
54
opts = {limit: limit, timeout: timeout}
-
54
opts[:ignore_broken_pipes] = ignore_broken_pipes unless ignore_broken_pipes.nil?
-
54
Util.ffmpeg(*cmd, **opts).tap do |res|
-
48
Util::Thread.join_children! limit, timeout: timeout
-
end
-
48
proc_vis_node Thread.current, :remove
-
end
-
54
thr.value if thr.join limit # NOTE should not block for more than limit
-
end
-
-
1
private
-
-
1
def command
-
54
input_args + filter_args + output_args
-
end
-
-
1
def input_args
-
54
filter_args # NOTE must run first
-
54
@input_args ||= @inputs.map(&:args).reduce(:+)
-
end
-
-
# NOTE must run first
-
1
def filter_args
-
@filter_args ||= Filter.complex_args(
-
@outputs.map(&:filters).reduce(:+)
-
162
)
-
end
-
-
1
def output_args
-
54
filter_args # NOTE must run first
-
54
@output_args ||= @outputs.map(&:args).reduce(:+)
-
end
-
-
1
def channel_params(value, default)
-
616
if value
-
590
default.merge(value == true ? {} : value.to_h)
-
26
elsif value != false
-
18
{}
-
end
-
end
-
end
-
-
end
-
-
1
require_relative 'process/input'
-
1
require_relative 'process/output'
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
class << self
-
-
1
def resolve(io)
-
253
return io unless io.is_a? String # XXX XXX
-
-
3
File.open(io).tap do |file|
-
3
Ffmprb.logger.warn "Input file does no exist (#{file.path}), will probably fail" unless file.exist?
-
end
-
end
-
-
# TODO! check for unknown options
-
-
1
def video_args(video=nil)
-
140
video = Process.input_video_options.merge(video.to_h)
-
140
[].tap do |args|
-
140
fps = nil # NOTE ah, ruby
-
140
args.concat %W[-noautorotate] unless video.delete(:auto_rotate)
-
140
args.concat %W[-r #{fps}] if (fps = video.delete(:fps))
-
140
Util.assert_options_empty! video
-
end
-
end
-
-
1
def audio_args(audio=nil)
-
216
audio = Process.input_audio_options.merge(audio.to_h)
-
216
[].tap do |args|
-
216
Util.assert_options_empty! audio
-
end
-
end
-
-
end
-
-
1
attr_accessor :io
-
1
attr_reader :process
-
-
1
def initialize(io, process, video:, audio:)
-
253
@io = self.class.resolve(io)
-
253
@process = process
-
253
@channels = {
-
video: video && @io.channel?(:video) && OpenStruct.new(video),
-
audio: audio && @io.channel?(:audio) && OpenStruct.new(audio)
-
}
-
end
-
-
-
1
def copy(input)
-
6
input.chain_copy self
-
end
-
-
-
1
def args
-
220
[].tap do |args|
-
220
args.concat self.class.video_args(channel :video) if channel? :video
-
220
args.concat self.class.audio_args(channel :audio) if channel? :audio
-
220
args.concat ['-i', io.path]
-
end
-
end
-
-
1
def filters_for(lbl, video:, audio:)
-
240
in_lbl = process.input_label(self)
-
[
-
240
*(if video && channel?(:video)
-
152
if video.resolution && video.fps
-
44
Filter.scale_pad_fps video.resolution, video.fps, "#{in_lbl}:v", "#{lbl}:v"
-
108
elsif video.resolution
-
Filter.scale_pad video.resolution, "#{in_lbl}:v", "#{lbl}:v"
-
108
elsif video.fps
-
8
Filter.fps video.fps, "#{in_lbl}:v", "#{lbl}:v"
-
else
-
100
Filter.copy "#{in_lbl}:v", "#{lbl}:v"
-
end
-
88
elsif video
-
fail Error, "No video stream to provide"
-
end),
-
240
*(if audio && channel?(:audio)
-
227
Filter.anull "#{in_lbl}:a", "#{lbl}:a"
-
13
elsif audio
-
fail Error, "No audio stream to provide"
-
240
end)
-
]
-
end
-
-
1
def channel?(medium)
-
1069
io.channel? medium
-
end
-
-
1
def channel(medium)
-
356
@channels[medium]
-
end
-
-
-
1
def chain_copy(src_input)
-
6
src_input
-
end
-
-
end
-
-
end
-
-
end
-
-
1
require_relative 'input/chain_base'
-
1
require_relative 'input/channeled'
-
1
require_relative 'input/cropped'
-
1
require_relative 'input/cut'
-
1
require_relative 'input/looping'
-
1
require_relative 'input/loud'
-
1
require_relative 'input/temp'
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
class ChainBase < Input
-
-
1
def initialize(unfiltered)
-
75
@io = unfiltered
-
end
-
-
74
def unfiltered; @io; end
-
3
def unfiltered=(input); @io = input; end
-
-
-
1
def chain_copy(src_input) # XXX SPEC ME
-
2
dup.tap do |top|
-
2
top.unfiltered = unfiltered.chain_copy(src_input)
-
end
-
end
-
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
def video
-
4
Channeled.new self, audio: false
-
end
-
-
1
def audio
-
4
Channeled.new self, video: false
-
end
-
-
1
class Channeled < ChainBase
-
-
1
def initialize(unfiltered, video: true, audio: true)
-
8
super unfiltered
-
8
@limited_channels = {video: video, audio: audio}
-
end
-
-
1
def channel(medium)
-
super(medium) if @limited_channels[medium]
-
end
-
-
1
def filters_for(lbl, video:, audio:)
-
-
# Doing nothing
-
-
8
unfiltered.filters_for lbl,
-
video: channel?(:video) && video, audio: channel?(:audio) && audio
-
end
-
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
def crop(ratio) # NOTE ratio is either a CROP_PARAMS symbol-ratio hash or a single (global) ratio
-
8
Cropped.new self, crop: ratio
-
end
-
-
1
class Cropped < ChainBase
-
-
1
attr_reader :ratios
-
-
1
def initialize(unfiltered, crop:)
-
8
super unfiltered
-
8
self.ratios = crop
-
end
-
-
1
def filters_for(lbl, video:, audio:)
-
-
# Cropping
-
-
8
lbl_aux = "cp#{lbl}"
-
8
lbl_tmp = "tmp#{lbl}"
-
unfiltered.filters_for(lbl_aux, video: unsize(video), audio: audio) +
-
[
-
8
*((video && channel?(:video))? [
-
Filter.crop_prop(ratios, "#{lbl_aux}:v", "#{lbl_tmp}:v"),
-
Filter.scale_pad(video.resolution, "#{lbl_tmp}:v", "#{lbl}:v")
-
]: nil),
-
8
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
-
8
]
-
end
-
-
1
private
-
-
1
CROP_PARAMS = %i[top left bottom right width height]
-
-
1
def unsize(video)
-
8
fail Error, "requires resolution" unless video.resolution
-
8
OpenStruct.new(video).tap do |video|
-
8
video.resolution = nil
-
end
-
end
-
-
1
def ratios=(ratios)
-
8
@ratios =
-
8
if ratios.is_a?(Numeric)
-
5
{top: ratios, left: ratios, bottom: ratios, right: ratios}
-
else
-
3
ratios
-
end.tap do |ratios| # NOTE validation
-
fail "Allowed crop params are: #{CROP_PARAMS}" unless
-
8
ratios && ratios.respond_to?(:keys) && (ratios.keys - CROP_PARAMS).empty?
-
-
8
ratios.each do |key, value|
-
fail Error, "Crop #{key} must be between 0 and 1 (not '#{value}')" unless
-
30
(0...1).include? value
-
end
-
end
-
end
-
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
def cut(from: 0, to: nil)
-
43
Cut.new self, from: from, to: to
-
end
-
-
1
class Cut < ChainBase
-
-
1
attr_reader :from, :to
-
-
1
def initialize(unfiltered, from:, to:)
-
43
super unfiltered
-
43
@from = from
-
43
@to = to.to_f == 0 ? nil : to
-
-
43
fail Error, "cut from: must be" unless from
-
43
fail Error, "cut from: must be less than to:" unless !to || from < to
-
end
-
-
1
def filters_for(lbl, video:, audio:)
-
fail Error, "cut needs resolution and fps (reorder your filters?)" unless
-
43
!video || video.resolution && video.fps
-
-
# Trimming
-
-
43
lbl_aux = "tm#{lbl}"
-
unfiltered.filters_for(lbl_aux, video: video, audio: audio) +
-
43
if to
-
41
lbl_blk = "bl#{lbl}"
-
41
lbl_pad = "pd#{lbl}"
-
[
-
41
*((video && channel?(:video))?
-
Filter.blank_source(to - from, video.resolution, video.fps, "#{lbl_blk}:v") +
-
Filter.concat_v(["#{lbl_aux}:v", "#{lbl_blk}:v"], "#{lbl_pad}:v") +
-
Filter.trim(from, to, "#{lbl_pad}:v", "#{lbl}:v")
-
: nil),
-
41
*((audio && channel?(:audio))?
-
Filter.silent_source(to - from, "#{lbl_blk}:a") +
-
Filter.concat_a(["#{lbl_aux}:a", "#{lbl_blk}:a"], "#{lbl_pad}:a") +
-
Filter.atrim(from, to, "#{lbl_pad}:a", "#{lbl}:a")
-
41
: nil)
-
]
-
2
elsif from == 0
-
[
-
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
-
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
-
]
-
else # !to
-
[
-
2
*((video && channel?(:video))? Filter.trim(from, nil, "#{lbl_aux}:v", "#{lbl}:v"): nil),
-
4
*((audio && channel?(:audio))? Filter.atrim(from, nil, "#{lbl_aux}:a", "#{lbl}:a"): nil)
-
]
-
43
end
-
end
-
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
def loop(times=Util.ffmpeg_inputs_max)
-
6
Ffmprb.logger.warn "Looping more than #{Util.ffmpeg_inputs_max} times is 'unstable': either use double looping or ask for this feature" if times > Util.ffmpeg_inputs_max
-
6
Looping.new self, times
-
end
-
-
1
class Looping < ChainBase
-
-
1
attr_reader :times
-
-
1
def initialize(unfiltered, times)
-
6
super unfiltered
-
-
6
@times = times
-
-
6
@raw = @_unfiltered = unfiltered
-
# NOTE find the actual input io (not a filter)
-
6
@raw = @raw.unfiltered while @raw.respond_to? :unfiltered
-
end
-
-
1
def filters_for(lbl, video:, audio:)
-
-
# The plan:
-
# 1) Create and route an aux input which would hold the filtered, looped and parameterised stream off the raw input (keep the raw input)
-
# 2) Tee+buffer the original raw input io: one stream goes back into the process throw the raw input io replacement fifo; the other is fed into the filtering process
-
# 3) Which uses the same underlying filters to produce a filtered and parameterised stream, which is fed into the looping process through a N-Tee+buffer
-
# 4) Invoke the looping process which just concatenates its N inputs and produces the new raw input (the aux input)
-
-
# Looping
-
# NOTE all the processing is done before looping
-
-
6
aux_input(video: video, audio: audio).filters_for lbl,
-
video: video && OpenStruct.new, audio: audio && OpenStruct.new
-
end
-
-
1
protected
-
-
1
def aux_input(video:, audio:)
-
-
# NOTE (2)
-
# NOTE replace the raw input io with a copy io, getting original fifo/file
-
6
intermediate_extname = Process.intermediate_channel_extname(video: @raw.io.channel?(:video), audio: @raw.io.channel?(:audio))
-
6
src_io = @raw.temporise_io!(intermediate_extname)
-
6
if src_io.extname != intermediate_extname # NOTE kinda like src_io is not suitable for piping
-
3
meh_src_io, src_io = src_io, File.temp_fifo(intermediate_extname)
-
3
Util::Thread.new "source converter" do
-
3
Ffmprb.process do
-
-
3
inp = input(meh_src_io)
-
3
output(src_io, video: nil, audio: nil) do # XXX this is not properly tested, unfortunately
-
3
lay inp
-
end
-
-
end
-
end
-
end
-
6
cpy_io = File.temp_fifo(src_io.extname)
-
6
Ffmprb.logger.debug{"(L2) Temporising the raw input (#{src_io.path}) and creating copy (#{cpy_io.path})"}
-
-
6
src_io.threaded_buffered_copy_to @raw.io, cpy_io
-
-
# NOTE (3)
-
# NOTE preprocessed and filtered fifo
-
6
dst_io = File.temp_fifo(intermediate_extname)
-
6
@raw.process.proc_vis_node dst_io
-
-
6
Util::Thread.new "looping input processor" do
-
6
Ffmprb.logger.debug{"(L3) Pre-processing into (#{dst_io.path})"}
-
-
6
Ffmprb.process @_unfiltered, parent: @raw.process do |unfiltered| # TODO limit:
-
-
6
inp = input(cpy_io)
-
6
output(dst_io, video: video, audio: audio) do
-
6
lay inp.copy(unfiltered)
-
end
-
-
end
-
end
-
-
163
buff_ios = (1..times).map{File.temp_fifo intermediate_extname}
-
6
Ffmprb.logger.debug{"Preprocessed #{dst_io.path} will be teed to #{buff_ios.map(&:path).join '; '}"}
-
6
Util::Thread.new "cloning buffer watcher" do
-
6
dst_io.threaded_buffered_copy_to(*buff_ios).tap do |io_buff|
-
6
Util::Thread.join_children!
-
6
Ffmprb.logger.warn "Looping ~from #{src_io.path} finished before its consumer: if you just wanted to loop input #{Util.ffmpeg_inputs_max} times, that's fine, but if you expected it to loop indefinitely... #{Util.ffmpeg_inputs_max} is the maximum #loop can do at the moment, and it may just not be enough in this case (workaround by concatting or file a complaint at #{Ffmprb::GEM_GITHUB_URL}/issues please)." if times == Util.ffmpeg_inputs_max && io_buff.stats.blocks_buff == 0
-
end
-
end
-
-
# NOTE additional (filtered, processed and looped) input io
-
6
aux_io = File.temp_fifo(intermediate_extname)
-
-
# NOTE (4)
-
-
6
Util::Thread.new "looper" do
-
6
Ffmprb.logger.debug{"Looping #{buff_ios.size} times"}
-
-
6
Ffmprb.logger.debug{"(L4) Looping (#{buff_ios.map &:path}) into (#{aux_io.path})"}
-
6
begin # NOTE may not write its entire output, it's ok
-
6
Ffmprb.process parent: @raw.process, ignore_broken_pipes: false do
-
-
163
ins = buff_ios.map{ |i| input i }
-
6
output(aux_io, video: nil, audio: nil) do
-
163
ins.each{ |i| lay i }
-
end
-
-
end
-
rescue Util::BrokenPipeError
-
5
looping_max = false # NOTE see the above warning
-
end
-
end
-
-
# NOTE (1)
-
-
6
Ffmprb.logger.debug{"(L1) Creating a new input (#{aux_io.path}) to the process"}
-
6
@raw.process.input(aux_io)
-
end
-
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
def mute
-
1
Loud.new self, volume: 0
-
end
-
-
1
def volume(vol)
-
9
Loud.new self, volume: vol
-
end
-
-
1
class Loud < ChainBase
-
-
1
def initialize(unfiltered, volume:)
-
10
super unfiltered
-
10
@volume = volume
-
-
10
fail Error, "volume cannot be nil" if volume.nil?
-
end
-
-
1
def filters_for(lbl, video:, audio:)
-
-
# Modulating volume
-
-
10
lbl_aux = "ld#{lbl}"
-
unfiltered.filters_for(lbl_aux, video: video, audio: audio) +
-
[
-
10
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
-
10
*((audio && channel?(:audio))? Filter.volume(@volume, "#{lbl_aux}:a", "#{lbl}:a"): nil)
-
10
]
-
end
-
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Input
-
-
1
def temporise_io!(extname=nil)
-
6
process.proc_vis_edge @io, process, :remove
-
6
@io.tap do
-
6
@io = File.temp_fifo(extname || io.extname)
-
6
process.proc_vis_edge @io, process
-
end
-
end
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
class Process
-
-
1
class Output
-
-
1
class << self
-
-
# XXX check for unknown options
-
-
1
def video_args(video=nil)
-
43
video = Process.output_video_options.merge(video.to_h)
-
43
[].tap do |args|
-
43
encoder = pixel_format = nil # NOTE ah, ruby
-
43
args.concat %W[-c:v #{encoder}] if (encoder = video.delete(:encoder))
-
43
args.concat %W[-pix_fmt #{pixel_format}] if (pixel_format = video.delete(:pixel_format))
-
43
video.delete :resolution # NOTE is handled otherwise
-
43
video.delete :fps # NOTE is handled otherwise
-
43
Util.assert_options_empty! video
-
end
-
end
-
-
1
def audio_args(audio=nil)
-
63
audio = Process.output_audio_options.merge(audio.to_h)
-
63
[].tap do |args|
-
63
encoder = nil
-
63
args.concat %W[-c:a #{encoder}] if (encoder = audio.delete(:encoder))
-
63
args.concat %W[-ar #{sampling_freq}] if (sampling_freq = audio.delete(:sampling_freq))
-
63
Util.assert_options_empty! audio
-
end
-
end
-
-
1
def resolve(io)
-
55
return io unless io.is_a? String # XXX XXX
-
-
2
File.create(io).tap do |file|
-
2
Ffmprb.logger.warn "Output file exists (#{file.path}), will probably overwrite" if file.exist?
-
end
-
end
-
-
end
-
-
1
attr_reader :io
-
1
attr_reader :process
-
-
1
def initialize(io, process, video:, audio:)
-
55
@io = self.class.resolve(io)
-
55
@process = process
-
55
@channels = {
-
video: video && @io.channel?(:video) && OpenStruct.new(video),
-
audio: audio && @io.channel?(:audio) && OpenStruct.new(audio)
-
}
-
55
if channel?(:video)
-
41
channel(:video).resolution.to_s.split('x').each do |dim|
-
72
fail Error, "Both dimensions of a resolution must be divisible by 2, sorry about that" unless dim.to_i % 2 == 0
-
end
-
end
-
end
-
-
# XXX This method is exceptionally long at the moment. This is not too grand.
-
# However, structuring the code should be undertaken with care, as not to harm the composition clarity.
-
1
def filters
-
55
fail Error, "Nothing to roll..." unless @reels
-
55
fail Error, "Supporting just full_screen for now, sorry." unless @reels.all?(&:full_screen?)
-
55
return @filters if @filters
-
-
55
idx = process.output_index(self)
-
-
55
@filters = []
-
-
# Concatting
-
55
segments = []
-
-
55
@reels.each_with_index do |curr_reel, i|
-
-
232
lbl = nil
-
-
232
if curr_reel.reel
-
-
# NOTE mapping input to this lbl
-
-
232
lbl = "o#{idx}rl#{i}"
-
-
# NOTE Image-Padding to match the target resolution
-
# TODO full screen only at the moment (see exception above)
-
-
232
Ffmprb.logger.debug{"#{self} asking for filters of #{curr_reel.reel.io.inspect} video: #{channel(:video)}, audio: #{channel(:audio)}"}
-
232
@filters.concat(
-
curr_reel.reel.filters_for lbl, video: channel(:video), audio: channel(:audio)
-
)
-
end
-
-
232
trim_prev_at = curr_reel.after || (curr_reel.transition && 0)
-
232
transition_length = curr_reel.transition ? curr_reel.transition.length : 0
-
-
232
if trim_prev_at
-
-
# NOTE make sure previous reel rolls _long_ enough AND then _just_ enough
-
-
9
prev_lbl = segments.pop
-
-
9
lbl_pad = "bl#{prev_lbl}#{i}"
-
# NOTE generously padding the previous segment to support for all the cases
-
@filters.concat(
-
Filter.blank_source trim_prev_at + transition_length,
-
channel(:video).resolution, channel(:video).fps, "#{lbl_pad}:v"
-
9
) if channel?(:video)
-
@filters.concat(
-
Filter.silent_source trim_prev_at + transition_length, "#{lbl_pad}:a"
-
9
) if channel?(:audio)
-
-
9
if prev_lbl
-
3
lbl_aux = lbl_pad
-
3
lbl_pad = "pd#{prev_lbl}#{i}"
-
@filters.concat(
-
Filter.concat_v ["#{prev_lbl}:v", "#{lbl_aux}:v"], "#{lbl_pad}:v"
-
3
) if channel?(:video)
-
@filters.concat(
-
Filter.concat_a ["#{prev_lbl}:a", "#{lbl_aux}:a"], "#{lbl_pad}:a"
-
3
) if channel?(:audio)
-
end
-
-
9
if curr_reel.transition
-
-
# NOTE Split the previous segment for transition
-
-
7
if trim_prev_at > 0
-
@filters.concat(
-
Filter.split "#{lbl_pad}:v", ["#{lbl_pad}a:v", "#{lbl_pad}b:v"]
-
2
) if channel?(:video)
-
@filters.concat(
-
Filter.asplit "#{lbl_pad}:a", ["#{lbl_pad}a:a", "#{lbl_pad}b:a"]
-
2
) if channel?(:audio)
-
2
lbl_pad, lbl_pad_ = "#{lbl_pad}a", "#{lbl_pad}b"
-
else
-
5
lbl_pad, lbl_pad_ = nil, lbl_pad
-
end
-
end
-
-
9
if lbl_pad
-
-
# NOTE Trim the previous segment finally
-
-
4
new_prev_lbl = "tm#{prev_lbl}#{i}a"
-
-
@filters.concat(
-
Filter.trim 0, trim_prev_at, "#{lbl_pad}:v", "#{new_prev_lbl}:v"
-
4
) if channel?(:video)
-
@filters.concat(
-
Filter.atrim 0, trim_prev_at, "#{lbl_pad}:a", "#{new_prev_lbl}:a"
-
4
) if channel?(:audio)
-
-
4
segments << new_prev_lbl
-
4
Ffmprb.logger.debug{"Concatting segments: #{new_prev_lbl} pushed"}
-
end
-
-
9
if curr_reel.transition
-
-
# NOTE snip the end of the previous segment and combine with this reel
-
-
7
lbl_end1 = "o#{idx}tm#{i}b"
-
7
lbl_reel = "o#{idx}tn#{i}"
-
-
7
if !lbl # no reel
-
lbl_aux = "o#{idx}bk#{i}"
-
@filters.concat(
-
Filter.blank_source transition_length, channel(:video).resolution, channel(:video).fps, "#{lbl_aux}:v"
-
) if channel?(:video)
-
@filters.concat(
-
Filter.silent_source transition_length, "#{lbl_aux}:a"
-
) if channel?(:audio)
-
end # NOTE else hope lbl is long enough for the transition
-
-
@filters.concat(
-
Filter.trim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:v", "#{lbl_end1}:v"
-
7
) if channel?(:video)
-
@filters.concat(
-
Filter.atrim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:a", "#{lbl_end1}:a"
-
7
) if channel?(:audio)
-
-
# TODO the only supported transition, see #*lay
-
@filters.concat(
-
Filter.blend_v transition_length, channel(:video).resolution, channel(:video).fps, ["#{lbl_end1}:v", "#{lbl || lbl_aux}:v"], "#{lbl_reel}:v"
-
7
) if channel?(:video)
-
@filters.concat(
-
Filter.blend_a transition_length, ["#{lbl_end1}:a", "#{lbl || lbl_aux}:a"], "#{lbl_reel}:a"
-
7
) if channel?(:audio)
-
-
7
lbl = lbl_reel
-
end
-
-
end
-
-
232
segments << lbl # NOTE can be nil
-
end
-
-
55
segments.compact!
-
-
55
lbl_out = segments[0]
-
-
55
if segments.size > 1
-
30
lbl_out = "o#{idx}o"
-
-
@filters.concat(
-
134
Filter.concat_v segments.map{|s| "#{s}:v"}, "#{lbl_out}:v"
-
30
) if channel?(:video)
-
@filters.concat(
-
196
Filter.concat_a segments.map{|s| "#{s}:a"}, "#{lbl_out}:a"
-
30
) if channel?(:audio)
-
end
-
-
# Overlays
-
-
# NOTE in-process overlays first
-
-
55
@overlays.to_a.each_with_index do |over_reel, i|
-
8
next if over_reel.duck # NOTE this is currently a single case of multi-process... process
-
-
5
fail Error, "Video overlays are not implemented just yet, sorry..." if over_reel.reel.channel?(:video)
-
-
# Audio overlaying
-
-
5
lbl_nxt = "o#{idx}o#{i}"
-
-
5
lbl_over = "o#{idx}l#{i}"
-
5
@filters.concat( # NOTE audio only, see above
-
over_reel.reel.filters_for lbl_over, video: false, audio: channel(:audio)
-
)
-
@filters.concat(
-
Filter.copy "#{lbl_out}:v", "#{lbl_nxt}:v"
-
5
) if channel?(:video)
-
@filters.concat(
-
Filter.amix_to_first_same_volume ["#{lbl_out}:a", "#{lbl_over}:a"], "#{lbl_nxt}:a"
-
5
) if channel?(:audio)
-
-
5
lbl_out = lbl_nxt
-
end
-
-
# NOTE multi-process overlays last
-
-
55
@channel_lbl_ios = {} # XXX this is a spaghetti machine
-
55
@channel_lbl_ios["#{lbl_out}:v"] = io if channel?(:video)
-
55
@channel_lbl_ios["#{lbl_out}:a"] = io if channel?(:audio)
-
-
# TODO supporting just "full" overlays for now, see exception in #add_reel
-
55
@overlays.to_a.each_with_index do |over_reel, i|
-
-
# NOTE this is currently a single case of multi-process... process
-
8
if over_reel.duck
-
3
fail Error, "Don't know how to duck video... yet" if over_reel.duck != :audio
-
-
3
Ffmprb.logger.info "ATTENTION: ducking audio (due to the absence of a simple ffmpeg filter) does not support streaming main input. yet."
-
-
# So ducking just audio here, ye?
-
# XXX check if we're on audio channel
-
-
3
main_av_o = @channel_lbl_ios["#{lbl_out}:a"]
-
3
fail Error, "Main output does not contain audio to duck" unless main_av_o
-
-
3
intermediate_extname = Process.intermediate_channel_extname video: main_av_o.channel?(:video), audio: main_av_o.channel?(:audio)
-
3
main_av_inter_i, main_av_inter_o = File.threaded_buffered_fifo(intermediate_extname, reader_open_on_writer_idle_limit: Util::ThreadedIoBuffer.timeout * 2, proc_vis: process)
-
3
@channel_lbl_ios.each do |channel_lbl, io|
-
5
@channel_lbl_ios[channel_lbl] = main_av_inter_i if io == main_av_o # XXX ~~~spaghetti
-
end
-
3
process.proc_vis_edge process, main_av_o, :remove
-
3
process.proc_vis_edge process, main_av_inter_i
-
3
Ffmprb.logger.debug{"Re-routed the main audio output (#{main_av_inter_i.path}->...->#{main_av_o.path}) through the process of audio ducking"}
-
-
3
over_a_i, over_a_o = File.threaded_buffered_fifo(Process.intermediate_channel_extname(audio: true, video: false), proc_vis: process)
-
3
lbl_over = "o#{idx}l#{i}"
-
3
@filters.concat(
-
over_reel.reel.filters_for lbl_over, video: false, audio: channel(:audio)
-
)
-
3
@channel_lbl_ios["#{lbl_over}:a"] = over_a_i
-
3
process.proc_vis_edge process, over_a_i
-
3
Ffmprb.logger.debug{"Routed and buffering auxiliary output fifos (#{over_a_i.path}>#{over_a_o.path}) for overlay"}
-
-
3
inter_i, inter_o = File.threaded_buffered_fifo(intermediate_extname, proc_vis: process)
-
3
Ffmprb.logger.debug{"Allocated fifos to buffer media (#{inter_i.path}>#{inter_o.path}) while finding silence"}
-
-
3
ignore_broken_pipes_was = process.ignore_broken_pipes # XXX maybe throw an exception instead?
-
3
process.ignore_broken_pipes = true # NOTE audio ducking process may break the overlay pipe
-
-
3
Util::Thread.new "audio ducking" do
-
3
process.proc_vis_edge main_av_inter_o, inter_i # XXX mark it better
-
3
silence = Ffmprb.find_silence(main_av_inter_o, inter_i)
-
-
3
Ffmprb.logger.debug{
-
silence_map = silence.map{|s| "#{s.start_at}-#{s.end_at}"}
-
"Audio ducking with silence: [#{silence_map.join ', '}]"
-
}
-
-
3
Process.duck_audio inter_o, over_a_o, silence, main_av_o,
-
process_options: {parent: process, ignore_broken_pipes: ignore_broken_pipes_was, timeout: process.timeout},
-
video: channel(:video), audio: channel(:audio)
-
end
-
end
-
-
end
-
-
55
@filters
-
end
-
-
1
def args
-
55
fail Error, "Must generate filters first." unless @channel_lbl_ios
-
-
55
[].tap do |args|
-
55
io_channel_lbls = {} # XXX ~~~spaghetti
-
55
@channel_lbl_ios.each do |channel_lbl, io|
-
92
(io_channel_lbls[io] ||= []) << channel_lbl
-
end
-
55
io_channel_lbls.each do |io, channel_lbls|
-
58
channel_lbls.each do |channel_lbl|
-
92
args.concat ['-map', "[#{channel_lbl}]"]
-
end
-
58
args.concat self.class.video_args(channel :video) if channel? :video
-
58
args.concat self.class.audio_args(channel :audio) if channel? :audio
-
58
args << io.path
-
end
-
end
-
end
-
-
1
def input(io, video: true, audio: true)
-
1
process.input io, video: video, audio: audio
-
end
-
-
1
def roll(
-
reel,
-
onto: :full_screen,
-
after: nil,
-
transition: nil
-
)
-
232
fail Error, "Nothing to roll..." unless reel
-
fail Error, "Supporting :transition with :after only at the moment, sorry." unless
-
232
!transition || after || @reels.to_a.empty?
-
-
232
add_reel reel, after, transition, (onto == :full_screen)
-
end
-
1
alias :lay :roll
-
-
1
def overlay(
-
reel,
-
at: 0,
-
duck: nil
-
)
-
8
fail Error, "Nothing to overlay..." unless reel
-
8
fail Error, "Nothing to lay over yet..." if @reels.to_a.empty?
-
8
fail Error, "Ducking overlays should come last... for now" if !duck && @overlays.to_a.last && @overlays.to_a.last.duck
-
-
8
add_snip reel, at, duck
-
end
-
-
1
def channel(medium)
-
1056
@channels[medium]
-
end
-
-
1
def channel?(medium)
-
415
!!channel(medium)
-
end
-
-
1
private
-
-
1
def reels_channel?(medium)
-
@reels.to_a.all?{|r| !r.reel || r.reel.channel?(medium)}
-
end
-
-
1
def add_reel(reel, after, transition, full_screen)
-
232
fail Error, "No time to roll..." if after && after.to_f <= 0
-
232
fail Error, "Partial (not coming last in process) overlays are currently unsupported, sorry." unless @overlays.to_a.empty?
-
-
# NOTE limited functionality: transition = {effect => duration}
-
# TODO temporary obviously, see rendering
-
232
trans =
-
232
if transition
-
fail "Unsupported (yet) transition, sorry." unless
-
7
transition.size == 1 && transition[:blend]
-
7
OpenStruct.new length: transition[:blend].to_f
-
end
-
-
232
(@reels ||= []) <<
-
232
OpenStruct.new(reel: reel, after: after, transition: trans, full_screen?: full_screen)
-
end
-
-
1
def add_snip(reel, at, duck)
-
8
(@overlays ||= []) <<
-
8
OpenStruct.new(reel: reel, at: at, duck: duck)
-
end
-
-
end
-
-
end
-
-
end
-
1
require 'open3'
-
-
1
module Ffmprb
-
-
1
class Error < StandardError; end
-
-
1
module Util
-
-
1
class BrokenPipeError < Error; end
-
1
class TimeLimitError < Error; end
-
-
1
class << self
-
-
1
attr_accessor :ffmpeg_cmd, :ffmpeg_inputs_max, :ffprobe_cmd
-
1
attr_accessor :cmd_timeout
-
-
1
def ffprobe(*args, limit: nil, timeout: cmd_timeout)
-
37
sh *ffprobe_cmd, *args, limit: limit, timeout: timeout
-
end
-
-
# TODO warn on broken pipes incompatibility with 4.x or something
-
1
def ffmpeg(*args, limit: nil, timeout: cmd_timeout, ignore_broken_pipes: true)
-
args = ['-loglevel', 'debug'] + args if
-
171
Ffmprb.ffmpeg_debug
-
171
sh *ffmpeg_cmd, *args, output: :stderr, limit: limit, timeout: timeout, ignore_broken_pipes: ignore_broken_pipes
-
end
-
-
1
def sh(*cmd, input: nil, output: :stdout, limit: nil, timeout: cmd_timeout, ignore_broken_pipes: false)
-
290
cmd = cmd.map &:to_s unless cmd.size == 1
-
3515
cmd_str = cmd.size != 1 ? cmd.map{|c| sh_escape c}.join(' ') : cmd.first
-
290
timeout = [timeout, limit].compact.min
-
290
thr = Thread.new "`#{cmd_str}`" do
-
290
Ffmprb.logger.info "Popening `#{cmd_str}`..."
-
290
Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
-
290
begin
-
290
stdin.write input if input
-
290
stdin.close
-
-
290
log_cmd = cmd.first.upcase
-
290
stdout_r = Reader.new(stdout, store: output == :stdout, log_with: log_cmd)
-
290
stderr_r = Reader.new(stderr, store: true, log_with: log_cmd, log_as: output == :stderr && Logger::DEBUG || Logger::INFO)
-
-
290
Thread.timeout_or_live(limit, log: "while waiting for `#{cmd_str}`", timeout: timeout) do |time|
-
290
value = wait_thr.value
-
290
status = value.exitstatus # NOTE blocking
-
290
if status != 0
-
24
if value.signaled? && value.termsig == Signal.list['PIPE'] # TODO! this doesn't seem to work for ffmpeg 4.x (it ignores SIGPIPEs)
-
11
if ignore_broken_pipes
-
5
Ffmprb.logger.info "Ignoring broken pipe: #{cmd_str}"
-
else
-
6
fail BrokenPipeError, cmd_str
-
end
-
else
-
13
status ||= "sig##{value.termsig}"
-
13
fail Error, "#{cmd_str} (#{status}):\n#{stderr_r.read}"
-
end
-
end
-
end
-
271
Ffmprb.logger.debug{"FINISHED: #{cmd_str}"}
-
-
271
Thread.join_children! limit, timeout: timeout
-
-
# NOTE only one of them will return non-nil, see above
-
271
stdout_r.read || stderr_r.read
-
ensure
-
290
process_dead! wait_thr, cmd_str, limit
-
end
-
end
-
end
-
290
thr.value
-
end
-
-
1
def assert_options_empty!(opts)
-
518
fail ArgumentError, "Unknown options: #{opts}" unless opts.empty?
-
end
-
1
protected
-
-
# NOTE a best guess kinda method
-
1
def sh_escape(str)
-
3225
if str !~ /^[a-z0-9\/.:_-]*$/i && str !~ /"/
-
171
"\"#{str}\""
-
else
-
3054
str
-
end
-
end
-
-
1
def process_dead!(wait_thr, cmd_str, limit)
-
290
grace = limit ? limit/4 : 1
-
290
return unless wait_thr.alive?
-
-
# NOTE a simplistic attempt to gracefully terminate a child process
-
# the successful completion is via exception...
-
begin
-
Ffmprb.logger.info "Sorry it came to this, but I'm terminating `#{cmd_str}`(#{wait_thr.pid})..."
-
::Process.kill 'TERM', wait_thr.pid
-
sleep grace
-
Ffmprb.logger.info "Very sorry it came to this, but I'm terminating `#{cmd_str}`(#{wait_thr.pid}) again..."
-
::Process.kill 'TERM', wait_thr.pid
-
sleep grace
-
Ffmprb.logger.warn "Die `#{cmd_str}`(#{wait_thr.pid}), die!.. (killing amok)"
-
::Process.kill 'KILL', wait_thr.pid
-
sleep grace
-
Ffmprb.logger.warn "Checking if `#{cmd_str}`(#{wait_thr.pid}) finally dead..."
-
::Process.kill 0, wait_thr.pid
-
Ffmprb.logger.error "Still alive -- `#{cmd_str}`(#{wait_thr.pid}), giving up..."
-
rescue Errno::ESRCH
-
Ffmprb.logger.info "Apparently `#{cmd_str}`(#{wait_thr.pid}) is dead..."
-
end
-
-
fail Error, "System error or something: waiting for the thread running `#{cmd_str}`(#{wait_thr.pid})..." unless
-
wait_thr.join limit
-
end
-
-
end
-
-
-
1
class Reader < Thread
-
-
1
def initialize(input, store: false, log_with: nil, log_as: Logger::DEBUG)
-
580
@output = ''
-
580
@queue = Queue.new
-
580
super "reader" do
-
580
begin
-
580
while s = input.gets
-
446370
Ffmprb.logger.log log_as, "#{log_with}: #{s.chomp}" if log_with
-
446370
@output << s if store
-
end
-
580
@queue.enq @output
-
rescue Exception
-
@queue.enq Error.new("Exception in a reader thread")
-
end
-
end
-
end
-
-
1
def read
-
518
case res = @queue.deq
-
when Exception
-
fail res
-
when ''
-
nil
-
else
-
284
res
-
end
-
end
-
-
end
-
-
end
-
-
end
-
-
# require 'ffmprb/util/synchro'
-
1
require_relative 'util/proc_vis'
-
1
require_relative 'util/thread'
-
1
require_relative 'util/threaded_io_buffer'
-
1
require 'set'
-
1
require 'monitor'
-
-
1
module Ffmprb
-
-
1
module Util
-
-
1
module ProcVis
-
-
1
UPDATE_PERIOD_SEC = 1
-
-
1
module Node
-
-
1
attr_accessor :_proc_vis
-
-
1
def proc_vis_name
-
lbl = respond_to?(:label) && label ||
-
short_name ||
-
to_s
-
# ).gsub(/\W+/, '_').sub(/^[^[:alpha:]]*/, '')
-
"#{object_id} [labelType=\"html\" label=#{lbl.to_json}]"
-
end
-
-
1
def proc_vis_node(node, op=:upsert)
-
83775
_proc_vis.proc_vis_node node, op if _proc_vis
-
end
-
-
1
def proc_vis_edge(from, to, op=:upsert)
-
1650
_proc_vis.proc_vis_edge from, to, op if _proc_vis
-
end
-
-
1
private
-
-
1
def short_name
-
return unless respond_to? :name
-
-
short =
-
if name.length <= 30
-
name
-
else
-
"#{name[0..13]}..#{name[-14..-1]}"
-
end
-
"#{self.class.name.split('::').last}: #{short}"
-
end
-
-
end
-
-
1
module ClassMethods
-
-
1
attr_accessor :proc_vis_firebase
-
-
1
def proc_vis_node(obj, op=:upsert)
-
55
return unless proc_vis_init?
-
fail Error, "Must be a #{Node.name}" unless obj.kind_of? Node # XXX duck typing FTW
-
-
obj._proc_vis = self
-
obj.proc_vis_name.tap do |lbl|
-
proc_vis_sync do
-
@_proc_vis_nodes ||= {}
-
if op == :remove
-
@_proc_vis_nodes.delete obj
-
else
-
@_proc_vis_nodes[obj] = lbl
-
end
-
end
-
proc_vis_update # XXX optimise
-
end
-
end
-
-
1
def proc_vis_edge(from, to, op=:upsert)
-
return unless proc_vis_init?
-
-
if op == :upsert
-
proc_vis_node from
-
proc_vis_node to
-
end
-
"#{from.object_id} -> #{to.object_id}".tap do |edge|
-
proc_vis_sync do
-
@_proc_vis_edges ||= SortedSet.new
-
if op == :remove
-
@_proc_vis_edges.delete edge
-
else
-
@_proc_vis_edges << edge
-
end
-
end
-
proc_vis_update
-
end
-
end
-
-
1
private
-
-
1
def proc_vis_update
-
@_proc_vis_upq.enq 1
-
end
-
-
-
1
def proc_vis_do_update
-
nodes = @_proc_vis_nodes.map{ |_, node| "#{node};"}.join("\n") if @_proc_vis_nodes
-
edges = @_proc_vis_edges.map{ |edge| "#{edge};"}.join("\n") if @_proc_vis_edges
-
proc_vis_firebase_client.set proc_vis_pid, dot: [*nodes, *edges].join("\n")
-
end
-
-
1
def proc_vis_pid
-
@proc_vis_pid ||= object_id.tap do |pid|
-
Ffmprb.logger.info "You may view your process visualised at: https://#{proc_vis_firebase}.firebaseapp.com/?pid=#{pid}"
-
end
-
end
-
-
1
def proc_vis_init?
-
55
!!proc_vis_firebase_client
-
end
-
-
1
def proc_vis_up_init
-
@_proc_vis_thr ||= Thread.new do # NOTE update throttling
-
prev_t = Time.now
-
while @_proc_vis_upq.deq # NOTE currently, runs forever (nil terminator needed)
-
proc_vis_do_update
-
Thread.current.live! # XXX not the best we can do here
-
while Time.now - prev_t < UPDATE_PERIOD_SEC
-
@_proc_vis_upq.deq # NOTE drains the queue
-
end
-
@_proc_vis_upq.enq 1
-
end
-
end
-
end
-
-
1
def proc_vis_sync_init
-
1
@_proc_vis_mon ||= Monitor.new
-
1
@_proc_vis_upq ||= Queue.new
-
end
-
1
def proc_vis_sync(&blk)
-
@_proc_vis_mon.synchronize &blk if blk
-
end
-
-
1
def proc_vis_firebase_client
-
55
return @proc_vis_firebase_client if defined? @proc_vis_firebase_client
-
1
@proc_vis_firebase_client =
-
1
if proc_vis_firebase
-
url = "https://#{proc_vis_firebase}.firebaseio.com/proc/"
-
Ffmprb.logger.debug{"Connecting to #{url}"}
-
begin
-
Firebase::Client.new(url).tap do
-
Ffmprb.logger.info "Connected to #{url}"
-
proc_vis_up_init
-
end
-
rescue
-
Ffmprb.logger.error "Could not connect to #{url}"
-
end
-
end
-
end
-
-
end
-
-
1
def self.included(klass)
-
1
klass.extend ClassMethods
-
1
klass.send :proc_vis_sync_init
-
end
-
-
-
end
-
-
end
-
-
end
-
1
module Ffmprb
-
-
1
module Util
-
-
1
class Thread < ::Thread
-
1
include ProcVis::Node
-
-
1
class Error < Ffmprb::Error; end
-
1
class ParentError < Error; end
-
-
1
class << self
-
-
1
attr_accessor :timeout
-
-
1
def timeout_or_live(limit=nil, log: "while doing this", timeout: self.timeout, &blk)
-
1008
started_at = Time.now
-
1008
timeouts = 0
-
1008
logged_timeouts = 1
-
1008
begin
-
1080
timeouts += 1
-
1080
time = Time.now - started_at
-
1080
fail TimeLimitError if limit && time > limit
-
1079
Timeout.timeout timeout do
-
1079
blk.call time
-
end
-
95
rescue Timeout::Error
-
74
if timeouts > 2 * logged_timeouts
-
10
Ffmprb.logger.info "A little bit of timeout #{log.respond_to?(:call)? log.call : log} (##{timeouts})"
-
10
logged_timeouts = timeouts
-
end
-
74
current.live!
-
72
retry
-
end
-
end
-
-
1
def join_children!(limit=nil, timeout: self.timeout)
-
376
Thread.current.join_children! limit, timeout: timeout
-
end
-
-
end
-
-
1
attr_reader :name
-
-
1
def initialize(name="some", main: false, &blk)
-
836
orig_caller = caller
-
836
@name = name
-
836
@parent = Thread.current
-
836
@live_children = []
-
836
@children_mon = Monitor.new
-
836
@dead_children_q = Queue.new
-
836
Ffmprb.logger.debug{"about to launch #{name}"}
-
836
sync_q = Queue.new
-
836
super() do
-
836
@parent.proc_vis_node self if @parent.respond_to? :proc_vis_node
-
836
if @parent.respond_to? :child_lives
-
554
@parent.child_lives self
-
else
-
282
Ffmprb.logger.warn "Not the main: true thread run by a not #{self.class.name} thread" unless main
-
end
-
836
sync_q.enq :ok
-
836
Ffmprb.logger.debug{"#{name} thread launched"}
-
836
begin
-
836
blk.call.tap do
-
801
Ffmprb.logger.debug{"#{name} thread done"}
-
end
-
rescue Exception
-
35
Ffmprb.logger.warn "#{$!.class.name} raised in #{name} thread: #{$!.message}\nBacktrace:\n\t#{$!.backtrace.join("\n\t")}"
-
35
cause = $!
-
Ffmprb.logger.warn "...caused by #{cause.class.name}: #{cause.message}\nBacktrace:\n\t#{cause.backtrace.join("\n\t")}" while
-
35
cause = cause.cause
-
35
fail $! # XXX I have no idea why I need to give it `$!` -- the docs say I need not
-
ensure
-
836
@parent.child_dies self if @parent.respond_to? :child_dies
-
836
@parent.proc_vis_node self, :remove if @parent.respond_to? :proc_vis_node
-
end
-
end
-
836
sync_q.deq
-
end
-
-
# TODO protected: none of these methods should be called by a user code, the only public methods are above
-
-
1
def live!
-
21883
fail ParentError if @parent.status.nil?
-
end
-
-
1
def child_lives(thr)
-
554
@children_mon.synchronize do
-
554
Ffmprb.logger.debug{"picking up #{thr.name} thread"}
-
554
@live_children << thr
-
end
-
554
proc_vis_edge self, thr
-
end
-
-
1
def child_dies(thr)
-
554
@children_mon.synchronize do
-
554
Ffmprb.logger.debug{"releasing #{thr.name} thread"}
-
554
@dead_children_q.enq thr
-
554
fail "System Error" unless @live_children.delete thr
-
end
-
554
proc_vis_edge self, thr, :remove
-
end
-
-
1
def join_children!(limit=nil, timeout: Thread.timeout)
-
376
timeout = [timeout, limit].compact.min
-
376
Ffmprb.logger.debug "joining threads: #{@live_children.size} live, #{@dead_children_q.size} dead"
-
376
until @live_children.empty? && @dead_children_q.empty?
-
521
thr = self.class.timeout_or_live limit, log: "joining threads: #{@live_children.size} live, #{@dead_children_q.size} dead", timeout: timeout do
-
526
@dead_children_q.deq
-
end
-
521
Ffmprb.logger.debug "joining the late #{thr.name} thread"
-
521
fail "System Error" unless thr.join(timeout) # NOTE should not block
-
end
-
end
-
-
end
-
-
end
-
-
end
-
1
require 'ostruct'
-
-
1
module Ffmprb
-
-
1
module Util
-
-
# TODO the events mechanism is currently unused (and commented out) => synchro mechanism not needed
-
1
class ThreadedIoBuffer
-
# XXX include Synchro
-
1
include ProcVis::Node
-
-
1
class << self
-
-
1
attr_accessor :blocks_max
-
1
attr_accessor :block_size
-
1
attr_accessor :timeout
-
1
attr_accessor :timeout_limit
-
1
attr_accessor :io_wait_timeout
-
-
end
-
-
-
1
attr_reader :stats
-
-
-
# NOTE input/output can be lambdas for single asynchronic io evaluation
-
# the lambdas must be timeout-interrupt-safe (since they are wrapped in timeout blocks)
-
# NOTE all ios are being opened and closed as soon as possible
-
1
def initialize(input, *outputs, keep_outputs_open_on_input_idle_limit: nil)
-
29
super() # NOTE for the monitor, apparently
-
-
29
Ffmprb.logger.debug{"ThreadedIoBuffer initializing with (#{ThreadedIoBuffer.blocks_max}x#{ThreadedIoBuffer.block_size})"}
-
-
29
@input = input
-
29
@outputs = outputs.map do |outp|
-
192
OpenStruct.new _io: outp, q: SizedQueue.new(ThreadedIoBuffer.blocks_max)
-
end
-
29
@stats = Stats.new(self)
-
29
@keep_outputs_open_on_input_idle_limit = keep_outputs_open_on_input_idle_limit
-
# @events = {}
-
-
29
Thread.new "io buffer main" do
-
29
init_reader!
-
29
@outputs.each do |output|
-
192
init_writer_output! output
-
192
init_writer! output
-
end
-
-
29
Thread.join_children!.tap do
-
28
Ffmprb.logger.debug{"ThreadedIoBuffer (#{@input.path}->#{@outputs.map(&:io).map(&:path)}) terminated successfully (#{stats})"}
-
end
-
end
-
end
-
# TODO?
-
#
-
# def once(event, &blk)
-
# event = event.to_sym
-
# wait_for_handler!
-
# if @events[event].respond_to? :call
-
# fail Error, "Once upon a time (one #once(event) at a time) please"
-
# elsif @events[event]
-
# Ffmprb.logger.debug{"ThreadedIoBuffer (post-)reacting to #{event}"}
-
# @handler_thr = Util::Thread.new "#{event} handler", &blk
-
# else
-
# Ffmprb.logger.debug{"ThreadedIoBuffer subscribing to #{event}"}
-
# @events[event] = blk
-
# end
-
# end
-
# handle_synchronously :once
-
#
-
# def reader_done!
-
# Ffmprb.logger.debug{"ThreadedIoBuffer reader terminated (#{stats})"}
-
# fire! :reader_done
-
# end
-
#
-
# def terminated!
-
# fire! :terminated
-
# end
-
#
-
# def timeout!
-
# fire! :timeout
-
# end
-
-
# protected
-
#
-
# def fire!(event)
-
# wait_for_handler!
-
# Ffmprb.logger.debug{"ThreadedIoBuffer firing #{event}"}
-
# if blk = @events.to_h[event.to_sym]
-
# @handler_thr = Util::Thread.new "#{event} handler", &blk
-
# end
-
# @events[event.to_sym] = true
-
# end
-
# handle_synchronously :fire!
-
#
-
-
1
def label
-
"IObuff: Curr/Peak/Max=#{stats.blocks_buff}/#{stats.blocks_max}/#{ThreadedIoBuffer.blocks_max} In/Out=#{stats.bytes_in}/#{stats.bytes_out}"
-
end
-
-
1
private
-
-
1
class AllOutputsBrokenError < Error
-
end
-
-
1
def reader_input! # NOTE just for reader thread
-
32
if @input.respond_to?(:call)
-
29
Ffmprb.logger.debug{"Opening buffer input"}
-
29
@input = @input.call
-
29
Ffmprb.logger.debug{"Opened buffer input: #{@input.path}"}
-
end
-
32
@input
-
end
-
-
# NOTE to be called after #init_writer_output! only
-
1
def writer_output!(output) # NOTE just for writer thread
-
192
if output.thr
-
192
output.thr.join
-
192
output.thr = nil
-
end
-
192
output.io
-
end
-
-
# NOTE reads roughly as much input as writers can write, then closes the stream; times out on buffer overflow
-
1
def init_reader!
-
29
Thread.new("buffer reader") do
-
29
begin
-
29
input_io = reader_input!
-
29
loop do # NOTE until EOFError, see below
-
23181
s = ''
-
23181
while s.length < ThreadedIoBuffer.block_size
-
43465
timeouts = 0
-
43465
logged_timeouts = 1
-
43465
begin
-
63444
ss = input_io.read_nonblock(ThreadedIoBuffer.block_size - s.length)
-
43441
stats.add_bytes_in ss.length
-
43441
s += ss
-
rescue IO::WaitReadable
-
19983
if @keep_outputs_open_on_input_idle_limit && stats.bytes_in > 0 && stats.blocks_buff == 0 && timeouts * ThreadedIoBuffer.io_wait_timeout > @keep_outputs_open_on_input_idle_limit
-
4
if s.length > 0 # NOTE let's see if it helps outputting an incomplete block
-
2
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) giving a chance to write #{s.length}/#{ThreadedIoBuffer.block_size}b after waiting >#{@keep_outputs_open_on_input_idle_limit}s, after reading #{stats.bytes_in}b"}
-
2
break
-
else
-
2
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) giving up after waiting >#{@keep_outputs_open_on_input_idle_limit}s, after reading #{stats.bytes_in}b, closing outputs"}
-
2
raise EOFError
-
end
-
else
-
19979
Thread.current.live!
-
19979
timeouts += 1
-
19979
if timeouts > 2 * logged_timeouts
-
8
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) retrying... (#{timeouts} reads): #{$!.class}"}
-
8
logged_timeouts = timeouts
-
end
-
19979
IO.select [input_io], nil, nil, ThreadedIoBuffer.io_wait_timeout
-
19979
retry
-
end
-
rescue EOFError
-
20
output_enq! s
-
20
raise
-
rescue IO::WaitWritable # NOTE should not really happen, so just for conformance
-
Ffmprb.logger.error "ThreadedIoBuffer reader (from #{input_io.path}) gets a #{$!} - should not really happen."
-
IO.select nil, [input_io], nil, ThreadedIoBuffer.io_wait_timeout
-
retry
-
end
-
end
-
23159
output_enq! s
-
end
-
rescue EOFError
-
22
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) breaking off"}
-
rescue AllOutputsBrokenError
-
6
Ffmprb.logger.info "All outputs broken"
-
rescue Exception
-
1
@reader_failed = Error.new("Reader failed: #{$!}")
-
1
raise
-
ensure
-
29
begin
-
29
output_enq! nil # NOTE EOF signal
-
rescue
-
end
-
29
begin
-
29
input_io.close if input_io.respond_to?(:close)
-
rescue
-
Ffmprb.logger.error "#{$!.class.name} closing ThreadedIoBuffer input: #{$!.message}"
-
end
-
# reader_done!
-
29
Ffmprb.logger.debug{"ThreadedIoBuffer reader terminated (#{stats})"}
-
end
-
end
-
end
-
-
1
def init_writer_output!(output)
-
192
return output.io = output._io unless output._io.respond_to?(:call)
-
-
192
output.thr = Thread.new("buffer writer output helper") do
-
192
Ffmprb.logger.debug{"Opening buffer output"}
-
192
output.io =
-
Thread.timeout_or_live nil, log: "in the buffer writer helper thread", timeout: ThreadedIoBuffer.timeout do |time|
-
250
fail Error, "giving up buffer writer init since the reader has failed (#{@reader_failed.message})" if @reader_failed
-
250
output._io.call
-
end
-
192
Ffmprb.logger.debug{"Opened buffer output: #{output.io.path}"}
-
end
-
end
-
-
# NOTE writes as much output as possible, then terminates when the reader dies
-
1
def init_writer!(output)
-
192
Thread.new("buffer writer") do
-
192
begin
-
192
output_io = writer_output!(output)
-
192
while s = output_deq!(output) # NOTE until EOF signal
-
31804
timeouts = 0
-
31804
logged_timeouts = 1
-
31804
begin
-
33634
fail @reader_failed if @reader_failed # NOTE otherwise, output_io should not be nil
-
33633
written = output_io.write_nonblock(s)
-
32151
stats.add_bytes_out written
-
-
32151
if written != s.length
-
472
s = s[written..-1]
-
472
raise IO::EAGAINWaitWritable
-
end
-
-
rescue IO::WaitWritable
-
1830
Thread.current.live!
-
1830
timeouts += 1
-
1830
if timeouts > 2 * logged_timeouts
-
140
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io.path}) retrying... (#{timeouts} writes): #{$!.class}"}
-
140
logged_timeouts = timeouts
-
end
-
1830
IO.select nil, [output_io], nil, ThreadedIoBuffer.io_wait_timeout
-
1830
retry
-
rescue IO::WaitReadable # NOTE should not really happen, so just for conformance
-
Ffmprb.logger.error "ThreadedIoBuffer writer (to #{output_io.path}) gets a #{$!} - should not really happen."
-
IO.select [output_io], nil, ThreadedIoBuffer.io_wait_timeout
-
retry
-
end
-
end
-
67
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io.path}) breaking off"}
-
rescue Errno::EPIPE
-
124
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io.path}) broken"}
-
124
output.broken = true
-
ensure
-
# terminated!
-
192
begin
-
192
output_io.close if !output.broken && output_io && output_io.respond_to?(:close)
-
rescue
-
Ffmprb.logger.error "#{$!.class.name} closing ThreadedIoBuffer output: #{$!.message}"
-
end
-
192
output.broken = true
-
192
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io && output_io.path}) terminated (#{stats})"}
-
end
-
end
-
end
-
#
-
# def wait_for_handler!
-
# @handler_thr.join if @handler_thr
-
# @handler_thr = nil
-
# end
-
-
1
def output_enq!(item)
-
13
fail AllOutputsBrokenError if
-
@outputs.select do |output|
-
58728
next if output.broken
-
-
37133
timeouts = 0
-
37133
logged_timeouts = 1
-
37133
begin
-
# NOTE let's assume there's no race condition here between the possible timeout exception and enq
-
37136
Timeout.timeout(ThreadedIoBuffer.timeout) do
-
37136
output.q.enq item
-
end
-
37130
stats.blocks_for output
-
-
rescue Timeout::Error
-
6
next if output.broken
-
-
4
timeouts += 1
-
4
if timeouts == 2 * logged_timeouts
-
2
Ffmprb.logger.warn "A little bit of timeout (>#{timeouts*ThreadedIoBuffer.timeout}s idle) with #{ThreadedIoBuffer.blocks_max}x#{ThreadedIoBuffer.block_size}b blocks (buffering #{reader_input!.path}->...; #{@outputs.reject(&:io).size}/#{@outputs.size} unopen/total)"
-
2
logged_timeouts = timeouts
-
end
-
-
4
retry unless timeouts >= ThreadedIoBuffer.timeout_limit # NOTE the queue has probably overflown
-
-
1
@reader_failed ||= Error.new("the writer has failed with timeout limit while queuing") # NOTE screw the race condition
-
# timeout!
-
1
fail Error, "Looks like we're stuck (>#{ThreadedIoBuffer.timeout_limit*ThreadedIoBuffer.timeout}s idle) with #{ThreadedIoBuffer.blocks_max}x#{ThreadedIoBuffer.block_size}b blocks (buffering #{reader_input!.path}->...)..."
-
end
-
23208
end.empty?
-
end
-
-
1
def output_deq!(outp)
-
31871
outp.q.deq.tap do
-
31871
stats.blocks_for outp
-
end
-
end
-
-
1
class Stats < OpenStruct
-
1
include MonitorMixin
-
-
1
def initialize(proc)
-
29
@proc = proc
-
29
@output_blocks = {}
-
29
super blocks_buff: 0, blocks_max: 0, bytes_in: 0, bytes_out: 0
-
end
-
-
1
def add_bytes_in(n)
-
43441
synchronize do
-
43441
self.bytes_in += n
-
43441
@proc.proc_vis_node @proc # NOTE update
-
end
-
end
-
-
1
def add_bytes_out(n)
-
32151
synchronize do
-
32151
self.bytes_out += n
-
32151
@proc.proc_vis_node @proc # NOTE update
-
end
-
end
-
-
1
def blocks_for(outp)
-
69001
synchronize do
-
69001
blocks = @output_blocks[outp.object_id] = outp.q.length
-
69001
if blocks > blocks_max
-
6952
self.blocks_max = blocks
-
6952
@proc.proc_vis_node @proc # NOTE update
-
end
-
69001
self.blocks_buff = @output_blocks.values.reduce(0, :+)
-
end
-
end
-
-
end
-
-
end
-
-
end
-
-
end
-
1
describe Ffmprb::Execution do
-
-
1
around do |example|
-
4
Ffmprb::File.temp('.flv') do |tf|
-
4
@av_file_o = tf
-
4
example.run
-
end
-
end
-
-
1
it "should run the script (no params)" do
-
1
Ffmprb::File.temp('.ffmprb') do |tf|
-
1
tf.write <<-FFMPRB
-
-
output('#{@av_file_o.path}') do
-
roll input('#{@av_file_c_gor_9.path}')
-
overlay input('#{@a_file_g_16.path}')
-
end
-
-
FFMPRB
-
-
1
cmd = "exe/ffmprb < #{tf.path}"
-
-
1
expect(Ffmprb::Util.sh cmd, output: :stderr).to match /WARN.+Output file exists/ # NOTE temp files are _created_ above
-
1
expect(@av_file_o.length).to be_approximately @av_file_c_gor_9.length
-
end
-
end
-
-
1
it "should run the script" do
-
1
Ffmprb::File.temp('.ffmprb') do |tf|
-
1
tf.write <<-FFMPRB
-
|av_main_i, a_over_i, av_main_o|
-
-
in1 = input(av_main_i)
-
in2 = input(a_over_i)
-
output(av_main_o) do
-
roll in1
-
overlay in2
-
end
-
-
FFMPRB
-
-
1
cmd = "exe/ffmprb #{@av_file_c_gor_9.path} #{@a_file_g_16.path} #{@av_file_o.path} < #{tf.path}"
-
-
1
expect(Ffmprb::Util.sh cmd, output: :stderr).to match /WARN.+Output file exists/ # NOTE temp files are _created_ above
-
1
expect(@av_file_o.length).to be_approximately @av_file_c_gor_9.length
-
end
-
end
-
-
1
[['', 300, :to], [' not', 90, :not_to]].each do |wat, cut, to_not_to|
-
2
it "should#{wat} warn about the looping limitation" do
-
-
2
inp_s = <<-FFMPRB
-
-
in1 = input('#{@av_file_c_gor_9.path}')
-
output('#{@av_file_o.path}') do
-
roll in1.loop.cut(to: #{cut})
-
end
-
-
FFMPRB
-
2
expect(Ffmprb::Util.sh 'exe/ffmprb', input: inp_s, output: :stderr).send(
-
to_not_to,
-
match(/WARN.+Looping.+finished before its consumer/)
-
)
-
2
expect(@av_file_o.length true).to be_approximately cut
-
end
-
end
-
-
end
-
1
require 'rmagick'
-
1
require 'sox'
-
-
1
MIN_VOLUME = -0xFFFF
-
-
1
describe Ffmprb do
-
-
-
1
it 'has a version number' do
-
1
expect(Ffmprb::VERSION).not_to be nil
-
end
-
-
# IMPORTANT NOTE Examples here use static (pre-generated) sample files, but the interface is streaming-oriented
-
# So there's just a hope it all works well with streams, which really must be replaced by appropriate specs
-
-
1
context :process do
-
-
1
around do |example|
-
37
Ffmprb::File.temp('.mp4') do |tf|
-
37
@av_out_file = tf
-
37
Ffmprb::File.temp('.flv') do |tf|
-
37
@av_out_stream = tf
-
37
Ffmprb::File.temp('.mp3') do |tf|
-
37
@a_out_file = tf
-
37
example.run
-
end
-
end
-
end
-
end
-
-
1
def check_av_c_gor_at!(at, file: @av_out_file)
-
14
file.sample at: at do |shot, sound|
-
14
check_reddish! pixel_data(shot, 250, 10)
-
14
check_greenish! pixel_data(shot, 250, 110)
-
14
check_note! :C6, wave_data(sound)
-
end
-
end
-
-
1
def check_av_e_bow_at!(at)
-
1
@av_out_file.sample at: at do |shot, sound|
-
1
check_white! pixel_data(shot, 250, 10)
-
1
check_black! pixel_data(shot, 250, 110)
-
1
check_note! :E6, wave_data(sound)
-
end
-
end
-
-
1
def check_av_btn_wtb_at!(at, black: false)
-
8
@av_out_file.sample at: at do |shot, sound|
-
8
pixel = pixel_data(shot, 250, 110)
-
8
wave = wave_data(sound)
-
8
if black
-
2
check_black! pixel
-
2
expect(wave.volume).to eq MIN_VOLUME
-
else
-
6
check_white! pixel
-
6
check_note! :B6, wave
-
6
expect(wave.volume).to be > MIN_VOLUME
-
end
-
end
-
end
-
-
1
def check_black!(pixel)
-
7
expect(channel_max pixel).to eq 0
-
end
-
-
1
def check_white!(pixel)
-
12
expect(channel_min pixel).to eq 0xFFFF
-
end
-
-
1
def check_greenish!(pixel)
-
36
expect(pixel.green).to be > pixel.red
-
36
expect(pixel.green).to be > pixel.blue
-
36
expect(2 * (pixel.red - pixel.blue).abs).to be < (pixel.green - pixel.blue).abs
-
36
expect(2 * (pixel.red - pixel.blue).abs).to be < (pixel.green - pixel.red).abs
-
end
-
-
1
def check_reddish!(pixel)
-
15
expect(pixel.red).to be > pixel.green
-
15
expect(pixel.red).to be > pixel.blue
-
15
expect(2 * (pixel.green - pixel.blue).abs).to be < (pixel.red - pixel.blue).abs
-
15
expect(2 * (pixel.green - pixel.blue).abs).to be < (pixel.red - pixel.green).abs
-
end
-
-
1
def check_note!(note, wave)
-
43
expect(wave.frequency).to be_approximately NOTES[note]
-
end
-
-
-
1
it "should behave like README says it does" do
-
1
flick_mp4 = @av_file_btn_wtb_16.path
-
1
track_mp3 = @a_file_g_16.path
-
1
cine_flv = @av_out_stream.path
-
-
1
Ffmprb.process do
-
-
1
in_main = input(flick_mp4)
-
1
output cine_flv, video: {resolution: '1280x720'} do
-
1
roll in_main.crop(0.25).cut(from: 2, to: 5), transition: {blend: 1}
-
1
roll in_main.volume(2).cut(from: 6, to: 16), after: 2, transition: {blend: 1}
-
1
overlay input(track_mp3).volume(0.8)
-
end
-
-
end
-
-
1
expect(@av_out_stream.length).to be_approximately 12
-
-
end
-
-
1
it "should fail on too much inputs" do
-
1
too_much = 99
-
1
expect {
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
34
inps = (0..100).map{input file_input}
-
output file_output do
-
inps.each do |inp|
-
roll inp
-
end
-
end
-
-
end
-
}.to raise_error Ffmprb::Error
-
end
-
-
1
it "should fail on unknown options" do
-
1
expect {
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file, magic: :yes_please!) do |file_input, file_output|
-
-
output file_output do
-
lay file_input
-
end
-
-
end
-
}.to raise_error ArgumentError
-
end
-
-
1
it "should transcode" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output, video: {resolution: Ffmprb::HD_720p, fps: 30}) do
-
1
roll in1
-
end
-
-
end
-
-
1
check_av_c_gor_at! 1
-
1
expect(@av_out_file.resolution).to eq Ffmprb::HD_720p
-
1
expect(@av_out_file.length).to be_approximately 9
-
end
-
-
1
describe "video only (no audio)" do
-
-
1
around do |example|
-
1
Ffmprb::File.temp_fifo '.apng' do |tmp_papng|
-
-
1
thr = Thread.new do
-
1
Ffmprb::Util.ffmpeg '-filter_complex', 'color=white:d=2:r=25', tmp_papng.path
-
end
-
-
1
@v_in_fifo = tmp_papng
-
-
1
begin
-
1
example.run
-
ensure
-
1
thr.join
-
end
-
end
-
end
-
-
1
it "should transcode with defaults (no audio track)" do
-
1
Ffmprb.process(@v_in_fifo, @av_out_file) do |fifo_input, file_output|
-
-
1
in1 = input(fifo_input, video: {fps: 25})
-
1
output(file_output, audio: false) do
-
1
roll in1
-
end
-
-
end
-
-
1
expect(@av_out_file.length).to be_approximately 2
-
-
1
@av_out_file.sample at: 0.5, audio: false do |image|
-
1
check_white! pixel_data(image, 250, 110)
-
end
-
1
expect {
-
1
@av_out_file.sample at: 0.5, video: false do |sound|
-
expect(false).to be_truthy
-
end
-
}.to raise_error Ffmprb::Error
-
end
-
-
1
xit "should transcode with defaults (silence)" do
-
Ffmprb.process(@v_in_fifo, @av_out_file) do |fifo_input, file_output|
-
-
in1 = input(fifo_input, video: {fps: 25})
-
output(file_output) do
-
roll in1
-
end
-
-
end
-
-
expect(@av_out_file.length).to be_approximately 2
-
@av_out_file.sample at: 0.5 do |image, sound|
-
check_white! pixel_data(image, 250, 110)
-
expect(wave_data(sound).volume).to eq MIN_VOLUME
-
end
-
end
-
-
end
-
-
1
it "should partially support multiple outputs" do
-
1
Ffmprb::File.temp('.mp4') do |another_av_out_file|
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output1|
-
-
1
in1 = input(file_input)
-
1
output(file_output1, video: {resolution: Ffmprb::HD_720p, fps: 30}) do
-
1
roll in1.cut(to: 6)
-
end
-
1
output(another_av_out_file, video: {resolution: Ffmprb::HD_720p, fps: 30}) do
-
1
roll in1
-
end
-
-
end
-
-
1
check_av_c_gor_at! 1
-
1
check_av_c_gor_at! 1, file: another_av_out_file
-
1
expect(@av_out_file.resolution).to eq Ffmprb::HD_720p
-
1
expect(another_av_out_file.resolution).to eq Ffmprb::HD_720p
-
1
expect(@av_out_file.length).to be_approximately 6
-
1
expect(another_av_out_file.length).to be_approximately 9
-
end
-
end
-
-
1
it "should ignore broken pipes (or not)" do
-
1
[[:to, false, Ffmprb::Error], [:not_to, true, nil]].each do |to_not_to, ignore_broken_pipes, error|
-
2
Ffmprb::File.temp_fifo('.flv') do |av_pipe|
-
2
Thread.new do
-
2
begin
-
2
tmp = File.open(av_pipe.path, 'r')
-
2
tmp.read(1)
-
ensure
-
2
tmp.close if tmp
-
end
-
end
-
-
2
expect do
-
2
Ffmprb.process(@av_file_e_bow_9, ignore_broken_pipes: ignore_broken_pipes) do |file_input|
-
-
2
in1 = input(file_input)
-
2
output(av_pipe, video: {resolution: Ffmprb::HD_1080p, fps: 60}) do
-
2
roll in1.loop
-
end
-
-
end
-
end.send to_not_to, raise_error(*error)
-
end
-
end
-
end
-
-
1
it "should parse path arguments (and transcode)" do
-
1
Ffmprb.process(@av_file_e_bow_9.path, @av_out_file.path) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1
-
end
-
-
end
-
-
1
check_av_e_bow_at! 1
-
1
expect(@av_out_file.resolution).to eq Ffmprb::CGA
-
1
expect(@av_out_file.length).to be_approximately 9
-
end
-
-
1
it "should concat" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1
-
1
roll in1
-
end
-
-
end
-
-
1
check_av_c_gor_at! 2
-
1
check_av_c_gor_at! 8
-
1
expect(@av_out_file.length).to be_approximately 18
-
end
-
-
1
it "should loop" do
-
1
Ffmprb::Util::ThreadedIoBuffer.block_size.tap do |default|
-
1
begin
-
1
Ffmprb::Util::ThreadedIoBuffer.block_size = 1024 # NOTE to check for excessive memory consumption during looping etc
-
-
1
Ffmprb.process(@av_file_btn_wtb_16, @av_out_stream) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1
-
end
-
-
end
-
-
1
expect(@av_out_stream.length).to be_approximately 16
-
-
1
Ffmprb.process(@av_out_stream, @av_out_file) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1.cut(to: 12).loop.cut(to: 47)
-
end
-
-
end
-
-
1
check_av_btn_wtb_at! 2
-
1
check_av_btn_wtb_at! 6, black: true
-
1
check_av_btn_wtb_at! 10
-
1
check_av_btn_wtb_at! 14
-
1
check_av_btn_wtb_at! 18, black: true
-
1
check_av_btn_wtb_at! 45
-
-
1
expect(@av_out_file.length).to be_approximately 47
-
ensure
-
1
Ffmprb::Util::ThreadedIoBuffer.block_size = default
-
end
-
end
-
end
-
-
1
it "should roll reels after specific time (cutting previous reels)" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_file_btn_wtb_16, @av_out_file) do |file_input, file_input_2, file_output|
-
-
1
in1 = input(file_input)
-
1
in2 = input(file_input_2)
-
1
output(file_output) do
-
1
roll in1
-
1
roll in2, after: 3
-
end
-
-
end
-
-
1
check_av_c_gor_at! 2
-
1
check_av_btn_wtb_at! 4
-
1
expect(@av_out_file.length).to be_approximately 19
-
end
-
-
1
it "should roll reels after specific time (even the first one, adding blanks in the beginning)" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1, after: 3
-
end
-
-
end
-
-
1
check_av_c_gor_at! 4
-
1
expect(@av_out_file.length).to be_approximately 12
-
end
-
-
-
1
[12, 21].each do |duration|
-
2
it "should cut to precise duration (total 12 <=> cut after #{duration})" do
-
2
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
2
in1 = input(file_input)
-
2
output(file_output) do
-
2
roll in1
-
2
roll in1.cut to: (duration - file_input.length)
-
end
-
-
end
-
-
2
check_av_c_gor_at! 5
-
2
check_av_c_gor_at! 7
-
2
expect(@av_out_file.length).to be_approximately duration
-
end
-
end
-
-
1
it "should crop segments" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1.crop(0.25)
-
1
roll in1
-
1
roll in1.crop(width: 0.25, height: 0.25)
-
1
roll in1.crop(left: 0, top: 0, width: 0.25, height: 0.25)
-
end
-
-
end
-
-
1
@av_out_file.sample at: 5 do |snap, sound|
-
1
check_greenish! pixel_data(snap, 100, 10)
-
1
check_note! :C6, wave_data(sound)
-
end
-
1
check_av_c_gor_at! 14
-
1
@av_out_file.sample at: 23 do |snap, sound|
-
1
check_greenish! pixel_data(snap, 100, 10)
-
1
check_note! :C6, wave_data(sound)
-
end
-
1
@av_out_file.sample at: 32 do |snap, sound|
-
1
check_reddish! pixel_data(snap, 100, 10)
-
1
check_note! :C6, wave_data(sound)
-
end
-
end
-
-
1
it "should cut and crop segments" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1.crop(0.25).cut(to: 3)
-
1
roll in1
-
end
-
-
end
-
-
1
@av_out_file.sample at: 2 do |snap, sound|
-
1
check_greenish! pixel_data(snap, 100, 10)
-
1
check_note! :C6, wave_data(sound)
-
end
-
1
check_av_c_gor_at! 4
-
1
expect(@av_out_file.length).to be_approximately 12
-
end
-
-
# TODO might be insufficient
-
1
it "should cut segments in any order" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |file_input, file_output|
-
-
1
in1 = input(file_input)
-
1
output(file_output) do
-
1
roll in1.cut(from: 1)
-
1
roll in1.crop(0.25).cut(to: 5)
-
end
-
-
end
-
-
1
check_av_c_gor_at! 1
-
1
@av_out_file.sample at: 9 do |snap, sound|
-
1
check_greenish! pixel_data(snap, 100, 10)
-
1
check_note! :C6, wave_data(sound)
-
end
-
1
expect(@av_out_file.length).to be_approximately 13
-
end
-
-
1
it "should change volume and mute" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |av_i, av_o|
-
1
in1 = input(av_i)
-
1
output(av_o) do
-
1
roll in1.cut(to: 4)
-
1
roll in1.cut(to: 4).mute
-
1
roll in1.cut(to: 4).volume(0.5)
-
end
-
end
-
-
1
expect(
-
3
[5, 9, 1].map{|s| wave_data(@av_out_file.sample_audio at: s).volume}
-
).to be_ascending
-
end
-
-
1
it "should modulate volume" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_out_file) do |av_i, av_o|
-
1
in1 = input(av_i)
-
1
output(av_o) do
-
1
roll in1.cut(to: 3)
-
1
roll in1.volume(1.9 => 0, 4.1 => 0, 6 => 0.5, 7.9 => 1)
-
end
-
end
-
-
8
volume_at = ->(sec){wave_data(@av_out_file.sample_audio at: sec).volume}
-
-
1
expect(volume_at.call 0.1).to be_approximately volume_at.call(11)
-
1
expect(
-
[4, 3.75, 3.5].map(&volume_at)
-
).to be_ascending
-
1
expect(volume_at.call 5).to eq volume_at.call 6
-
end
-
-
1
it "should detect silence and pass input to output" do
-
1
silence = Ffmprb.find_silence(@av_file_btn_wtb_16, @av_out_file)
-
1
expect(silence.length).to eq 2
-
1
prev_silent_end_at = 0
-
1
silence.each do |silent|
-
2
@av_out_file.sample at: silent.start_at + 1 do |image, sound|
-
2
expect(wave_data(sound).volume).to eq MIN_VOLUME
-
2
check_black! pixel_data(image, 100, 100)
-
end
-
2
@av_out_file.sample at: (prev_silent_end_at + silent.start_at)/2 do |image, sound|
-
2
expect(wave_data(sound).volume).to be > MIN_VOLUME
-
2
check_white! pixel_data(image, 100, 100)
-
end
-
2
prev_silent_end_at = silent.end_at
-
end
-
end
-
-
1
context "media" do
-
-
13
let(:m_input) {{video: @v_file_6, audio: @a_file_g_16}}
-
13
let(:m_output_extname) {{video: '.y4m', audio: '.wav'}}
-
-
1
medium_params = {
-
video: {},
-
audio: {encoder: nil}
-
}
-
1
[[:video, :audio], [:audio, :video]].each do |medium, not_medium|
-
[
-
lambda do |av_file_input, m_file_input, m_file_output| ##1
-
2
in1 = input(av_file_input)
-
2
output(m_file_output, medium => medium_params[medium], not_medium => false) do
-
2
roll in1.cut(from: 3, to: 5)
-
2
roll in1.cut(from: 3, to: 5)
-
end
-
end,
-
lambda do |av_file_input, m_file_input, m_file_output| ##2
-
2
in1 = input(av_file_input)
-
2
output(m_file_output, medium => medium_params[medium]) do
-
2
roll in1.send(medium).cut(from: 3, to: 5)
-
2
roll in1.send(medium).cut(from: 3, to: 5)
-
end
-
end,
-
lambda do |av_file_input, m_file_input, m_file_output| ##3
-
2
in1 = input(av_file_input)
-
2
output(m_file_output, medium => medium_params[medium]) do
-
2
roll in1.cut(from: 3, to: 5)
-
2
roll in1.cut(from: 3, to: 5)
-
end
-
end,
-
lambda do |av_file_input, m_file_input, m_file_output| ##4
-
2
in1 = input(m_file_input)
-
2
output(m_file_output, medium => medium_params[medium], not_medium => false) do
-
2
roll in1.cut(from: 3, to: 5)
-
2
roll in1.cut(from: 3, to: 5)
-
end
-
end,
-
lambda do |av_file_input, m_file_input, m_file_output| ##5
-
2
in1 = input(m_file_input)
-
2
output(m_file_output, medium => medium_params[medium]) do
-
2
roll in1.send(medium).cut(from: 3, to: 5)
-
2
roll in1.send(medium).cut(from: 3, to: 5)
-
end
-
end,
-
lambda do |av_file_input, m_file_input, m_file_output| ##6
-
2
in1 = input(m_file_input)
-
2
output(m_file_output, medium => medium_params[medium]) do
-
2
roll in1.cut(from: 3, to: 5)
-
2
roll in1.cut(from: 3, to: 5)
-
end
-
end
-
2
].each_with_index do |script, i|
-
-
12
it "should work with video only and audio only, as input and as output (#{medium}##{i+1})" do
-
-
12
Ffmprb::File.temp(m_output_extname[medium]) do |m_output|
-
-
12
Ffmprb.process(@av_file_c_gor_9, m_input[medium], m_output, &script)
-
-
12
m_output.sample at: 2.5, medium => true, not_medium => false do |sample|
-
12
case medium
-
when :video
-
6
check_greenish! pixel_data(sample, 100, 100)
-
when :audio
-
12
expect{wave_data(m_output)}.not_to raise_error # NOTE audio format compat. check
-
6
check_note! (i < 3 ? :C6 : :G6), wave_data(sample)
-
end
-
end
-
-
-
12
expect(m_output.length).to be_approximately 4
-
12
expect{
-
12
m_output.sample at: 3, not_medium => true, medium => false
-
}.to raise_error Ffmprb::Error
-
end
-
-
end
-
-
end
-
end
-
end
-
-
1
context "stitching" do
-
-
1
it "should transition between two reels" do
-
1
Ffmprb.process(@av_file_c_gor_9, @av_file_e_bow_9, @av_out_file) do |input1, input2, output1|
-
-
1
in1, in2 = input(input1), input(input2)
-
1
output(output1) do
-
1
lay in1.crop(0.25), transition: {blend: 3}
-
1
lay in2.crop(left: 0, top: 0, width: 0.1, height: 0.1).cut(to: 8), after: 6, transition: {blend: 2}
-
end
-
-
end
-
-
1
last_green = nil
-
1
last_volume = nil
-
# NOTE should transition from black+silent to green+C6 in 3 secs
-
1
times = [0.1, 1.1, 2.1, 3.1, 4.1]
-
1
times.each do |at|
-
5
@av_out_file.sample at: at do |snap, sound|
-
5
pixel = pixel_data(snap, 100, 100)
-
5
check_greenish! pixel
-
5
if last_green
-
4
if at == times[-1]
-
1
expect(pixel.green).to eq last_green
-
else
-
3
expect(pixel.green).to be > last_green
-
end
-
end
-
5
last_green = pixel.green
-
-
5
wave = wave_data(sound)
-
5
check_note! :C6, wave
-
5
if last_volume
-
4
if at == times[-1]
-
1
expect(wave.volume).to be_approximately last_volume
-
else
-
3
expect(wave.volume).to be > last_volume
-
end
-
end
-
5
last_volume = wave.volume
-
end
-
end
-
-
1
last_red = nil
-
1
last_frequency = nil
-
# NOTE should transition from green+C6 to white+E6 in 2 secs
-
1
times = [4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5]
-
1
times.each do |at|
-
9
@av_out_file.sample at: at do |snap, sound|
-
9
pixel = pixel_data(snap, 100, 100)
-
-
9
check_greenish! pixel unless times[-2..-1].include? at
-
9
expect(0xFFFF - pixel.red).to be_approximately (0xFFFF - pixel.blue)
-
-
9
if last_red
-
8
if times.values_at(0..2, -1).include? at
-
3
expect(pixel.red).to eq last_red
-
else
-
5
expect(pixel.red).to be > last_red
-
end
-
end
-
9
last_red = pixel.red
-
-
9
wave = wave_data(sound)
-
-
9
if times[0..1].include? at
-
2
check_note! :C6, wave
-
7
elsif times[-2..-1].include? at
-
2
check_note! :E6, wave
-
else
-
5
expect(wave.frequency).to be > last_frequency
-
end
-
9
last_frequency = wave.frequency
-
end
-
end
-
-
# NOTE should transition from white+E6 to black+silent in 2 secs
-
# XXX times = [10.9, 11.9, 12.9]
-
-
1
expect(@av_out_file.length).to be_approximately 14
-
end
-
-
1
it "should run an external effect tool for a transition"
-
-
end
-
-
1
context :audio_overlay do
-
-
1
it "should overlay sound with volume" do
-
1
Ffmprb.process(@av_file_btn_wtb_16, @a_file_g_16, @av_out_file) do |input1, input2, output1|
-
-
1
in1 = input(input1)
-
1
in2 = input(input2)
-
1
output(output1) do
-
1
lay in1.volume(0 => 0.5, 4 => 0.5, 5 => 1)
-
1
overlay in2.cut(to: 5).volume(2.0 => 0, 4.0 => 1)
-
end
-
-
end
-
-
1
volume_first =
-
wave_data(@av_out_file.sample at: 0, video: false) do |sound|
-
expect(sound.frequency).to be_between NOTES.G6, NOTES.B6
-
sound.volume
-
end
-
-
1
check_av_btn_wtb_at! 2
-
-
1
wave_data(@av_out_file.sample at: 2, video: false) do |sound|
-
expect(sound.frequency).to be_approximately NOTES.B6
-
expect(sound.volume).to be < volume_first
-
end
-
-
1
wave_data(@av_out_file.sample at: 4, video: false) do |sound|
-
expect(sound.frequency).to be_between NOTES.G6, NOTES.B6
-
expect(sound.volume).to be_approximately volume_first
-
end
-
-
1
expect(
-
wave_data(@av_out_file.sample at: 9, video: false).frequency
-
).to be_approximately NOTES.B6
-
end
-
-
1
it "should loop and duck the overlay sound wrt the main sound" do
-
1
Ffmprb.process(@av_file_btn_wtb_16, @a_file_g_16, @av_out_file) do |input1, input2, output1|
-
-
1
in1 = input(input1)
-
1
in2 = input(input2)
-
1
output(output1, video: {resolution: '800x600'}) do
-
1
lay in1.loop(2), transition: {blend: 1}
-
1
overlay in2.loop, duck: :audio
-
end
-
-
end
-
-
1
@av_out_file.sample at: 2 do |snap, sound|
-
1
check_white! pixel_data(snap, 100, 100)
-
1
expect(wave_data(sound).frequency).to be_between(NOTES.G6, NOTES.B6)
-
end
-
-
1
@av_out_file.sample at: 6 do |snap, sound|
-
1
check_black! pixel_data(snap, 100, 100)
-
1
expect(wave_data(sound).frequency).to be_within(10).of NOTES.G6
-
end
-
-
1
expect(@av_out_file.resolution).to eq '800x600'
-
1
expect(@av_out_file.length).to be_approximately 32
-
end
-
-
1
it "should duck some overlay sound wrt some main sound" do
-
1
Ffmprb::Util::ThreadedIoBuffer.block_size.tap do |default|
-
1
begin
-
1
Ffmprb::Util::ThreadedIoBuffer.block_size = 1024 # NOTE to check for excessive memory consumption during looping etc
-
-
1
Ffmprb.process(@av_file_btn_wtb_16, @a_file_g_16, @av_out_file) do |input1, input2, output1|
-
-
1
in1 = input(input1)
-
1
in2 = input(input2)
-
1
output(output1) do
-
1
lay in1.cut(to: 10), transition: {blend: 1}
-
1
overlay in2.cut(from: 4).loop, duck: :audio
-
end
-
-
end
-
-
1
@av_out_file.sample at: 2 do |snap, sound|
-
1
check_white! pixel_data(snap, 100, 100)
-
1
expect(wave_data(sound).frequency).to be_between(NOTES.G6, NOTES.B6)
-
end
-
-
1
@av_out_file.sample at: 6 do |snap, sound|
-
1
check_black! pixel_data(snap, 100, 100)
-
1
expect(wave_data(sound).frequency).to be_within(10).of NOTES.G6
-
end
-
-
1
expect(@av_out_file.length).to be_approximately 10
-
ensure
-
1
Ffmprb::Util::ThreadedIoBuffer.block_size = default
-
end
-
end
-
end
-
-
1
it "should duck some overlay sound wrt some main sound" do
-
1
Ffmprb.process(@a_file_g_16, @a_out_file) do |input1, output1|
-
-
1
in1 = input(input1)
-
1
output(output1) do
-
1
roll in1.cut(from: 4, to: 12), transition: {blend: 1}
-
1
overlay in1, duck: :audio
-
end
-
-
end
-
-
1
expect(@a_out_file.length).to be_approximately(8)
-
-
1
[2, 6].each do |at|
-
2
check_note! :G6, wave_data(@a_out_file.sample_audio at: at)
-
end
-
end
-
-
end
-
-
1
context :samples do
-
-
1
it "should shoot snaps" # XXX not sure if this functionality is needed
-
-
end
-
-
end
-
-
1
context :info do
-
-
1
it "should return the length of a clip" do
-
1
expect(@av_file_c_gor_9.length).to be_approximately 9
-
end
-
-
end
-
-
1
def pixel_data(snap, x, y)
-
72
Magick::Image.read(snap.path)[0].pixel_color(x, y)
-
end
-
-
1
def wave_data(sound)
-
78
sox_info = Ffmprb::Util.sh(Sox::SOX_COMMAND, sound.path, '-n', 'stat', output: :stderr)
-
-
78
OpenStruct.new.tap do |data|
-
78
data.frequency = $1.to_f if sox_info =~ /Rough\W+frequency:\W*([\d.]+)/
-
78
data.frequency = 0 unless data.frequency && data.frequency > 0
-
78
data.volume = -$1.to_f if sox_info =~ /Volume\W+adjustment:\W*([\d.]+)/
-
78
data.volume ||= MIN_VOLUME
-
end
-
end
-
-
1
def channel_min(pixel)
-
12
[pixel.red, pixel.green, pixel.blue].min
-
end
-
-
1
def channel_max(pixel)
-
7
[pixel.red, pixel.green, pixel.blue].max
-
end
-
-
end
-
1
require 'mkfifo'
-
-
1
TST_STR_6K = 'Roger?' * 1024
-
-
1
describe Ffmprb::File do
-
-
1
around do |example|
-
8
Ffmprb::Util::ThreadedIoBuffer.block_size.tap do |default|
-
8
begin
-
8
Ffmprb::Util::ThreadedIoBuffer.block_size = 1024
-
8
example.run
-
ensure
-
8
Ffmprb::Util::ThreadedIoBuffer.block_size = default
-
end
-
end
-
end
-
-
1
it "should wrap ruby Files"
-
-
1
context "simple buffered fifos" do
-
-
1
around do |example|
-
Ffmprb::Util::Thread.new "test", main: true do
-
5
@fifo = Ffmprb::File.threaded_buffered_fifo '.ext'
-
5
example.run
-
5
Ffmprb::Util::Thread.join_children!
-
5
end.join
-
end
-
-
1
it "should have the destination readable (while writing to)" do
-
-
# piggy-backing another test
-
1
expect(@fifo[0].extname).to eq '.ext'
-
1
expect(@fifo[1].extname).to eq '.ext'
-
-
1
Timeout.timeout(4) do
-
1
file_out = File.open(@fifo[0].path, 'w')
-
1
file_in = File.open(@fifo[1].path, 'r')
-
-
1
writer = Thread.new do
-
1
512.times do
-
512
file_out.write(TST_STR_6K)
-
end
-
1
file_out.close
-
end
-
-
1
reader = Thread.new do
-
1
512.times do
-
512
expect(file_in.read(6*1024) == TST_STR_6K).to be_truthy
-
end
-
1
expect(file_in.read 1).to eq nil # EOF
-
1
file_in.close
-
end
-
-
1
writer.join
-
1
reader.join
-
end
-
end
-
-
1
it "should not timeout if the reader is a bit slow" do
-
1
Ffmprb::Util::ThreadedIoBuffer.timeout_limit.tap do |default|
-
1
begin
-
1
Ffmprb::Util::ThreadedIoBuffer.timeout_limit = 2
-
-
1
File.open(@fifo[0].path, 'w') do |file_out|
-
1
File.open(@fifo[1].path, 'r') do |file_in|
-
1
Timeout.timeout(8) do
-
1
thr = Thread.new do
-
1
file_out.write(TST_STR_6K * 512)
-
1
file_out.close
-
end
-
1
sleep 1
-
1
expect(file_in.read == TST_STR_6K * 512).to be_truthy
-
1
thr.join
-
1
Ffmprb::Util::Thread.join_children!
-
end
-
end
-
end
-
ensure
-
1
Ffmprb::Util::ThreadedIoBuffer.timeout_limit = default
-
end
-
end
-
end
-
-
1
it "should timeout if the reader is very slow" do
-
1
Ffmprb::Util::ThreadedIoBuffer.timeout_limit.tap do |default|
-
1
begin
-
1
Ffmprb::Util::ThreadedIoBuffer.timeout_limit = 2
-
-
1
File.open(@fifo[0].path, 'w') do |file_out|
-
1
File.open(@fifo[1].path, 'r') do |file_in|
-
1
Timeout.timeout(8) do
-
1
expect{
-
1
file_out.write(TST_STR_6K * 1024)
-
}.to raise_error Errno::EPIPE
-
end
-
end
-
end
-
1
expect{
-
1
Ffmprb::Util::Thread.join_children!
-
}.to raise_error Ffmprb::Error
-
ensure
-
1
Ffmprb::Util::ThreadedIoBuffer.timeout_limit = default
-
end
-
end
-
end
-
-
1
it "should be writable (before the destination is ever read), up to the buffer size(1024*1024)" do
-
1
Timeout.timeout(2) do
-
1
file_out = File.open(@fifo[0].path, 'w')
-
1
file_in = File.open(@fifo[1].path, 'r')
-
1
file_out.write(TST_STR_6K * 64)
-
1
file_out.close
-
1
expect(file_in.read == TST_STR_6K * 64).to be_truthy
-
1
file_in.close
-
end
-
end
-
-
1
it "should break the writer if the reader is broken" do
-
1
Timeout.timeout(2) do
-
1
file_out = File.open(@fifo[0].path, 'w')
-
1
file_in = File.open(@fifo[1].path, 'r')
-
1
thr = Thread.new do
-
1
begin
-
1
file_in.read(64)
-
ensure
-
1
file_in.close
-
end
-
end
-
1
expect {
-
1
begin
-
1
file_out.write(TST_STR_6K * 1024)
-
ensure
-
1
file_out.close
-
end
-
}.to raise_error Errno::EPIPE
-
1
thr.join
-
end
-
end
-
-
end
-
-
1
context "N-Tee buffering" do
-
-
1
around do |example|
-
3
temp_fifos = []
-
3
temp_fifos << @master_fifo = Ffmprb::File.temp_fifo
-
3
temp_fifos << @copy_fifo1 = Ffmprb::File.temp_fifo
-
3
temp_fifos << @copy_fifo2 = Ffmprb::File.temp_fifo
-
3
temp_fifos << @copy_fifo3 = Ffmprb::File.temp_fifo
-
-
3
begin
-
3
example.run
-
ensure
-
3
temp_fifos.each &:unlink
-
end
-
end
-
-
1
it "should feed readers everything the writer has written" do
-
1
Timeout.timeout(15) do
-
1
thrs = []
-
thrs << Thread.new do
-
1
File.open @copy_fifo1.path, 'r' do |file|
-
1
expect(file.read(6*1024) == TST_STR_6K).to be_truthy
-
end
-
1
end
-
thrs << Thread.new do
-
1
File.open @copy_fifo2.path, 'r' do |file|
-
1
512.times {
-
512
expect(file.read(6*1024) == TST_STR_6K).to be_truthy
-
512
sleep 0.001
-
}
-
end
-
1
end
-
thrs << Thread.new do
-
1
sleep 1
-
1
File.open @copy_fifo3.path, 'r' do |file|
-
1
expect(file.read == TST_STR_6K * 1024).to be_truthy
-
end
-
1
end
-
-
1
@master_fifo.threaded_buffered_copy_to @copy_fifo1, @copy_fifo2, @copy_fifo3
-
-
1
File.open @master_fifo.path, 'w' do |file|
-
1
file.write TST_STR_6K * 1024
-
end
-
-
1
thrs.each &:join
-
end
-
end
-
-
1
it "should pass on closed readers" do
-
1
Timeout.timeout(15) do
-
1
thrs = []
-
thrs << Thread.new do
-
1
File.open @copy_fifo1.path, 'r' do |file|
-
1
file.read 64
-
end
-
1
end
-
thrs << Thread.new do
-
1
File.open @copy_fifo2.path, 'r' do |file|
-
1
1024.times {
-
1024
file.read 1024
-
1024
sleep 0.001
-
}
-
end
-
1
File.open @copy_fifo3.path, 'r' do |file|
-
1
expect(file.read == TST_STR_6K * 1024).to be_truthy
-
end
-
1
end
-
-
1
@master_fifo.threaded_buffered_copy_to @copy_fifo1, @copy_fifo2, @copy_fifo3
-
-
1
File.open @master_fifo.path, 'w' do |file|
-
1
file.write(TST_STR_6K * 1024)
-
end
-
-
1
thrs.each &:join
-
end
-
end
-
-
1
it "should terminate once all readers are done or broken" do
-
1
Timeout.timeout(15) do
-
1
thrs = []
-
thrs << Thread.new do
-
1
File.open @copy_fifo1.path, 'r' do |file|
-
1
file.read 64
-
end
-
1
end
-
thrs << Thread.new do
-
1
File.open @copy_fifo2.path, 'r' do |file|
-
1
1024.times { |i|
-
1024
expect(file.read(1024).length).to eq 1024
-
1024
sleep 0.001
-
}
-
end
-
1
File.open @copy_fifo3.path, 'r' do |file|
-
1
file.read 64
-
end
-
1
end
-
-
1
@master_fifo.threaded_buffered_copy_to @copy_fifo1, @copy_fifo2, @copy_fifo3
-
-
1
expect {
-
1
File.open @master_fifo.path, 'w' do |file|
-
1
i = file.write(TST_STR_6K * 1024)
-
end
-
}.to raise_error Errno::EPIPE
-
-
1
thrs.each &:join
-
end
-
end
-
-
end
-
-
end