181 lines
7.8 KiB
Swift
181 lines
7.8 KiB
Swift
|
|
import XCTest
|
||
|
|
import Foundation
|
||
|
|
import ForgeColor
|
||
|
|
import ForgeGrade
|
||
|
|
@testable import ForgeProxy
|
||
|
|
|
||
|
|
final class FFmpegCommandTests: XCTestCase {
|
||
|
|
|
||
|
|
// ProRes proxy preset argv golden.
|
||
|
|
func testProResProxyArgs() {
|
||
|
|
let cmd = FFmpegCommandBuilder.build(
|
||
|
|
input: "/media/A001C001.mov",
|
||
|
|
output: "/proxies/A001C001.mov",
|
||
|
|
preset: .proresProxy,
|
||
|
|
scale: .half,
|
||
|
|
lutPath: nil,
|
||
|
|
burnIn: nil)
|
||
|
|
XCTAssertEqual(cmd.first, "-y")
|
||
|
|
XCTAssertTrue(cmd.contains("-i"))
|
||
|
|
XCTAssertTrue(cmd.contains("/media/A001C001.mov"))
|
||
|
|
XCTAssertTrue(cmd.contains("prores_ks"))
|
||
|
|
// Profile 0 = proxy.
|
||
|
|
let profileIdx = cmd.firstIndex(of: "-profile:v")!
|
||
|
|
XCTAssertEqual(cmd[profileIdx + 1], "0")
|
||
|
|
XCTAssertTrue(cmd.contains("scale=iw/2:ih/2"))
|
||
|
|
XCTAssertEqual(cmd.last, "/proxies/A001C001.mov")
|
||
|
|
}
|
||
|
|
|
||
|
|
// H264 preset.
|
||
|
|
func testH264Args() {
|
||
|
|
let cmd = FFmpegCommandBuilder.build(
|
||
|
|
input: "in.mov", output: "out.mp4",
|
||
|
|
preset: .h264, scale: .quarter, lutPath: nil, burnIn: nil)
|
||
|
|
XCTAssertTrue(cmd.contains("libx264"))
|
||
|
|
XCTAssertTrue(cmd.contains("scale=iw/4:ih/4"))
|
||
|
|
}
|
||
|
|
|
||
|
|
// DNxHR preset.
|
||
|
|
func testDNxHRArgs() {
|
||
|
|
let cmd = FFmpegCommandBuilder.build(
|
||
|
|
input: "in.mov", output: "out.mxf",
|
||
|
|
preset: .dnxhrLB, scale: .full, lutPath: nil, burnIn: nil)
|
||
|
|
XCTAssertTrue(cmd.contains("dnxhd"))
|
||
|
|
XCTAssertTrue(cmd.contains("dnxhr_lb"))
|
||
|
|
// Full scale: no scale filter unless LUT/burn present.
|
||
|
|
XCTAssertFalse(cmd.joined(separator: " ").contains("scale="))
|
||
|
|
}
|
||
|
|
|
||
|
|
// LUT filter: lut3d with escaped path joins filter chain before scale.
|
||
|
|
func testLutFilter() {
|
||
|
|
let cmd = FFmpegCommandBuilder.build(
|
||
|
|
input: "in.mov", output: "out.mov",
|
||
|
|
preset: .proresProxy, scale: .half,
|
||
|
|
lutPath: "/tmp/my look's.cube", burnIn: nil)
|
||
|
|
let vf = cmd[cmd.firstIndex(of: "-vf")! + 1]
|
||
|
|
XCTAssertTrue(vf.contains("lut3d="))
|
||
|
|
// ffmpeg filter escaping: ' -> '\'' inside quoted value, or backslash escapes.
|
||
|
|
XCTAssertTrue(vf.contains("my look"))
|
||
|
|
XCTAssertTrue(vf.contains("lut3d="), "lut3d must precede scale")
|
||
|
|
XCTAssertLessThan(vf.range(of: "lut3d=")!.lowerBound, vf.range(of: "scale=")!.lowerBound)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Burn-in: drawtext with clip name + timecode.
|
||
|
|
func testBurnIn() {
|
||
|
|
let cmd = FFmpegCommandBuilder.build(
|
||
|
|
input: "in.mov", output: "out.mov",
|
||
|
|
preset: .h264, scale: .half,
|
||
|
|
lutPath: nil,
|
||
|
|
burnIn: BurnIn(clipName: "A001C001", timecode: "10:00:00:00", fps: 24))
|
||
|
|
let vf = cmd[cmd.firstIndex(of: "-vf")! + 1]
|
||
|
|
XCTAssertTrue(vf.contains("drawtext"))
|
||
|
|
XCTAssertTrue(vf.contains("A001C001"))
|
||
|
|
XCTAssertTrue(vf.contains("timecode"))
|
||
|
|
// TC colons escaped for filter syntax.
|
||
|
|
XCTAssertTrue(vf.contains("10\\:00\\:00\\:00"))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Audio: copy through.
|
||
|
|
func testAudioCopied() {
|
||
|
|
let cmd = FFmpegCommandBuilder.build(
|
||
|
|
input: "in.mov", output: "out.mov",
|
||
|
|
preset: .proresProxy, scale: .full, lutPath: nil, burnIn: nil)
|
||
|
|
let aIdx = cmd.firstIndex(of: "-c:a")!
|
||
|
|
XCTAssertEqual(cmd[aIdx + 1], "copy")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
final class ProxyJobTests: XCTestCase {
|
||
|
|
|
||
|
|
// Runner invokes executable with built argv; fake ffmpeg script records args.
|
||
|
|
func testRunnerInvokesWithArgs() async throws {
|
||
|
|
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
|
||
|
|
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
||
|
|
defer { try? FileManager.default.removeItem(atPath: dir) }
|
||
|
|
|
||
|
|
let fake = dir + "/fake-ffmpeg"
|
||
|
|
try #"""
|
||
|
|
#!/bin/bash
|
||
|
|
echo "$@" > "$(dirname "$0")/argv.txt"
|
||
|
|
exit 0
|
||
|
|
"""#.write(toFile: fake, atomically: true, encoding: .utf8)
|
||
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
|
||
|
|
|
||
|
|
let runner = ProxyRunner(ffmpegPath: fake)
|
||
|
|
try await runner.run(args: ["-y", "-i", "in.mov", "out.mov"])
|
||
|
|
|
||
|
|
let argv = try String(contentsOfFile: dir + "/argv.txt", encoding: .utf8)
|
||
|
|
XCTAssertEqual(argv.trimmingCharacters(in: .whitespacesAndNewlines), "-y -i in.mov out.mov")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Nonzero exit throws with stderr.
|
||
|
|
func testRunnerThrowsOnFailure() async throws {
|
||
|
|
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
|
||
|
|
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
||
|
|
defer { try? FileManager.default.removeItem(atPath: dir) }
|
||
|
|
let fake = dir + "/fake-ffmpeg"
|
||
|
|
try "#!/bin/bash\necho 'boom' >&2\nexit 1\n".write(toFile: fake, atomically: true, encoding: .utf8)
|
||
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
|
||
|
|
|
||
|
|
let runner = ProxyRunner(ffmpegPath: fake)
|
||
|
|
do {
|
||
|
|
try await runner.run(args: ["x"])
|
||
|
|
XCTFail("expected throw")
|
||
|
|
} catch let e as ProxyError {
|
||
|
|
guard case .ffmpegFailed(_, let stderr) = e else { return XCTFail("wrong error") }
|
||
|
|
XCTAssertTrue(stderr.contains("boom"))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Graded job: snapshot -> temp .cube written -> lut3d arg references it -> cleaned up after.
|
||
|
|
func testGradedJobWritesCube() async throws {
|
||
|
|
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
|
||
|
|
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
||
|
|
defer { try? FileManager.default.removeItem(atPath: dir) }
|
||
|
|
let fake = dir + "/fake-ffmpeg"
|
||
|
|
try #"""
|
||
|
|
#!/bin/bash
|
||
|
|
echo "$@" > "$(dirname "$0")/argv.txt"
|
||
|
|
# capture cube existence at run time
|
||
|
|
if [[ "$@" == *lut3d* ]]; then
|
||
|
|
cube=$(echo "$@" | grep -oE "[^ =']*\.cube" | head -1)
|
||
|
|
[[ -f "$cube" ]] && echo "cube-exists" > "$(dirname "$0")/cube.txt"
|
||
|
|
fi
|
||
|
|
exit 0
|
||
|
|
"""#.write(toFile: fake, atomically: true, encoding: .utf8)
|
||
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
|
||
|
|
|
||
|
|
let stack = GradeStack(nodes: [GradeNode(kind: .cdl(CDL(
|
||
|
|
slope: SIMD3(1.2, 1, 1), offset: .zero, power: .one, saturation: 1)))])
|
||
|
|
let snapshot = try GradeSnapshot.encode(stack)
|
||
|
|
|
||
|
|
let job = ProxyJob(
|
||
|
|
input: "in.mov",
|
||
|
|
output: dir + "/out.mov",
|
||
|
|
preset: .proresProxy,
|
||
|
|
scale: .half,
|
||
|
|
gradeSnapshot: snapshot,
|
||
|
|
burnIn: nil)
|
||
|
|
try await job.run(ffmpegPath: fake, latticeSize: 9)
|
||
|
|
|
||
|
|
let argv = try String(contentsOfFile: dir + "/argv.txt", encoding: .utf8)
|
||
|
|
XCTAssertTrue(argv.contains("lut3d="))
|
||
|
|
XCTAssertEqual(try String(contentsOfFile: dir + "/cube.txt", encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines), "cube-exists")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ungraded fallback: no snapshot -> no lut3d in args.
|
||
|
|
func testUngradedNoLut() async throws {
|
||
|
|
let dir = NSTemporaryDirectory() + "forge-proxy-\(UUID().uuidString)"
|
||
|
|
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
||
|
|
defer { try? FileManager.default.removeItem(atPath: dir) }
|
||
|
|
let fake = dir + "/fake-ffmpeg"
|
||
|
|
try "#!/bin/bash\necho \"$@\" > \"$(dirname \"$0\")/argv.txt\"\nexit 0\n".write(toFile: fake, atomically: true, encoding: .utf8)
|
||
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fake)
|
||
|
|
|
||
|
|
let job = ProxyJob(input: "in.mov", output: dir + "/out.mov", preset: .h264, scale: .half, gradeSnapshot: nil, burnIn: nil)
|
||
|
|
try await job.run(ffmpegPath: fake, latticeSize: 9)
|
||
|
|
let argv = try String(contentsOfFile: dir + "/argv.txt", encoding: .utf8)
|
||
|
|
XCTAssertFalse(argv.contains("lut3d"))
|
||
|
|
}
|
||
|
|
}
|