Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust non-media item filtering in ffprobe #6023

Merged
merged 3 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/models/master_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ def saveDerivativesHash(derivative_hash)

def reloadTechnicalMetadata!
# Reset ffprobe
@ffprobe = Avalon::FFprobe.new(FileLocator.new(file_location).location)
@ffprobe = Avalon::FFprobe.new(FileLocator.new(file_location))

# Formats like MP4 can be caught as both audio and video
# so the case statement flows in the preferred order
Expand Down
58 changes: 41 additions & 17 deletions lib/avalon/ffprobe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,71 @@

module Avalon
class FFprobe
# media_path should be the output of `FileLocator.new(file_location).location`
def initialize(media_path)
# @param [FileLocator] media_file a file locator instance for the media file
masaball marked this conversation as resolved.
Show resolved Hide resolved
def initialize(media_file)
@media_file = media_file
end

def json_output
return @json_output unless @json_output.nil?
return @json_output = {} unless valid_content_type?(@media_file)
ffprobe = Settings&.ffprobe&.path || 'ffprobe'
raw_output = `#{ffprobe} -i "#{media_path}" -v quiet -show_format -show_streams -count_packets -of json`
raw_output = `#{ffprobe} -i "#{@media_file.location}" -v quiet -show_format -show_streams -of json`
# $? is a variable for the exit status of the last executed process.
# Success == 0, any other value means the command failed in some way.
unless $?.exitstatus == 0
@json_output = {}
Rails.logger.error "File processing failed. Please ensure that FFprobe is installed and that the correct path is configured."
return
return @json_output = {}
end
@json_output = JSON.parse(raw_output).deep_symbolize_keys
@video_stream = @json_output[:streams].select { |stream| stream[:codec_type] == 'video' }.first
@json_output
end

def video?
# ffprobe treats plain text files as ANSI or ASCII art. This sets the codec type to video
# but leaves display aspect ratio `nil`. If display_aspect_ratio is nil, return false.
# ffprobe treats image files as a single frame video. This sets the codec type to video
# but the packet/frame count will equal 1. If packet count equals 1, return false.
return true if @video_stream && @video_stream[:display_aspect_ratio] && @video_stream[:nb_read_packets].to_i > 1
def video_stream
return unless json_output.present?
@video_stream ||= json_output[:streams].select { |stream| stream[:codec_type] == 'video' }.first
end

def audio_stream
return unless json_output.present?
@audio_stream ||= json_output[:streams].select { |stream| stream[:codec_type] == 'audio' }.first
end

false
def video?
video_stream.present?
end

def audio?
@json_output[:streams]&.any? { |stream| stream[:codec_type] == 'audio' }
audio_stream.present?
end

def duration
return unless video? || audio?
# ffprobe return duration as seconds. Existing Avalon logic expects milliseconds.
(@json_output[:format][:duration].to_f * 1000).to_i
(json_output[:format][:duration].to_f * 1000).to_i
end

def display_aspect_ratio
@video_stream[:display_aspect_ratio] if video?
if video? && video_stream[:display_aspect_ratio].present?
video_stream[:display_aspect_ratio]
elsif video?
video_stream[:width].to_f / video_stream[:height].to_f
end
end

def original_frame_size
"#{@video_stream[:width]}x#{@video_stream[:height]}" if video?
"#{video_stream[:width]}x#{video_stream[:height]}" if video?
end

private

def valid_content_type? media_file
masaball marked this conversation as resolved.
Show resolved Hide resolved
# Remove S3 credentials or other params from extension output
extension = File.extname(media_file.location)&.gsub(/[\?#].*/, '')
# Fall back on file extension if magic bytes fail to identify file
content_type = Marcel::MimeType.for media_file.reader, extension: extension

['audio', 'video'].any? { |type| content_type.include?(type) }
end
end
end
57 changes: 33 additions & 24 deletions spec/lib/avalon/ffprobe_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@
describe Avalon::FFprobe do
subject { described_class.new(test_file) }

let(:video_file) { Rails.root.join('spec', 'fixtures', 'videoshort.mp4').to_s }
let(:audioless_video_file) { Rails.root.join('spec', 'fixtures', 'videoshort_no_audio.mp4').to_s }
let(:audio_file) { Rails.root.join('spec', 'fixtures', 'jazz-performance.mp4').to_s }
let(:video_file) { FileLocator.new(Rails.root.join('spec', 'fixtures', 'videoshort.mp4').to_s) }
let(:audioless_video_file) { FileLocator.new(Rails.root.join('spec', 'fixtures', 'videoshort_no_audio.mp4').to_s) }
let(:audio_file) { FileLocator.new(Rails.root.join('spec', 'fixtures', 'jazz-performance.mp4').to_s) }
let(:image_file) { FileLocator.new(Rails.root.join('spec', 'fixtures', 'collection_poster.png').to_s) }
let(:text_file) { FileLocator.new(Rails.root.join('spec', 'fixtures', 'chunk_test.txt').to_s) }

describe 'error handling' do
let(:test_file) { video_file }

it 'logs an error if ffprobe is misconfigured' do
allow(Settings.ffprobe).to receive(:path).and_return('misconfigured/path')
expect(Rails.logger).to receive(:error)
subject
subject.json_output
end
end

Expand All @@ -50,12 +52,12 @@
end

context 'with non-media files' do
let(:text_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'chunk_test.txt')) }
let(:image_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'collection_poster.png')) }
let(:text_test) { described_class.new(text_file) }
let(:image_test) { described_class.new(image_file) }

it 'returns false' do
expect(text_file.video?).to be false
expect(image_file.video?).to be false
expect(text_test.video?).to be false
expect(image_test.video?).to be false
end
end
end
Expand Down Expand Up @@ -86,12 +88,12 @@
end

context 'with non-media files' do
let(:text_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'chunk_test.txt')) }
let(:image_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'collection_poster.png')) }
let(:text_test) { described_class.new(text_file) }
let(:image_test) { described_class.new(image_file) }

it 'returns false' do
expect(text_file.audio?).to be false
expect(image_file.audio?).to be false
expect(text_test.audio?).to be false
expect(image_test.audio?).to be false
end
end
end
Expand All @@ -104,12 +106,12 @@
end

context 'with non-media files' do
let(:text_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'chunk_test.txt')) }
let(:image_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'collection_poster.png')) }
let(:text_test) { described_class.new(text_file) }
let(:image_test) { described_class.new(image_file) }

it 'returns nil' do
expect(text_file.duration).to be nil
expect(image_file.duration).to be nil
expect(text_test.duration).to be nil
expect(image_test.duration).to be nil
end
end
end
Expand All @@ -121,6 +123,13 @@
it 'returns the display aspect ratio' do
expect(subject.display_aspect_ratio).to eq '20:11'
end

context 'file missing display aspect ratio' do
it 'calculates the display aspect ratio' do
subject.instance_variable_set(:@video_stream, subject.video_stream.except!(:display_aspect_ratio))
expect(subject.display_aspect_ratio).to eq(200.to_f / 110.to_f)
end
end
end

context 'with audio' do
Expand All @@ -131,12 +140,12 @@
end

context 'with non-media files' do
let(:text_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'chunk_test.txt')) }
let(:image_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'collection_poster.png')) }
let(:text_test) { described_class.new(text_file) }
let(:image_test) { described_class.new(image_file) }

it 'returns nil' do
expect(text_file.display_aspect_ratio).to be nil
expect(image_file.display_aspect_ratio).to be nil
expect(text_test.display_aspect_ratio).to be nil
expect(image_test.display_aspect_ratio).to be nil
end
end
end
Expand All @@ -159,12 +168,12 @@
end

context 'with non-media files' do
let(:text_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'chunk_test.txt')) }
let(:image_file) { described_class.new(Rails.root.join('spec', 'fixtures', 'collection_poster.png')) }
let(:text_test) { described_class.new(text_file) }
let(:image_test) { described_class.new(image_file) }

it 'returns nil' do
expect(text_file.original_frame_size).to be nil
expect(image_file.original_frame_size).to be nil
expect(text_test.original_frame_size).to be nil
expect(image_test.original_frame_size).to be nil
end
end
end
Expand Down