| |
| |
| |
| |
| |
| |
| |
| |
| import { spawn, type ChildProcess } from "node:child_process"; |
| import { mkdirSync } from "node:fs"; |
| import { join } from "node:path"; |
|
|
| export interface ScreenRecording { |
| outputPath: string; |
| |
| |
| |
| |
| |
| finishesAt: number; |
| |
| |
| |
| |
| waitForFinish: () => Promise<void>; |
| |
| |
| |
| |
| |
| abort: () => void; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function startScreenRecording( |
| outputPath: string, |
| durationSeconds: number, |
| ): Promise<ScreenRecording | null> { |
| if (process.platform !== "darwin") { |
| console.warn( |
| "[showcase] --capture is macOS-only (needs /usr/sbin/screencapture). Skipping.", |
| ); |
| return null; |
| } |
|
|
| mkdirSync(join(outputPath, ".."), { recursive: true }); |
|
|
| const proc: ChildProcess = spawn( |
| "screencapture", |
| ["-v", "-V", String(durationSeconds), outputPath], |
| { |
| stdio: ["pipe", "pipe", "pipe"], |
| detached: true, |
| }, |
| ); |
| const startedAt = Date.now(); |
|
|
| let earlyStdout = ""; |
| let earlyError = ""; |
| proc.stdout?.on("data", (chunk: Buffer) => { |
| earlyStdout += chunk.toString(); |
| }); |
| proc.stderr?.on("data", (chunk: Buffer) => { |
| earlyError += chunk.toString(); |
| }); |
|
|
| |
| |
| await new Promise((r) => setTimeout(r, 1200)); |
| if (proc.exitCode !== null) { |
| const details = |
| [earlyError.trim(), earlyStdout.trim()].filter(Boolean).join(" | ") || |
| "no output"; |
| console.warn( |
| `[showcase] screencapture failed to start (exit=${proc.exitCode}): ${details}`, |
| ); |
| return null; |
| } |
|
|
| const waitForFinish = () => |
| new Promise<void>((resolve) => { |
| if (proc.exitCode !== null) { |
| resolve(); |
| return; |
| } |
| proc.once("exit", () => resolve()); |
| }); |
|
|
| const abort = () => { |
| try { |
| proc.kill("SIGKILL"); |
| } catch { |
| |
| } |
| }; |
|
|
| return { |
| outputPath, |
| finishesAt: startedAt + durationSeconds * 1000, |
| waitForFinish, |
| abort, |
| }; |
| } |
|
|
| export function buildRecordingPath(): string { |
| const stamp = new Date() |
| .toISOString() |
| .replace(/[:.]/g, "-") |
| .replace("T", "_") |
| .slice(0, 19); |
| return join(process.cwd(), "demo", "recordings", `showcase-${stamp}.mov`); |
| } |
|
|
| export async function recordingCountdown(seconds: number): Promise<void> { |
| console.log("\n[showcase] === RECORDING MODE ==="); |
| console.log("[showcase] Alice is up in fullscreen app mode."); |
| console.log("[showcase] Start your screen recorder NOW.\n"); |
| for (let i = seconds; i > 0; i--) { |
| console.log(`[showcase] demo starts in ${i}...`); |
| await new Promise((r) => setTimeout(r, 1000)); |
| } |
| console.log("[showcase] action!\n"); |
| } |
|
|