From 7c8815868047d03b4e596025fabbeaf3a66ae0e3 Mon Sep 17 00:00:00 2001 From: Zsolt Kozaroczy Date: Thu, 12 Nov 2026 13:22:39 +0100 Subject: [PATCH 1/4] fix(remux): drop undecodable audio streams before remux The old plain `-c copy` only kept the first audio stream during remuxig the media. However, if the first audio is not decodable by ffmpeg (e.g.: apac in a sample video), later ffmpeg executions will fail, even if it would be able to execute the transcode using the non-default audio stream. The new code detects if there are any non-decodable audio streams and pick the first decodable if need to. --- lib/ffmpeg/remuxer.rb | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/ffmpeg/remuxer.rb b/lib/ffmpeg/remuxer.rb index 56ab892..d29aead 100644 --- a/lib/ffmpeg/remuxer.rb +++ b/lib/ffmpeg/remuxer.rb @@ -52,7 +52,10 @@ def annexb?(media) def process(media, output_path, &) media = Media.new(media, load: false) unless media.is_a?(Media) - status = ffmpeg_copy(media, output_path, &) + audio_map = decodable_audio_map_patch(media) + copy_map = audio_map.nil? ? [] : ['-map', '0:v:0?', *audio_map] + + status = ffmpeg_copy(media, *copy_map, output_path, &) return status if status.success? return status unless self.class.annexb?(media) @@ -64,7 +67,7 @@ def process(media, output_path, &) return annexb_status unless annexb_status.success? mka_path = File.join(tmpdir, 'remux.mka') - mka_status = ffmpeg_copy(media, '-vn', mka_path, &) + mka_status = ffmpeg_copy(media, *(audio_map.nil? ? ['-vn'] : audio_map), mka_path, &) return mka_status unless mka_status.success? video = annexb_status.media.first @@ -125,6 +128,22 @@ def ffmpeg_copy(media, *args, inargs: [], &) ) end + # Maps the first audio stream that ffmpeg can decode. Without an explicit + # map, ffmpeg picks the audio stream with the most channels, which can be an + # undecodable one (e.g. iPhone spatial audio, codec_name "unknown") and + # drops the usable stream. Returns nil if there is no decodable audio or all + # audio streams are decodable + def decodable_audio_map_patch(media) + decodable, undecodable = media.audio_streams.partition { decodable_audio?(_1) } + return nil if undecodable.empty? + + decodable.first&.then { ['-map', "0:#{_1.index}"] } + end + + def decodable_audio?(stream) + stream.supported? && !['', 'unknown', 'none'].include?(stream.codec_name.to_s) + end + def annexb_filter(media) ['-bsf:v', "#{media.video_codec_name}_mp4toannexb"] end From 04467baade9d1a8d308c7217796022622126420c Mon Sep 17 00:00:00 2001 From: Zsolt Kozaroczy Date: Thu, 12 Nov 2026 14:44:30 +0100 Subject: [PATCH 2/4] fix(remux): reset internal data properly on load Running the remux could change the number of audio streams as it only keeps the first one. However, the memoized audio_streams instance variable was never cleaned up, hence a later ffmpeg command tried to map the second audio stream (marked decodable using the cache, but it had a different index by that time) and generated a non-valid ffmpeg command. New code cleans up the memoized instance variables to avoid this situation. --- lib/ffmpeg/media.rb | 7 +++++++ spec/ffmpeg/media_spec.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/lib/ffmpeg/media.rb b/lib/ffmpeg/media.rb index c347456..e16ec7d 100644 --- a/lib/ffmpeg/media.rb +++ b/lib/ffmpeg/media.rb @@ -228,6 +228,13 @@ def load! @size = @metadata[:format][:size].to_i @streams = @metadata[:streams].map { |metadata| Stream.new(metadata, stderr) } + %i[ + @video_streams + @default_video_stream + @audio_streams + @default_audio_stream + @codec_names + ].each { remove_instance_variable(_1) if instance_variable_defined? _1 } @tags = @metadata[:format][:tags] @format_name = @metadata[:format][:format_name] diff --git a/spec/ffmpeg/media_spec.rb b/spec/ffmpeg/media_spec.rb index 1b84d9e..fa07303 100644 --- a/spec/ffmpeg/media_spec.rb +++ b/spec/ffmpeg/media_spec.rb @@ -48,6 +48,36 @@ module FFMPEG subject.load! end + context 'when the media is loaded again after its streams changed' do + let(:old_fixture) { fixture_media_file('widescreen-multi-audio.mp4') } + let(:new_fixture) { fixture_media_file('widescreen-no-audio.mp4') } + let(:tmpdir) { Dir.mktmpdir } + let(:path) { File.join(tmpdir, 'media.mp4') } + + before { FileUtils.cp(old_fixture, path) } + after { FileUtils.remove_entry(tmpdir) } + + it 'resets the memoized values calculated from the streams' do + subject.load! + old_streams = subject.streams + + expect(subject.audio_streams).not_to be_empty + expect(subject.default_audio_stream).not_to be_nil + expect(subject.audio_streams?).to be(true) + + # The spec helper caches ffprobe output by arguments, so drop it to see the new file. + FFMPEG.instance_variable_set(:@ffprobe_cache, nil) + FileUtils.cp(new_fixture, path) + subject.instance_variable_set(:@loaded, false) + subject.load! + + expect(subject.streams).not_to eq(old_streams) + expect(subject.audio_streams).to be_empty + expect(subject.default_audio_stream).to be_nil + expect(subject.audio_streams?).to be(false) + end + end + context 'when the file does not exist' do let(:path) { fixture_media_file('missing.mp4') } From d6fb69deb1ad401105be321029789ea5c5516ef8 Mon Sep 17 00:00:00 2001 From: Zsolt Kozaroczy Date: Thu, 12 Nov 2026 15:57:23 +0100 Subject: [PATCH 3/4] Fix rubocop offenses --- spec/ffmpeg/media_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/ffmpeg/media_spec.rb b/spec/ffmpeg/media_spec.rb index fa07303..25f7d0b 100644 --- a/spec/ffmpeg/media_spec.rb +++ b/spec/ffmpeg/media_spec.rb @@ -481,10 +481,9 @@ module FFMPEG video_profile: 'High', video_codec_name: 'h264', video_bit_rate: 41_401_600, - # rubocop:disable Layout/LineLength + # rubocop:disable-next Layout/LineLength video_overview: %r{h264 \(High\) \(avc1 / 0x31637661\), yuvj420p\(pc, bt709/bt709/bt709, (progressive|unknown)\), 3840x2160 \[SAR 1:1 DAR 16:9\]}, - # rubocop:enable Layout/LineLength video_tags: { encoder: 'Lavc61.19.100 libx264', handler_name: 'VideoHandle', From 616007e0eec5f68c7f457a82a3e4b4335b5b217a Mon Sep 17 00:00:00 2001 From: Zsolt Kozaroczy Date: Thu, 12 Nov 2026 15:40:33 +0100 Subject: [PATCH 4/4] chore: Bump version to 8.5.1 and document changes --- CHANGELOG | 4 ++++ lib/ffmpeg/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 01bb4b9..13283bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +== 8.5.1 2026-11-12 +* Fix re-loading the media after a successful remux +* Pick decodable audio stream over a non-decodable during remux + == 8.5.0 2026-07-16 Improvements: diff --git a/lib/ffmpeg/version.rb b/lib/ffmpeg/version.rb index d6dcbc1..1fe8e97 100644 --- a/lib/ffmpeg/version.rb +++ b/lib/ffmpeg/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FFMPEG - VERSION = '8.5.0' + VERSION = '8.5.1' end