feat(camera): add snap/clip capture
This commit is contained in:
92
src/cli/nodes-camera.ts
Normal file
92
src/cli/nodes-camera.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
export type CameraFacing = "front" | "back";
|
||||
|
||||
export type CameraSnapPayload = {
|
||||
format: string;
|
||||
base64: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type CameraClipPayload = {
|
||||
format: string;
|
||||
base64: string;
|
||||
durationMs: number;
|
||||
hasAudio: boolean;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function asBoolean(value: unknown): boolean | undefined {
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
export function parseCameraSnapPayload(value: unknown): CameraSnapPayload {
|
||||
const obj = asRecord(value);
|
||||
const format = asString(obj.format);
|
||||
const base64 = asString(obj.base64);
|
||||
const width = asNumber(obj.width);
|
||||
const height = asNumber(obj.height);
|
||||
if (!format || !base64 || width === undefined || height === undefined) {
|
||||
throw new Error("invalid camera.snap payload");
|
||||
}
|
||||
return { format, base64, width, height };
|
||||
}
|
||||
|
||||
export function parseCameraClipPayload(value: unknown): CameraClipPayload {
|
||||
const obj = asRecord(value);
|
||||
const format = asString(obj.format);
|
||||
const base64 = asString(obj.base64);
|
||||
const durationMs = asNumber(obj.durationMs);
|
||||
const hasAudio = asBoolean(obj.hasAudio);
|
||||
if (
|
||||
!format ||
|
||||
!base64 ||
|
||||
durationMs === undefined ||
|
||||
hasAudio === undefined
|
||||
) {
|
||||
throw new Error("invalid camera.clip payload");
|
||||
}
|
||||
return { format, base64, durationMs, hasAudio };
|
||||
}
|
||||
|
||||
export function cameraTempPath(opts: {
|
||||
kind: "snap" | "clip";
|
||||
facing?: CameraFacing;
|
||||
ext: string;
|
||||
tmpDir?: string;
|
||||
id?: string;
|
||||
}) {
|
||||
const tmpDir = opts.tmpDir ?? os.tmpdir();
|
||||
const id = opts.id ?? randomUUID();
|
||||
const facingPart = opts.facing ? `-${opts.facing}` : "";
|
||||
const ext = opts.ext.startsWith(".") ? opts.ext : `.${opts.ext}`;
|
||||
return path.join(
|
||||
tmpDir,
|
||||
`clawdis-camera-${opts.kind}${facingPart}-${id}${ext}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function writeBase64ToFile(filePath: string, base64: string) {
|
||||
const buf = Buffer.from(base64, "base64");
|
||||
await fs.writeFile(filePath, buf);
|
||||
return { path: filePath, bytes: buf.length };
|
||||
}
|
||||
Reference in New Issue
Block a user