diff --git a/src/ffmpeg.ts b/src/ffmpeg.ts index fa41a1e..ba72c72 100644 --- a/src/ffmpeg.ts +++ b/src/ffmpeg.ts @@ -2,7 +2,7 @@ import * as cp from "child_process"; import * as fs from "fs"; import { ILogger } from "./Logger"; -async function spawnFfmpeg(logger: ILogger, ffmpegPath: string, argss: string[]): Promise { +export async function spawnFfmpeg(logger: ILogger, ffmpegPath: string, argss: string[]): Promise { return new Promise((resolve, reject) => { logger.log("Spawning FFMPEG", ffmpegPath, argss.join(" ")); diff --git a/test/ffmpeg.spec.ts b/test/ffmpeg.spec.ts new file mode 100644 index 0000000..12f5818 --- /dev/null +++ b/test/ffmpeg.spec.ts @@ -0,0 +1,111 @@ +jest.mock("child_process"); +jest.mock("fs"); + +import * as cp from "child_process"; +import * as EventEmitter from "events"; +import * as fs from "fs"; +import { spawnFfmpeg, mergeChunks, transmuxTsToMp4 } from "../src/ffmpeg"; + +describe("ffmpeg", () => { + const logger = { + error: jest.fn(), + log: jest.fn(), + }; + + function setupSpawnMock(): cp.ChildProcess { + const proc = new EventEmitter() as cp.ChildProcess; + (proc as any).stdout = new EventEmitter(); + (proc as any).stderr = new EventEmitter(); + + jest.spyOn(cp, "spawn").mockReturnValue(proc); + return proc; + } + async function wrapFfmpeg(cb: () => Promise): Promise { + const proc = setupSpawnMock(); + const promise = cb(); + proc.emit("close", 0); + await promise; + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("spawnFfmpeg", () => { + it("Should return a promise that resolves when the process closes", async () => { + await wrapFfmpeg(() => spawnFfmpeg(logger, "my-ffmpeg", ["arg1", "arg2"])); + + expect(cp.spawn).toBeCalledWith("my-ffmpeg", ["arg1", "arg2"]); + }); + + it("Should forward all messages and output to the logger", async () => { + const proc = setupSpawnMock(); + const promise = spawnFfmpeg(logger, "my-ffmpeg", ["arg1", "arg2"]); + proc.emit("message", "some message"); + proc.stdout.emit("data", "some stdout"); + proc.stderr.emit("data", "some stderr"); + proc.emit("close", 0); + await promise; + + expect(logger.log).toHaveBeenCalledTimes(4); + expect(logger.log).toHaveBeenNthCalledWith(2, "ffmpeg message:", "some message"); + expect(logger.log).toHaveBeenNthCalledWith(3, "ffmpeg stdout: some stdout"); + expect(logger.log).toHaveBeenNthCalledWith(4, "ffmpeg stderr: some stderr"); + }); + + it("Should throw if FFMPEG closes with an error", async () => { + const proc = setupSpawnMock(); + const promise = spawnFfmpeg(logger, "my-ffmpeg", ["arg1", "arg2"]); + proc.emit("close", 1); + + await expect(promise).rejects.toEqual("ffmpeg closed with status 1"); + expect(logger.error).toHaveBeenCalledWith("ffmpeg closed with status 1"); + }); + + it("Should throw if the process has an error", async () => { + const proc = setupSpawnMock(); + const promise = spawnFfmpeg(logger, "my-ffmpeg", ["arg1", "arg2"]); + proc.emit("error", "some error"); + + await expect(promise).rejects.toEqual("some error"); + expect(logger.error).toHaveBeenCalledWith("ffmpeg error:", "some error"); + }); + }); + + describe("mergeChunks", () => { + it("Should spawn a FFMPEG process with the correct parameters", async () => { + await wrapFfmpeg(() => mergeChunks(logger, "my-ffmpeg", ["segment1", "segment2"], "output-file")); + + expect(cp.spawn).toHaveBeenCalledWith("my-ffmpeg", [ + "-y", + "-loglevel", "warning", + "-f", "concat", + "-i", "ffmpeg-input.txt", + "-c", "copy", + "output-file", + ]); + }); + + it("Should write the segments to the temporary segments file then delete it", async () => { + await wrapFfmpeg(() => mergeChunks(logger, "my-ffmpeg", ["segment1", "segment2"], "output-file")); + + expect(fs.writeFileSync).toHaveBeenCalledWith("ffmpeg-input.txt", "file 'segment1'\nfile 'segment2'\n"); + expect(fs.unlinkSync).toHaveBeenCalledWith("ffmpeg-input.txt"); + }); + }); + + describe("transmuxTsToMp4", () => { + it("Should spawn a FFMPEG process with the correct parameters", async () => { + await wrapFfmpeg(() => transmuxTsToMp4(logger, "my-ffmpeg", "input-file", "output-file")); + + expect(cp.spawn).toHaveBeenCalledWith("my-ffmpeg", [ + "-y", + "-loglevel", "warning", + "-i", "input-file", + "-c", "copy", + "-bsf:a", "aac_adtstoasc", + "output-file", + ]); + }); + }); +});