| 1 | import Foundation |
| 2 | import AVFoundation |
| 3 | |
| 4 | /// Headless pipeline check: `sequencer --selftest <mediafile>`. |
| 5 | /// Verifies probe on the given file, then runs the full filmstrip/proxy |
| 6 | /// pipeline on a short synthetic clip, and prints sample Fusion Lua. |
| 7 | func runSelftest(path: String) { |
| 8 | func spin(until done: () -> Bool, timeout: TimeInterval) -> Bool { |
| 9 | let deadline = Date().addingTimeInterval(timeout) |
| 10 | while !done() { |
| 11 | if Date() > deadline { return false } |
| 12 | RunLoop.main.run(until: Date().addingTimeInterval(0.05)) |
| 13 | } |
| 14 | return true |
| 15 | } |
| 16 | |
| 17 | print("== Sequencer selftest ==") |
| 18 | print("ffmpeg: \(MediaPipeline.findExecutable("ffmpeg") ?? "NOT FOUND")") |
| 19 | print("ffprobe: \(MediaPipeline.findExecutable("ffprobe") ?? "NOT FOUND")") |
| 20 | print("cache: \(MediaPipeline.shared.cacheRoot.path)") |
| 21 | |
| 22 | // 1. Probe the real file. |
| 23 | print("\n-- probe: \(path)") |
| 24 | var probed: MediaItem? |
| 25 | var probeDone = false |
| 26 | MediaPipeline.shared.importFile(URL(fileURLWithPath: path)) { item in |
| 27 | probed = item |
| 28 | probeDone = true |
| 29 | } |
| 30 | guard spin(until: { probeDone }, timeout: 60) else { print("FAIL: probe timed out"); return } |
| 31 | if let m = probed { |
| 32 | print(String(format: "OK: %.1fs %dx%d @ %.2ffps audio=%@ key=%@", |
| 33 | m.duration, m.width, m.height, m.fps, m.hasAudio ? "yes" : "no", m.cacheKey)) |
| 34 | } else { |
| 35 | print("FAIL: could not probe \(path)") |
| 36 | } |
| 37 | |
| 38 | // 2. Full pipeline on a short synthetic clip. |
| 39 | print("\n-- pipeline on 4s synthetic clip") |
| 40 | let tmpDir = FileManager.default.temporaryDirectory |
| 41 | .appendingPathComponent("sequencer-selftest", isDirectory: true) |
| 42 | try? FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true) |
| 43 | let testClip = tmpDir.appendingPathComponent("test.mov") |
| 44 | if let ffmpeg = MediaPipeline.findExecutable("ffmpeg") { |
| 45 | let p = Process() |
| 46 | p.executableURL = URL(fileURLWithPath: ffmpeg) |
| 47 | p.arguments = ["-y", "-f", "lavfi", "-i", "testsrc2=size=1280x720:rate=30:duration=4", |
| 48 | "-f", "lavfi", "-i", "sine=frequency=440:duration=4", |
| 49 | "-c:v", "h264_videotoolbox", "-c:a", "aac", "-shortest", testClip.path] |
| 50 | p.standardError = Pipe() |
| 51 | try? p.run() |
| 52 | p.waitUntilExit() |
| 53 | if p.terminationStatus != 0 { print("FAIL: could not generate synthetic clip") } |
| 54 | } |
| 55 | |
| 56 | var testItem: MediaItem? |
| 57 | var testDone = false |
| 58 | MediaPipeline.shared.importFile(testClip) { item in |
| 59 | testItem = item |
| 60 | testDone = true |
| 61 | } |
| 62 | _ = spin(until: { testDone }, timeout: 30) |
| 63 | guard let item = testItem else { print("FAIL: probe of synthetic clip failed"); return } |
| 64 | |
| 65 | DocumentContext.headless.chunks.want(media: item, sourceTime: 0) |
| 66 | _ = spin(until: { |
| 67 | MediaPipeline.shared.status(for: item).filmstripReady |
| 68 | && DocumentContext.headless.chunks.isCovered(media: item, sourceTime: 0) |
| 69 | }, timeout: 120) |
| 70 | print("filmstrip: \(MediaPipeline.shared.status(for: item).filmstripReady ? "OK" : "FAIL")") |
| 71 | print("chunk 0: \(DocumentContext.headless.chunks.isCovered(media: item, sourceTime: 0) ? "OK" : "FAIL")") |
| 72 | |
| 73 | if let proxy = DocumentContext.headless.chunks.builtChunkURL(media: item, index: 0) { |
| 74 | let asset = AVURLAsset(url: proxy) |
| 75 | let sem = DispatchSemaphore(value: 0) |
| 76 | var playable = false |
| 77 | var codec = "?" |
| 78 | Task { |
| 79 | playable = (try? await asset.load(.isPlayable)) ?? false |
| 80 | if let track = try? await asset.loadTracks(withMediaType: .video).first, |
| 81 | let desc = try? await track.load(.formatDescriptions).first { |
| 82 | let sub = CMFormatDescriptionGetMediaSubType(desc) |
| 83 | codec = String(format: "%c%c%c%c", |
| 84 | (sub >> 24) & 255, (sub >> 16) & 255, (sub >> 8) & 255, sub & 255) |
| 85 | } |
| 86 | sem.signal() |
| 87 | } |
| 88 | _ = spin(until: { sem.wait(timeout: .now()) == .success }, timeout: 30) |
| 89 | print("proxy AVFoundation-playable: \(playable ? "OK" : "FAIL") (codec \(codec))") |
| 90 | } |
| 91 | |
| 92 | // 3. Fusion Lua sample. |
| 93 | print("\n-- fusion lua for a 1.5s clip starting at 2.0s") |
| 94 | var model = ProjectModel() |
| 95 | model.fps = item.fps |
| 96 | model.media = [item] |
| 97 | model.tracks = [Track(hue: 0.5)] |
| 98 | let clip = Clip(mediaId: item.id, track: .video(0), start: 2.0, srcIn: 1.0, duration: 1.5) |
| 99 | model.clips = [clip] |
| 100 | print(FusionExport.loaderLua(for: [clip], project: model)) |
| 101 | print("\n== selftest done ==") |
| 102 | } |