diff --git a/CHANGELOG b/CHANGELOG index 9c7991a..2c166ee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== 8.3.0 2026-03-19 +* Introduce inline remux when there's no output path provided + == 8.2.0 2026-03-11 Improvements: diff --git a/lib/ffmpeg/media.rb b/lib/ffmpeg/media.rb index bd63ac7..0da019e 100644 --- a/lib/ffmpeg/media.rb +++ b/lib/ffmpeg/media.rb @@ -85,22 +85,42 @@ def initialize(path, *ffprobe_args, load: true, autoload: true) # extraction, it falls back to extracting raw streams and re-muxing with # a corrected frame rate. # - # @param output_path [String, Pathname] The output path for the remuxed file. + # @param output_path [String, Pathname, nil] The output path for the remuxed file. + # Tries an inline replacement for nil value. # @param timeout [Integer, nil] Timeout in seconds for each ffmpeg command. # @yield [report] Reports from the ffmpeg command (see FFMPEG::Reporters). # @return [FFMPEG::Transcoder::Status] - def remux(output_path, timeout: nil, &block) - Remuxer.new(timeout:).process(self, output_path, &block) + def remux(output_path = nil, timeout: nil, &block) + return Remuxer.new(timeout:).process(self, output_path, &block) if output_path + raise ArgumentError if remote? + + Dir.mktmpdir do |tmpdir| + output_path = File.join(tmpdir, File.basename(@path)) + + status = Remuxer.new(timeout:).process(self, output_path, &block) + + if status.success? + File.unlink @path + File.mv output_path, @path + if @loaded + @loaded = false + load! + end + end + + status + end end # Remuxes the media file to the given output path via stream copy, # raising an error if the remux fails. # - # @param output_path [String, Pathname] The output path for the remuxed file. + # @param output_path [String, Pathname, nil] The output path for the remuxed file. + # Tries an inline replacement for nil value. # @param timeout [Integer, nil] Timeout in seconds for each ffmpeg command. # @yield [report] Reports from the ffmpeg command (see FFMPEG::Reporters). # @return [FFMPEG::Transcoder::Status] - def remux!(output_path, timeout: nil, &block) + def remux!(output_path = nil, timeout: nil, &block) remux(output_path, timeout:, &block).assert! end diff --git a/lib/ffmpeg/version.rb b/lib/ffmpeg/version.rb index c31ff83..96de165 100644 --- a/lib/ffmpeg/version.rb +++ b/lib/ffmpeg/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FFMPEG - VERSION = '8.2.0' + VERSION = '8.3.0' end diff --git a/spec/ffmpeg/media_spec.rb b/spec/ffmpeg/media_spec.rb index d855541..43b59fb 100644 --- a/spec/ffmpeg/media_spec.rb +++ b/spec/ffmpeg/media_spec.rb @@ -594,6 +594,125 @@ module FFMPEG end end + describe '#remux' do + context 'with an output_path' do + let(:output_path) { tmp_file(ext: 'mp4') } + let(:remuxer) { instance_double(Remuxer) } + let(:status) { instance_double(Transcoder::Status) } + + before do + allow(Remuxer).to receive(:new).and_return(remuxer) + allow(remuxer).to receive(:process).and_return(status) + end + + it 'delegates to a new Remuxer with the given output path' do + expect(Remuxer).to receive(:new).with(timeout: nil).and_return(remuxer) + expect(remuxer).to receive(:process).with(subject, output_path).and_return(status) + expect(subject.remux(output_path)).to be(status) + end + + it 'passes the timeout option to the Remuxer' do + timeout = rand(999) + expect(Remuxer).to receive(:new).with(timeout: timeout).and_return(remuxer) + expect(remuxer).to receive(:process).with(subject, output_path).and_return(status) + subject.remux(output_path, timeout: timeout) + end + end + + context 'without an output_path' do + context 'when the media is remote' do + let(:path) { fixture_media_file('landscape@4k60.mp4', remote: true) } + + it 'raises ArgumentError' do + expect { subject.remux }.to raise_error(ArgumentError) + end + end + + context 'when the media is local' do + let(:path) do + dst = tmp_file(ext: 'mp4') + FileUtils.cp(fixture_media_file('widescreen-no-audio.mp4'), dst) + dst + end + + let(:remuxer) { instance_double(Remuxer) } + + before do + allow(Remuxer).to receive(:new).and_return(remuxer) + end + + context 'when the remux succeeds' do + let(:remux_status) { instance_double(Transcoder::Status, success?: true) } + + before do + allow(remuxer).to receive(:process).and_return(remux_status) + allow(File).to receive(:unlink) + allow(File).to receive(:mv) + end + + it 'unlinks the original file and moves the remuxed output in its place' do + expect(File).to receive(:unlink).with(path) + expect(File).to receive(:mv).with(an_instance_of(String), path) + subject.remux + end + + it 'reloads the media metadata' do + subject # force initialization before setting expectation + expect(subject).to receive(:load!).once.and_call_original + subject.remux + end + + it 'returns the status' do + expect(subject.remux).to be(remux_status) + end + end + + context 'when the remux fails' do + let(:remux_status) { instance_double(Transcoder::Status, success?: false) } + + before do + allow(remuxer).to receive(:process).and_return(remux_status) + end + + it 'does not modify the original file' do + expect(File).not_to receive(:unlink) + expect(File).not_to receive(:mv) + subject.remux + end + + it 'returns the status' do + expect(subject.remux).to be(remux_status) + end + end + end + end + end + + describe '#remux!' do + context 'with an output_path' do + it 'calls assert! on the result of #remux' do + output_path = tmp_file(ext: 'mp4') + status = instance_double(Transcoder::Status) + + expect(subject).to receive(:remux).with(output_path, timeout: nil).and_return(status) + expect(status).to receive(:assert!).and_return(status) + + subject.remux!(output_path) + end + end + + context 'without an output_path' do + it 'calls assert! on the result of #remux without output_path' do + status = instance_double(Transcoder::Status) + + expect(subject).to receive(:remux).with(nil, timeout: nil).and_return(status) + expect(status).to receive(:assert!).and_return(status) + + subject.remux! + end + end + end + describe '#ffmpeg_execute' do it 'executes a ffmpeg command with the media as input' do reports = []