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
106 changes: 106 additions & 0 deletions tests/ffmpeg-core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,109 @@ describe(genName("state between calls"), () => {
core.FS.unlink("probe.json");
});
});

describe(genName("exec() repeated calls"), () => {
beforeEach(reset);

it("should produce identical output across runs", () => {
const outputs = [];
for (let i = 0; i < 5; i++) {
expect(
core.exec(
"-f", "lavfi", "-i", "testsrc=d=0.3:s=32x24:r=10",
"-pix_fmt", "yuv420p", "-f", "yuv4mpegpipe", "repeat.y4m"
)
).to.equal(0);
outputs.push(core.FS.readFile("repeat.y4m"));
core.FS.unlink("repeat.y4m");
}
const [first, ...rest] = outputs;
expect(first.length).to.not.equal(0);
rest.forEach((out) => expect(out).to.deep.equal(first));
});

it("should not carry inputs and outputs over to the next run", () => {
expect(core.exec("-f", "lavfi", "-i", "sine=d=0.2", "carry.wav")).to.equal(0);
expect(core.exec("-f", "lavfi", "-i", "testsrc=d=0.2:s=16x16:r=5", "carry.mp4")).to.equal(0);
expect(probe("carry.mp4").streams.map((s) => s.codec_type)).to.deep.equal(["video"]);
core.FS.unlink("carry.wav");
core.FS.unlink("carry.mp4");
});
});

describe(genName("ffprobe() repeated calls"), () => {
beforeEach(reset);

const probeStreams = (...args) => {
core.ffprobe("-v", "error", "-print_format", "json", ...args, "video.mp4", "-o", "probe.json");
const json = JSON.parse(new TextDecoder().decode(core.FS.readFile("probe.json")));
core.FS.unlink("probe.json");
return json;
};

it("should not keep -select_streams in a later call", () => {
expect(probeStreams("-show_streams", "-select_streams", "a").streams).to.have.lengthOf(0);
expect(probeStreams("-show_streams").streams).to.have.lengthOf(1);
});

it("should keep working when calls alternate with exec()", () => {
for (let i = 0; i < 3; i++) {
expect(probeStreams("-show_format").format.nb_streams).to.equal(1);
expect(core.exec("-f", "lavfi", "-i", "sine=d=0.1", "alt.wav")).to.equal(0);
core.FS.unlink("alt.wav");
}
});
});

describe(genName("exec() exit codes"), () => {
beforeEach(reset);

const run = (...args) => {
const logs = [];
core.setLogger(({ message }) => logs.push(message));
const ret = core.exec(...args);
return { ret, log: logs.join("\n") };
};

it("should return non-zero for an unknown option", () => {
const { ret, log } = run("-definitely-not-an-option");
expect(ret).to.not.equal(0);
expect(log).to.match(/Unrecognized option/);
});

it("should return non-zero when the input is missing", () => {
const { ret, log } = run("-i", "missing.mp4", "missing.avi");
expect(ret).to.not.equal(0);
expect(log).to.match(/No such file or directory/);
});

it("should return non-zero for an unknown encoder", () => {
const { ret } = run("-f", "lavfi", "-i", "nullsrc=s=16x16:d=0.1", "-c:v", "no-such-encoder", "bad.mp4");
expect(ret).to.not.equal(0);
});

it("should return non-zero when no output is given", () => {
expect(run("-f", "lavfi", "-i", "nullsrc=s=16x16:d=0.1").ret).to.not.equal(0);
});

it("should return the value left in core.ret", () => {
expect(core.exec("-definitely-not-an-option")).to.equal(core.ret);
});

it("should not throw when an option is missing its argument", () => {
expect(() => core.exec("-i")).to.not.throw();
});
});

describe(genName("reset() handlers"), () => {
it("should keep the logger and progress handlers", () => {
const logger = () => {};
const progress = () => {};
core.setLogger(logger);
core.setProgress(progress);
core.reset();
expect(core.logger).to.equal(logger);
expect(core.progress).to.equal(progress);
reset();
});
});
135 changes: 135 additions & 0 deletions tests/ffmpeg-node.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const corePath = fileURLToPath(

const load = (ffmpeg) => ffmpeg.load(coreURL ? { coreURL } : {});

// Endless input, so a command cannot finish before the call is settled.
const ENDLESS = ["-f", "lavfi", "-i", "testsrc=d=600:s=16x16", "-f", "null", "-"];

// fetchFile()'s local-path and file: URL branches only run under Node.js,
// so the browser test suite can't cover them; both core-variant commands
// run this same suite, but fetchFile() itself doesn't touch the core.
Expand Down Expand Up @@ -372,4 +375,136 @@ describe(genName("FFmpeg.terminate()"), function () {
ffmpeg.terminate();
}
});

const message = async (promise) => {
try {
await promise;
} catch (err) {
return String(err.message ?? err);
}
return undefined;
};

it("rejects pending calls with the terminate error", async () => {
const ffmpeg = new FFmpeg();
await load(ffmpeg);
const pending = ffmpeg.exec(ENDLESS);
ffmpeg.terminate();
expect(await message(pending)).to.match(/terminate/);
});

it("rejects calls made after terminate()", async () => {
const ffmpeg = new FFmpeg();
await load(ffmpeg);
ffmpeg.terminate();
expect(await message(ffmpeg.listDir("/"))).to.match(/not loaded/);
});

it("loads again with a fresh file system", async () => {
const ffmpeg = new FFmpeg();
await load(ffmpeg);
await ffmpeg.writeFile("/before", "x");
ffmpeg.terminate();
try {
expect(await load(ffmpeg)).to.be.true;
const names = (await ffmpeg.listDir("/")).map(({ name }) => name);
expect(names).to.not.include("before");
} finally {
ffmpeg.terminate();
}
});

it("does nothing when never loaded", () => {
expect(() => new FFmpeg().terminate()).to.not.throw();
});
});

describe(genName("abort signal"), function () {
this.timeout(60000);

const abortName = async (call) => {
const controller = new AbortController();
const pending = call(controller.signal);
controller.abort();
try {
await pending;
} catch (err) {
return err.name;
}
return undefined;
};

it("rejects exec() and file calls with an AbortError", async () => {
const ffmpeg = new FFmpeg();
await load(ffmpeg);
try {
expect(
await abortName((signal) => ffmpeg.exec(ENDLESS, -1, { signal }))
).to.equal("AbortError");
expect(
await abortName((signal) => ffmpeg.listDir("/", { signal }))
).to.equal("AbortError");
} finally {
ffmpeg.terminate();
}
});
});

describe(genName("repeated calls and exit codes"), function () {
this.timeout(60000);

let ffmpeg;

before(async () => {
ffmpeg = new FFmpeg();
await load(ffmpeg);
await ffmpeg.writeFile("video.mp4", b64ToUint8Array(VIDEO_1S_MP4));
});

after(() => {
ffmpeg.terminate();
});

it("produces identical output across exec() runs", async () => {
const outputs = [];
for (let i = 0; i < 3; i++) {
const ret = await ffmpeg.exec([
"-f", "lavfi", "-i", "testsrc=d=0.3:s=32x24:r=10",
"-pix_fmt", "yuv420p", "-f", "yuv4mpegpipe", "repeat.y4m",
]);
expect(ret).to.equal(0);
outputs.push(Buffer.from(await ffmpeg.readFile("repeat.y4m")));
await ffmpeg.deleteFile("repeat.y4m");
}
expect(outputs[0].length).to.not.equal(0);
for (const out of outputs) expect(out).to.deep.equal(outputs[0]);
});

it("does not keep ffprobe options between calls", async () => {
const streams = async (...args) => {
await ffmpeg.ffprobe([
"-v", "error", "-print_format", "json", ...args, "video.mp4", "-o", "p.json",
]);
const json = JSON.parse(await ffmpeg.readFile("p.json", "utf8"));
await ffmpeg.deleteFile("p.json");
return json.streams;
};
expect(await streams("-show_streams", "-select_streams", "a")).to.have.lengthOf(0);
expect(await streams("-show_streams")).to.have.lengthOf(1);
});

it("returns non-zero for bad arguments, a missing input and an unknown encoder", async () => {
expect(await ffmpeg.exec(["-definitely-not-an-option"])).to.not.equal(0);
expect(await ffmpeg.exec(["-i", "missing.mp4", "missing.avi"])).to.not.equal(0);
expect(
await ffmpeg.exec([
"-f", "lavfi", "-i", "nullsrc=s=16x16:d=0.1", "-c:v", "no-such-encoder", "bad.mp4",
])
).to.not.equal(0);
});

it("runs again after a failure", async () => {
expect(await ffmpeg.exec(["-definitely-not-an-option"])).to.not.equal(0);
expect(await ffmpeg.exec(["-i", "video.mp4", "-f", "null", "-"])).to.equal(0);
});
});
122 changes: 122 additions & 0 deletions tests/ffmpeg.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,125 @@ describe(genName("helpers (probe(), transcode(), extractFrames())"), function ()
expect(names.filter((n) => n.startsWith("ffmpeg-wasm-job-"))).to.deep.equal([]);
});
});

// Endless input, so the command cannot finish before the call is settled.
const ENDLESS = ["-f", "lavfi", "-i", "testsrc=d=600:s=16x16", "-f", "null", "-"];

describe(genName("abort signal"), function () {
it("should reject exec() with an AbortError", async () => {
const ffmpeg = await createFFmpeg();
try {
const controller = new AbortController();
const pending = ffmpeg.exec(ENDLESS, -1, { signal: controller.signal });
controller.abort();
let name;
try {
await pending;
} catch (err) {
name = err.name;
}
expect(name).to.equal("AbortError");
} finally {
ffmpeg.terminate();
}
});

it("should reject file calls with an AbortError", async () => {
const ffmpeg = await createFFmpeg();
try {
const controller = new AbortController();
const pending = ffmpeg.listDir("/", { signal: controller.signal });
controller.abort();
let name;
try {
await pending;
} catch (err) {
name = err.name;
}
expect(name).to.equal("AbortError");
} finally {
ffmpeg.terminate();
}
});
});

describe(genName("FFmpeg.terminate()"), function () {
const rejection = async (promise) => {
try {
await promise;
} catch (err) {
return String(err.message ?? err);
}
return undefined;
};

it("should reject pending calls with the terminate error", async () => {
const ffmpeg = await createFFmpeg();
const pending = ffmpeg.exec(ENDLESS);
ffmpeg.terminate();
expect(await rejection(pending)).to.match(/terminate/);
});

it("should reject calls made after terminate()", async () => {
const ffmpeg = await createFFmpeg();
ffmpeg.terminate();
expect(await rejection(ffmpeg.listDir("/"))).to.match(/not loaded/);
});

it("should load again with a fresh file system", async () => {
const ffmpeg = await createFFmpeg();
await ffmpeg.writeFile("/before", "x");
ffmpeg.terminate();
const config = { coreURL: CORE_URL, thread: FFMPEG_TYPE === "mt" };
try {
expect(await ffmpeg.load(config)).to.equal(true);
const names = (await ffmpeg.listDir("/")).map(({ name }) => name);
expect(names).to.not.include("before");
} finally {
ffmpeg.terminate();
}
});

it("should do nothing when never loaded", () => {
expect(() => new FFmpeg().terminate()).to.not.throw();
});
});

describe(genName("load() on a loaded instance"), function () {
it("should keep the loaded core and its files", async () => {
const ffmpeg = await createFFmpeg();
try {
await ffmpeg.writeFile("/kept.txt", "hello");
const config = { coreURL: CORE_URL, thread: FFMPEG_TYPE === "mt" };
expect(await ffmpeg.load(config)).to.equal(false);
expect(await ffmpeg.readFile("/kept.txt", "utf8")).to.equal("hello");
} finally {
ffmpeg.terminate();
}
});
});

describe(genName("FFmpeg.exec() exit codes"), function () {
let ffmpeg;

before(async () => {
ffmpeg = await createFFmpeg();
});

after(() => {
ffmpeg.terminate();
});

it("should return non-zero for bad arguments, a missing input and an unknown encoder", async () => {
expect(await ffmpeg.exec(["-definitely-not-an-option"])).to.not.equal(0);
expect(await ffmpeg.exec(["-i", "missing.mp4", "missing.avi"])).to.not.equal(0);
expect(
await ffmpeg.exec(["-f", "lavfi", "-i", "nullsrc=s=16x16:d=0.1", "-c:v", "no-such-encoder", "bad.mp4"])
).to.not.equal(0);
});

it("should run again after a failure", async () => {
expect(await ffmpeg.exec(["-definitely-not-an-option"])).to.not.equal(0);
expect(await ffmpeg.exec(["-f", "lavfi", "-i", "nullsrc=s=16x16:d=0.1", "-f", "null", "-"])).to.equal(0);
});
});
Loading