import { randomUUID } from "node:crypto"; import { mkdir, rename, rm, stat } from "node:fs/promises"; import path from "node:path"; import type { MediaSourceItem, MediaSourceListRequest, MediaSourceListResult, MediaSourceProvider, } from "@rakazo/adapter-kit"; import { PathOutsideRootError, resolveInside } from "@rakazo/media/node"; import { z } from "zod"; import type { MediaToolPaths } from "./media-folder-source-probe.js"; import { runMediaTool } from "./media-folder-source-probe.js"; /* * Deterministic synthetic library for tests and the scripted runtime. Config * `{ count, seed }` describes `count` assets split into three capture sessions * on separate days in three cities (more than 2 km apart), alternating two * visual themes (`__theme-a__` testsrc2, `__theme-b__` SMPTE bars). Every fifth * asset is a photo. File names carry media-emulator markers: * index % 10 == 0 __faces-1____person-a__ * index % 10 == 1 __faces-1____person-b__ * index == 2 __nsfw-explicit_nudity-0.9__ * index % 4 == 3 __speech__ (videos only) * index % 10 == 6 __cut-2000__ (videos only) * `resolve` renders the clip on first use (4 s, 320x180, ffmpeg testsrc2 or * smptehdbars plus a sine tone) and caches it under `fixturesDir//`. */ const ConfigSchema = z.object({ count: z.number().int().min(1).max(100_000), seed: z.number().int().min(0).max(2_147_483_647), }); const CLIP_MS = 4000; const CLIP_SIZE = { width: 320, height: 180, fps: 25 }; const SESSIONS = [ { lat: 52.52, lon: 13.405 }, { lat: 53.5511, lon: 9.9937 }, { lat: 48.1374, lon: 11.5755 }, ] as const; export interface EmulatorAsset { index: number; fileName: string; kind: "video" | "photo"; theme: "a" | "b"; session: number; capturedAt: Date; gpsLat: number; gpsLon: number; } /** The deterministic description of asset `index` in an emulator library. */ export function emulatorAsset(seed: number, count: number, index: number): EmulatorAsset { const kind = index % 5 === 4 ? "photo" : "video"; const theme = index % 2 === 0 ? "a" : "b"; const session = Math.min(SESSIONS.length - 1, Math.floor((index * SESSIONS.length) / count)); const firstInSession = Math.ceil((session * count) / SESSIONS.length); const day = Date.UTC(2026, 4, 2 + (seed % 28) + session * 2, 8 + session); const capturedAt = new Date(day + (index - firstInSession) * 3 * 60_000); const place = SESSIONS[session] ?? SESSIONS[0]; const jitter = ((index * 7919 + seed) % 100) / 100_000; const markers = [`__theme-${theme}__`]; if (index % 10 === 0) markers.push("__faces-1____person-a__"); if (index % 10 === 1) markers.push("__faces-1____person-b__"); if (index === 2) markers.push("__nsfw-explicit_nudity-0.9__"); if (kind === "video" && index % 4 === 3) markers.push("__speech__"); if (kind === "video" && index % 10 === 6) markers.push("__cut-2000__"); const ext = kind === "photo" ? "jpg" : "mp4"; return { index, fileName: `emu-${seed}-${String(index).padStart(5, "0")}${markers.join("")}.${ext}`, kind, theme, session, capturedAt, gpsLat: Math.round((place.lat + jitter) * 1e5) / 1e5, gpsLon: Math.round((place.lon + jitter) * 1e5) / 1e5, }; } function iso6709(lat: number, lon: number): string { const sign = (value: number, width: number) => `${value < 0 ? "-" : "+"}${Math.abs(value).toFixed(4).padStart(width, "0")}`; return `${sign(lat, 7)}${sign(lon, 8)}/`; } export interface EmulatorMediaSourceOptions extends MediaToolPaths { /** Cache for rendered clips; must lie inside the media root to be analysable. */ fixturesDir: string; } export class EmulatorMediaSourceProvider implements MediaSourceProvider { readonly kind = "emulator" as const; constructor(private readonly options: EmulatorMediaSourceOptions) {} async list(request: MediaSourceListRequest): Promise { request.signal?.throwIfAborted(); const { count, seed } = ConfigSchema.parse(request.config); const start = request.cursor ? decodeIndex(request.cursor) : 0; const end = Math.min(count, start + Math.max(1, Math.floor(request.limit))); const items: MediaSourceItem[] = []; for (let index = start; index < end; index++) { const asset = emulatorAsset(seed, count, index); items.push({ externalId: asset.fileName, kind: asset.kind, capturedAt: asset.capturedAt, durationMs: asset.kind === "video" ? CLIP_MS : null, width: CLIP_SIZE.width, height: CLIP_SIZE.height, fps: asset.kind === "video" ? CLIP_SIZE.fps : null, gpsLat: asset.gpsLat, gpsLon: asset.gpsLon, deviceModel: "Emulator", albumNames: [], isFavorite: index % 9 === 0, fileName: asset.fileName, sizeBytes: null, revision: `emulator-v1-${seed}`, }); } return { items, nextCursor: end < count ? Buffer.from(String(end)).toString("base64url") : null, }; } async resolve(config: Record, externalId: string): Promise { const { count, seed } = ConfigSchema.parse(config); const match = /^emu-(\d+)-(\d{5})/.exec(externalId); const index = match ? Number(match[2]) : -1; if (!match || Number(match[1]) !== seed || index >= count) throw new PathOutsideRootError(externalId); const asset = emulatorAsset(seed, count, index); if (asset.fileName !== externalId) throw new PathOutsideRootError(externalId); await mkdir(path.join(this.options.fixturesDir, String(seed)), { recursive: true }); const target = await resolveInside(this.options.fixturesDir, `${seed}/${asset.fileName}`); if (await exists(target)) return target; const partial = path.join( path.dirname(target), `.partial-${randomUUID()}.${asset.kind === "photo" ? "jpg" : "mp4"}`, ); try { await runMediaTool(this.options.ffmpegPath ?? "ffmpeg", renderArgs(asset, partial)); await rename(partial, target); } finally { await rm(partial, { force: true }); } return target; } } function renderArgs(asset: EmulatorAsset, out: string): string[] { const { width, height, fps } = CLIP_SIZE; const pattern = asset.theme === "a" ? "testsrc2" : "smptehdbars"; const video = `${pattern}=s=${width}x${height}:r=${fps}:d=${CLIP_MS / 1000}`; const metadata = [ "-metadata", `creation_time=${asset.capturedAt.toISOString()}`, "-metadata", `location=${iso6709(asset.gpsLat, asset.gpsLon)}`, ]; if (asset.kind === "photo") return ["-v", "error", "-y", "-f", "lavfi", "-i", video, "-frames:v", "1", out]; return [ "-v", "error", "-y", "-f", "lavfi", "-i", video, "-f", "lavfi", "-i", `sine=f=${220 + (asset.index % 12) * 40}:sample_rate=48000:d=${CLIP_MS / 1000}`, "-shortest", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-c:a", "aac", ...metadata, "-movflags", "+faststart", out, ]; } function decodeIndex(cursor: string): number { const value = Number(Buffer.from(cursor, "base64url").toString("utf8")); if (!Number.isInteger(value) || value < 0) throw new Error("invalid emulator cursor"); return value; } async function exists(file: string): Promise { try { return (await stat(file)).isFile(); } catch { return false; } }