Skip to content
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
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
== 8.3.1 2026-03-20

Fixes:
* Resolve no method error (`File.mv`) in `FFMPEG::Media#remux`.
* Resolve `FFMPEG::Transcoder::Status#success?` inconsistency when using remux-replace.

== 8.3.0 2026-03-19

Improvements:
* Introduce inline remux when there's no output path provided

== 8.2.0 2026-03-11
Expand Down
4 changes: 2 additions & 2 deletions lib/ffmpeg/media.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ def remux(output_path = nil, timeout: nil, &block)
status = Remuxer.new(timeout:).process(self, output_path, &block)

if status.success?
File.unlink @path
File.mv output_path, @path
FileUtils.mv output_path, @path
status.paths.map! { @path }
if @loaded
@loaded = false
load!
Expand Down
2 changes: 1 addition & 1 deletion lib/ffmpeg/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module FFMPEG
VERSION = '8.3.0'
VERSION = '8.3.1'
end
95 changes: 39 additions & 56 deletions spec/ffmpeg/media_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -597,24 +597,23 @@ module FFMPEG
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 'remuxes the media to the output path' do
status = subject.remux(output_path)

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)
expect(status).to be_a(Transcoder::Status)
expect(status.success?).to be(true)
expect(File.exist?(output_path)).to be(true)
expect(File.size(output_path)).to be > 0
end

it 'passes the timeout option to the Remuxer' do
remuxer = instance_double(Remuxer)
timeout = rand(999)

allow(remuxer).to receive(:process).and_return(instance_double(Transcoder::Status))
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
Expand All @@ -630,53 +629,33 @@ module FFMPEG

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
path = tmp_file(ext: 'mp4')
FileUtils.cp(fixture_media_file('landscape@4k60.mp4'), path)
path
end

let(:remuxer) { instance_double(Remuxer) }
it 'remuxes the file in place' do
original_duration = subject.duration
status = subject.remux

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
expect(status).to be_a(Transcoder::Status)
expect(status.success?).to be(true)
expect(File.exist?(path)).to be(true)
expect(File.size(path)).to be > 0
expect(subject.duration).to be_within(0.5).of(original_duration)
end

context 'when the remux fails' do
let(:remuxer) { instance_double(Remuxer) }
let(:remux_status) { instance_double(Transcoder::Status, success?: false) }

before do
allow(Remuxer).to receive(:new).and_return(remuxer)
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)
expect(FileUtils).not_to receive(:mv)
subject.remux
end

Expand All @@ -690,25 +669,29 @@ module FFMPEG

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)
let(:output_path) { tmp_file(ext: 'mp4') }

expect(subject).to receive(:remux).with(output_path, timeout: nil).and_return(status)
expect(status).to receive(:assert!).and_return(status)
it 'remuxes the media and returns the status' do
status = subject.remux!(output_path)

subject.remux!(output_path)
expect(status).to be_a(Transcoder::Status)
expect(status.success?).to be(true)
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)
let(:path) do
dst = tmp_file(ext: 'mp4')
FileUtils.cp(fixture_media_file('widescreen-no-audio.mp4'), dst)
dst
end

expect(subject).to receive(:remux).with(nil, timeout: nil).and_return(status)
expect(status).to receive(:assert!).and_return(status)
it 'remuxes the file in place and returns the status' do
status = subject.remux!

subject.remux!
expect(status).to be_a(Transcoder::Status)
expect(status.success?).to be(true)
expect(File.exist?(path)).to be(true)
end
end
end
Expand Down
Loading