@sequio/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sequio.js +53 -0
- package/dist/args.d.ts +53 -0
- package/dist/assets-node.d.ts +9 -0
- package/dist/assets.d.ts +17 -0
- package/dist/bundle.d.ts +9 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +34 -0
- package/dist/externals.d.ts +4 -0
- package/dist/externals.js +8 -0
- package/dist/frame.d.ts +14 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +12 -0
- package/dist/preview.d.ts +17 -0
- package/dist/render.d.ts +11 -0
- package/dist/version-B2TKvZen.js +338 -0
- package/dist/version.d.ts +2 -0
- package/package.json +67 -0
- package/preview/assets.ts +22 -0
- package/preview/index.html +115 -0
- package/preview/preview.ts +112 -0
package/bin/sequio.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `sequio` binary. Two modes:
|
|
4
|
+
*
|
|
5
|
+
* • **Published** — the package ships a built `dist/cli.js`; we import it
|
|
6
|
+
* directly (plain Node, no toolchain needed), and it resolves the sibling
|
|
7
|
+
* `@sequio/*` packages from `node_modules`.
|
|
8
|
+
*
|
|
9
|
+
* • **From source (this repo)** — there is no `dist/` yet, so — mirroring how
|
|
10
|
+
* the repo runs its other Node entry points (route-b, the verify scripts) —
|
|
11
|
+
* we execute `src/cli.ts` through tsx, pinning this package's tsconfig so its
|
|
12
|
+
* `paths` resolve `@sequio/*` straight from source without a prior build.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { dirname, resolve } from 'node:path';
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const built = resolve(here, '../dist/cli.js');
|
|
22
|
+
|
|
23
|
+
if (existsSync(built)) {
|
|
24
|
+
// Published: run the built entry (it self-invokes `main`).
|
|
25
|
+
await import(pathToFileURL(built).href);
|
|
26
|
+
} else {
|
|
27
|
+
// From source: launch `src/cli.ts` through tsx.
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const entry = resolve(here, '../src/cli.ts');
|
|
30
|
+
|
|
31
|
+
// Resolve tsx's bin from its manifest so this works regardless of where the
|
|
32
|
+
// (hoisted) dependency landed in the workspace's node_modules.
|
|
33
|
+
const tsxPkgPath = require.resolve('tsx/package.json');
|
|
34
|
+
const tsxPkg = require('tsx/package.json');
|
|
35
|
+
const tsxBin = resolve(dirname(tsxPkgPath), typeof tsxPkg.bin === 'string' ? tsxPkg.bin : tsxPkg.bin.tsx);
|
|
36
|
+
|
|
37
|
+
// Pin the tsconfig tsx uses to this package's own — it carries the `paths` that
|
|
38
|
+
// resolve @sequio/engine, @sequio/runtime and @sequio/server/route-b straight
|
|
39
|
+
// from source. Without this, tsx would pick up whatever tsconfig sits at the
|
|
40
|
+
// cwd (e.g. the workspace-root solution config, which has no `paths`), and the
|
|
41
|
+
// bare imports would fall back to the engine's unbuilt `dist/` and fail.
|
|
42
|
+
const tsconfig = resolve(here, '../tsconfig.json');
|
|
43
|
+
|
|
44
|
+
const child = spawn(process.execPath, [tsxBin, entry, ...process.argv.slice(2)], {
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
env: { ...process.env, TSX_TSCONFIG_PATH: tsconfig },
|
|
47
|
+
});
|
|
48
|
+
child.on('close', (code) => process.exit(code ?? 1));
|
|
49
|
+
child.on('error', (err) => {
|
|
50
|
+
console.error('✖ failed to launch sequio:', err.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure argument parsing for the `sequio` CLI. Kept free of any I/O so it can be
|
|
3
|
+
* unit-tested directly: hand it an argv array, get back a discriminated
|
|
4
|
+
* {@link CliCommand} (or a help/version/error sentinel). `cli.ts` does the I/O.
|
|
5
|
+
*/
|
|
6
|
+
/** `sequio render <file> [-o out] [--scale N] [--verify]`. */
|
|
7
|
+
export interface RenderCommand {
|
|
8
|
+
kind: 'render';
|
|
9
|
+
/** Path to the entry composition file (its `defineComposition` default export). */
|
|
10
|
+
file: string;
|
|
11
|
+
/** Output video path. `undefined` → `out.mp4`. */
|
|
12
|
+
out?: string;
|
|
13
|
+
/** Output resolution multiplier (N× the composition size). @default 1 */
|
|
14
|
+
scale: number;
|
|
15
|
+
/** Assert a valid video container came out (non-zero exit otherwise). */
|
|
16
|
+
verify: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** `sequio preview <file> [--watch] [-p port] [--host]`. */
|
|
19
|
+
export interface PreviewCommand {
|
|
20
|
+
kind: 'preview';
|
|
21
|
+
/** Path to the entry composition file. */
|
|
22
|
+
file: string;
|
|
23
|
+
/** Re-run the composition in the browser whenever a project file changes. */
|
|
24
|
+
watch: boolean;
|
|
25
|
+
/** Dev-server port. @default 6180 */
|
|
26
|
+
port: number;
|
|
27
|
+
/** Bind to `0.0.0.0` (expose on the network) instead of localhost. */
|
|
28
|
+
host: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** `sequio frame <file> [-t sec] [-o out.png] [--scale N]`. */
|
|
31
|
+
export interface FrameCommand {
|
|
32
|
+
kind: 'frame';
|
|
33
|
+
/** Path to the entry composition file. */
|
|
34
|
+
file: string;
|
|
35
|
+
/** Time (seconds) to sample. Clamped to `[0, duration]`. @default 0 */
|
|
36
|
+
time: number;
|
|
37
|
+
/** Output image path. `undefined` → `frame.png`. */
|
|
38
|
+
out?: string;
|
|
39
|
+
/** Output resolution multiplier (N× the composition size). @default 1 */
|
|
40
|
+
scale: number;
|
|
41
|
+
}
|
|
42
|
+
/** Show usage / version / a parse error and exit. */
|
|
43
|
+
export interface MetaCommand {
|
|
44
|
+
kind: 'help' | 'version' | 'error';
|
|
45
|
+
/** For `error`: the message to print (before usage) and exit non-zero. */
|
|
46
|
+
message?: string;
|
|
47
|
+
}
|
|
48
|
+
export type CliCommand = RenderCommand | PreviewCommand | FrameCommand | MetaCommand;
|
|
49
|
+
export declare const DEFAULT_PREVIEW_PORT = 6180;
|
|
50
|
+
export declare const USAGE = "sequio \u2014 programmable-timeline CLI\n\nUsage:\n sequio render <file> [options] Encode a composition to a video file\n sequio frame <file> [options] Export a single frame at a time as a PNG\n sequio preview <file> [options] Serve a live in-browser preview\n\nRender options (pure Node, PixiJS WebGPU \u2014 needs a GPU or Mesa lavapipe):\n -o, --out <path> Output path (default: out.mp4)\n -s, --scale <n> Render at n\u00D7 the composition resolution (default: 1)\n --verify Assert a valid video container came out\n\nFrame options (pure Node, PixiJS WebGPU \u2014 needs a GPU or Mesa lavapipe):\n -t, --time <sec> Timeline time to sample, in seconds (default: 0)\n -o, --out <path> Output PNG path (default: frame.png)\n -s, --scale <n> Render at n\u00D7 the composition resolution (default: 1)\n\nPreview options:\n -w, --watch Re-run on any project-file change (live reload)\n -p, --port <n> Dev-server port (default: 6180)\n --host Expose the server on the local network (0.0.0.0)\n\nGlobal:\n -h, --help Show this help\n -v, --version Print the version\n\n<file> is a TS/JS module whose default export is a\ndefineComposition(builder) (or a bare builder). Its sibling files are\nbundled with it, so relative imports (./scene, ./title) resolve.";
|
|
51
|
+
/** Parse `process.argv.slice(2)` into a {@link CliCommand}. Never throws. */
|
|
52
|
+
export declare function parseArgs(argv: string[]): CliCommand;
|
|
53
|
+
//# sourceMappingURL=args.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AssetLoader } from '@sequio/runtime';
|
|
2
|
+
/**
|
|
3
|
+
* Build an {@link AssetLoader} rooted at `projectRoot` (the entry file's
|
|
4
|
+
* directory). Reads `projectRoot/<path>` and wraps the bytes in a `Blob` (which
|
|
5
|
+
* both `ImageSource` and `VideoSource` accept). Refuses paths that escape the
|
|
6
|
+
* root and reports a missing file clearly.
|
|
7
|
+
*/
|
|
8
|
+
export declare function nodeAssetLoader(projectRoot: string): AssetLoader;
|
|
9
|
+
//# sourceMappingURL=assets-node.d.ts.map
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-side helpers for a composition's **local media assets** (the runtime owns
|
|
3
|
+
* the `loadAsset` contract — see `@sequio/runtime`'s `assets.ts`). These two
|
|
4
|
+
* concerns are the CLI's: which project files are *binary assets* (so
|
|
5
|
+
* {@link readBundle} keeps them out of the text bundle) and their MIME type (so
|
|
6
|
+
* the preview dev server can serve them over `/__asset/…`).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Whether a project file is a binary media/font asset rather than source code.
|
|
10
|
+
* {@link readBundle} skips these so a (potentially large) local `.mp4` is never
|
|
11
|
+
* read as UTF-8 into the JSON bundle — the browser fetches it via `/__asset/…`
|
|
12
|
+
* and Node reads it off disk (see `assets-node.ts`) instead.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isBinaryAssetPath(path: string): boolean;
|
|
15
|
+
/** MIME type for a media path, for the preview server's `/__asset/` responses. */
|
|
16
|
+
export declare function mimeForAsset(path: string): string;
|
|
17
|
+
//# sourceMappingURL=assets.d.ts.map
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RuntimeBundle } from '@sequio/runtime';
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot the project around `entryFile` into a {@link RuntimeBundle}.
|
|
4
|
+
*
|
|
5
|
+
* @param entryFile Path (absolute or cwd-relative) to the entry module.
|
|
6
|
+
* @throws if the file does not exist under its own directory.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readBundle(entryFile: string): RuntimeBundle;
|
|
9
|
+
//# sourceMappingURL=bundle.d.ts.map
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { p as n, s as t, a as i, b as l, U as c, v as p } from "./version-B2TKvZen.js";
|
|
2
|
+
async function u(s) {
|
|
3
|
+
const e = n(s);
|
|
4
|
+
switch (e.kind) {
|
|
5
|
+
case "help":
|
|
6
|
+
return console.log(c), 0;
|
|
7
|
+
case "version":
|
|
8
|
+
return console.log(p), 0;
|
|
9
|
+
case "error":
|
|
10
|
+
return console.error(`✖ ${e.message}
|
|
11
|
+
`), console.error(c), 2;
|
|
12
|
+
case "render":
|
|
13
|
+
return l(e.file, { out: e.out, scale: e.scale, verify: e.verify });
|
|
14
|
+
case "frame":
|
|
15
|
+
return i(e.file, { out: e.out, time: e.time, scale: e.scale });
|
|
16
|
+
case "preview": {
|
|
17
|
+
const r = await t(e.file, {
|
|
18
|
+
port: e.port,
|
|
19
|
+
host: e.host,
|
|
20
|
+
watch: e.watch
|
|
21
|
+
});
|
|
22
|
+
return console.log(`▸ sequio preview → ${r.url}`), console.log(` serving ${e.file}${e.watch ? " (watching for changes)" : ""}`), console.log(" press Ctrl-C to stop."), await new Promise((a) => {
|
|
23
|
+
const o = () => {
|
|
24
|
+
r.close().finally(() => a());
|
|
25
|
+
};
|
|
26
|
+
process.once("SIGINT", o), process.once("SIGTERM", o);
|
|
27
|
+
}), 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
u(process.argv.slice(2)).then((s) => process.exit(s)).catch((s) => {
|
|
32
|
+
console.error("✖", s instanceof Error ? s.message : s), process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/frame.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FrameOptions {
|
|
2
|
+
/** Time (seconds) to sample. Clamped to `[0, duration]`. @default 0 */
|
|
3
|
+
time?: number;
|
|
4
|
+
/** Output PNG path. @default frame.png */
|
|
5
|
+
out?: string;
|
|
6
|
+
/** Output resolution multiplier (N× the composition size). @default 1 */
|
|
7
|
+
scale?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Export one frame of `entryFile` to a PNG, returning the process exit code
|
|
11
|
+
* (0 = success).
|
|
12
|
+
*/
|
|
13
|
+
export declare function runFrame(entryFile: string, options?: FrameOptions): Promise<number>;
|
|
14
|
+
//# sourceMappingURL=frame.d.ts.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sequio/cli` — the `sequio` command line.
|
|
3
|
+
*
|
|
4
|
+
* Three commands, all thin front-ends over infrastructure the other packages
|
|
5
|
+
* already own:
|
|
6
|
+
* - `render <file>` → snapshot the composition into a {@link RuntimeBundle} and
|
|
7
|
+
* hand it to the server's Route B (pure Node, WebGPU) to encode a video.
|
|
8
|
+
* - `frame <file>` → the same Route B path, but seek to one time and write a
|
|
9
|
+
* single PNG — a fast visual check without a full render.
|
|
10
|
+
* - `preview <file>` → boot a Vite dev server whose page runs the same
|
|
11
|
+
* `Runtime` → `Composer` → `preview()` path in-browser; `--watch` live-reloads.
|
|
12
|
+
*
|
|
13
|
+
* This barrel is the programmatic surface (used by tests and embedders); the
|
|
14
|
+
* `sequio` binary is `bin/sequio.js`, which runs `src/cli.ts`.
|
|
15
|
+
*/
|
|
16
|
+
export { parseArgs, USAGE, DEFAULT_PREVIEW_PORT, type CliCommand, type RenderCommand, type FrameCommand, type PreviewCommand, type MetaCommand, } from './args';
|
|
17
|
+
export { readBundle } from './bundle';
|
|
18
|
+
export { runRender, type RenderOptions } from './render';
|
|
19
|
+
export { runFrame, type FrameOptions } from './frame';
|
|
20
|
+
export { startPreviewServer, type PreviewServer, type PreviewServerOptions, } from './preview';
|
|
21
|
+
export { version } from './version';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { D as s, U as e, p as n, r as v, a as E, b as d, s as o, v as p } from "./version-B2TKvZen.js";
|
|
2
|
+
export {
|
|
3
|
+
s as DEFAULT_PREVIEW_PORT,
|
|
4
|
+
e as USAGE,
|
|
5
|
+
n as parseArgs,
|
|
6
|
+
v as readBundle,
|
|
7
|
+
E as runFrame,
|
|
8
|
+
d as runRender,
|
|
9
|
+
o as startPreviewServer,
|
|
10
|
+
p as version
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PreviewServerOptions {
|
|
2
|
+
port?: number;
|
|
3
|
+
host?: boolean;
|
|
4
|
+
watch?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface PreviewServer {
|
|
7
|
+
/** The URL the preview is served at. */
|
|
8
|
+
url: string;
|
|
9
|
+
/** Shut the server (and any file watcher) down. */
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Start the preview dev server for `entryFile`. Resolves once it is listening;
|
|
14
|
+
* the returned handle carries the URL and a {@link PreviewServer.close}.
|
|
15
|
+
*/
|
|
16
|
+
export declare function startPreviewServer(entryFile: string, options?: PreviewServerOptions): Promise<PreviewServer>;
|
|
17
|
+
//# sourceMappingURL=preview.d.ts.map
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RenderOptions {
|
|
2
|
+
out?: string;
|
|
3
|
+
verify?: boolean;
|
|
4
|
+
/** Output resolution multiplier (N× the composition size). @default 1 */
|
|
5
|
+
scale?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Render `entryFile` to a video, returning the process exit code (0 = success).
|
|
9
|
+
*/
|
|
10
|
+
export declare function runRender(entryFile: string, options?: RenderOptions): Promise<number>;
|
|
11
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { resolve as f, dirname as m, basename as S } from "node:path";
|
|
2
|
+
import { cliExternals as h } from "./externals.js";
|
|
3
|
+
import { readFileSync as E, existsSync as w, statSync as P, createReadStream as x } from "node:fs";
|
|
4
|
+
import { fileURLToPath as U } from "node:url";
|
|
5
|
+
import { resolveAssetPath as R } from "@sequio/runtime";
|
|
6
|
+
import { NodeFileSystem as N } from "@sequio/runtime/node-fs";
|
|
7
|
+
import { createRequire as C } from "node:module";
|
|
8
|
+
const v = 6180, z = `sequio — programmable-timeline CLI
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
sequio render <file> [options] Encode a composition to a video file
|
|
12
|
+
sequio frame <file> [options] Export a single frame at a time as a PNG
|
|
13
|
+
sequio preview <file> [options] Serve a live in-browser preview
|
|
14
|
+
|
|
15
|
+
Render options (pure Node, PixiJS WebGPU — needs a GPU or Mesa lavapipe):
|
|
16
|
+
-o, --out <path> Output path (default: out.mp4)
|
|
17
|
+
-s, --scale <n> Render at n× the composition resolution (default: 1)
|
|
18
|
+
--verify Assert a valid video container came out
|
|
19
|
+
|
|
20
|
+
Frame options (pure Node, PixiJS WebGPU — needs a GPU or Mesa lavapipe):
|
|
21
|
+
-t, --time <sec> Timeline time to sample, in seconds (default: 0)
|
|
22
|
+
-o, --out <path> Output PNG path (default: frame.png)
|
|
23
|
+
-s, --scale <n> Render at n× the composition resolution (default: 1)
|
|
24
|
+
|
|
25
|
+
Preview options:
|
|
26
|
+
-w, --watch Re-run on any project-file change (live reload)
|
|
27
|
+
-p, --port <n> Dev-server port (default: ${v})
|
|
28
|
+
--host Expose the server on the local network (0.0.0.0)
|
|
29
|
+
|
|
30
|
+
Global:
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
-v, --version Print the version
|
|
33
|
+
|
|
34
|
+
<file> is a TS/JS module whose default export is a
|
|
35
|
+
defineComposition(builder) (or a bare builder). Its sibling files are
|
|
36
|
+
bundled with it, so relative imports (./scene, ./title) resolve.`;
|
|
37
|
+
function X(t) {
|
|
38
|
+
if (t[0] === "--" && (t = t.slice(1)), t.length === 0) return { kind: "help" };
|
|
39
|
+
const e = t[0];
|
|
40
|
+
return e === "-h" || e === "--help" ? { kind: "help" } : e === "-v" || e === "--version" ? { kind: "version" } : e === "render" ? j(t.slice(1)) : e === "frame" ? A(t.slice(1)) : e === "preview" ? F(t.slice(1)) : { kind: "error", message: `Unknown command: ${e}` };
|
|
41
|
+
}
|
|
42
|
+
function j(t) {
|
|
43
|
+
let e, i, l = 1, c = !1;
|
|
44
|
+
for (let o = 0; o < t.length; o++) {
|
|
45
|
+
const r = t[o];
|
|
46
|
+
if (r === "-h" || r === "--help") return { kind: "help" };
|
|
47
|
+
if (r === "-o" || r === "--out") {
|
|
48
|
+
if (i = t[++o], i === void 0) return { kind: "error", message: `${r} needs a path` };
|
|
49
|
+
} else if (r === "-s" || r === "--scale") {
|
|
50
|
+
const n = t[++o], s = Number(n);
|
|
51
|
+
if (!(s >= 1) || !Number.isFinite(s)) return { kind: "error", message: `${r} needs a number ≥ 1, got: ${n}` };
|
|
52
|
+
l = s;
|
|
53
|
+
} else if (r === "--verify") c = !0;
|
|
54
|
+
else {
|
|
55
|
+
if (r.startsWith("-")) return { kind: "error", message: `Unknown option: ${r}` };
|
|
56
|
+
if (e === void 0) e = r;
|
|
57
|
+
else return { kind: "error", message: `Unexpected argument: ${r}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return e === void 0 ? { kind: "error", message: "render needs a <file>" } : { kind: "render", file: e, out: i, scale: l, verify: c };
|
|
61
|
+
}
|
|
62
|
+
function A(t) {
|
|
63
|
+
let e, i, l = 0, c = 1;
|
|
64
|
+
for (let o = 0; o < t.length; o++) {
|
|
65
|
+
const r = t[o];
|
|
66
|
+
if (r === "-h" || r === "--help") return { kind: "help" };
|
|
67
|
+
if (r === "-o" || r === "--out") {
|
|
68
|
+
if (i = t[++o], i === void 0) return { kind: "error", message: `${r} needs a path` };
|
|
69
|
+
} else if (r === "-t" || r === "--time") {
|
|
70
|
+
const n = t[++o], s = Number(n);
|
|
71
|
+
if (!Number.isFinite(s) || s < 0) return { kind: "error", message: `${r} needs a number ≥ 0, got: ${n}` };
|
|
72
|
+
l = s;
|
|
73
|
+
} else if (r === "-s" || r === "--scale") {
|
|
74
|
+
const n = t[++o], s = Number(n);
|
|
75
|
+
if (!(s >= 1) || !Number.isFinite(s)) return { kind: "error", message: `${r} needs a number ≥ 1, got: ${n}` };
|
|
76
|
+
c = s;
|
|
77
|
+
} else {
|
|
78
|
+
if (r.startsWith("-")) return { kind: "error", message: `Unknown option: ${r}` };
|
|
79
|
+
if (e === void 0) e = r;
|
|
80
|
+
else return { kind: "error", message: `Unexpected argument: ${r}` };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return e === void 0 ? { kind: "error", message: "frame needs a <file>" } : { kind: "frame", file: e, time: l, out: i, scale: c };
|
|
84
|
+
}
|
|
85
|
+
function F(t) {
|
|
86
|
+
let e, i = !1, l = !1, c = v;
|
|
87
|
+
for (let o = 0; o < t.length; o++) {
|
|
88
|
+
const r = t[o];
|
|
89
|
+
if (r === "-h" || r === "--help") return { kind: "help" };
|
|
90
|
+
if (r === "-w" || r === "--watch") i = !0;
|
|
91
|
+
else if (r === "--host") l = !0;
|
|
92
|
+
else if (r === "-p" || r === "--port") {
|
|
93
|
+
const n = t[++o], s = Number(n);
|
|
94
|
+
if (!Number.isInteger(s) || s <= 0 || s > 65535)
|
|
95
|
+
return { kind: "error", message: `${r} needs a valid port, got: ${n}` };
|
|
96
|
+
c = s;
|
|
97
|
+
} else {
|
|
98
|
+
if (r.startsWith("-")) return { kind: "error", message: `Unknown option: ${r}` };
|
|
99
|
+
if (e === void 0) e = r;
|
|
100
|
+
else return { kind: "error", message: `Unexpected argument: ${r}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return e === void 0 ? { kind: "error", message: "preview needs a <file>" } : { kind: "preview", file: e, watch: i, port: c, host: l };
|
|
104
|
+
}
|
|
105
|
+
const T = /* @__PURE__ */ new Set([
|
|
106
|
+
// images
|
|
107
|
+
"png",
|
|
108
|
+
"jpg",
|
|
109
|
+
"jpeg",
|
|
110
|
+
"gif",
|
|
111
|
+
"webp",
|
|
112
|
+
"avif",
|
|
113
|
+
"bmp",
|
|
114
|
+
"ico",
|
|
115
|
+
// video
|
|
116
|
+
"mp4",
|
|
117
|
+
"webm",
|
|
118
|
+
"mov",
|
|
119
|
+
"mkv",
|
|
120
|
+
"avi",
|
|
121
|
+
"m4v",
|
|
122
|
+
// audio
|
|
123
|
+
"mp3",
|
|
124
|
+
"wav",
|
|
125
|
+
"m4a",
|
|
126
|
+
"aac",
|
|
127
|
+
"ogg",
|
|
128
|
+
"oga",
|
|
129
|
+
"flac",
|
|
130
|
+
"opus",
|
|
131
|
+
// fonts
|
|
132
|
+
"ttf",
|
|
133
|
+
"otf",
|
|
134
|
+
"woff",
|
|
135
|
+
"woff2"
|
|
136
|
+
]);
|
|
137
|
+
function $(t) {
|
|
138
|
+
const e = t.lastIndexOf(".");
|
|
139
|
+
return e < 0 ? "" : t.slice(e + 1).toLowerCase();
|
|
140
|
+
}
|
|
141
|
+
function q(t) {
|
|
142
|
+
return T.has($(t));
|
|
143
|
+
}
|
|
144
|
+
function W(t) {
|
|
145
|
+
return {
|
|
146
|
+
png: "image/png",
|
|
147
|
+
jpg: "image/jpeg",
|
|
148
|
+
jpeg: "image/jpeg",
|
|
149
|
+
gif: "image/gif",
|
|
150
|
+
webp: "image/webp",
|
|
151
|
+
avif: "image/avif",
|
|
152
|
+
bmp: "image/bmp",
|
|
153
|
+
ico: "image/x-icon",
|
|
154
|
+
mp4: "video/mp4",
|
|
155
|
+
webm: "video/webm",
|
|
156
|
+
mov: "video/quicktime",
|
|
157
|
+
mkv: "video/x-matroska",
|
|
158
|
+
avi: "video/x-msvideo",
|
|
159
|
+
m4v: "video/x-m4v",
|
|
160
|
+
mp3: "audio/mpeg",
|
|
161
|
+
wav: "audio/wav",
|
|
162
|
+
m4a: "audio/mp4",
|
|
163
|
+
aac: "audio/aac",
|
|
164
|
+
ogg: "audio/ogg",
|
|
165
|
+
oga: "audio/ogg",
|
|
166
|
+
flac: "audio/flac",
|
|
167
|
+
opus: "audio/opus",
|
|
168
|
+
ttf: "font/ttf",
|
|
169
|
+
otf: "font/otf",
|
|
170
|
+
woff: "font/woff",
|
|
171
|
+
woff2: "font/woff2"
|
|
172
|
+
}[$(t)] ?? "application/octet-stream";
|
|
173
|
+
}
|
|
174
|
+
function p(t) {
|
|
175
|
+
const e = f(t), i = m(e), l = "/" + S(e), c = new N(i), o = {};
|
|
176
|
+
for (const r of c.listFiles()) {
|
|
177
|
+
if (q(r)) continue;
|
|
178
|
+
const n = c.readFile(r);
|
|
179
|
+
n !== null && (o[r] = n);
|
|
180
|
+
}
|
|
181
|
+
if (o[l] === void 0)
|
|
182
|
+
throw new Error(`Entry file not found: ${t}`);
|
|
183
|
+
return { files: o, entry: l };
|
|
184
|
+
}
|
|
185
|
+
function b(t) {
|
|
186
|
+
const e = f(t);
|
|
187
|
+
return async (i) => {
|
|
188
|
+
const l = f(e, i);
|
|
189
|
+
if (l !== e && !l.startsWith(e + "/"))
|
|
190
|
+
throw new Error(`asset path escapes the project root: ${i}`);
|
|
191
|
+
let c;
|
|
192
|
+
try {
|
|
193
|
+
c = E(l);
|
|
194
|
+
} catch {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`local asset not found: ${i} (looked in ${e}). Drop the file there, or reference a network URL instead.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return new Blob([new Uint8Array(c)]);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function O(t) {
|
|
203
|
+
return t.length >= 12 && Buffer.from(t.subarray(4, 8)).toString("latin1") === "ftyp" ? "mp4" : t.length >= 4 && t[0] === 26 && t[1] === 69 && t[2] === 223 && t[3] === 163 ? "webm" : null;
|
|
204
|
+
}
|
|
205
|
+
async function Y(t, e = {}) {
|
|
206
|
+
const i = p(t), l = m(f(t)), { renderBundleToFile: c } = await import("@sequio/server/route-b");
|
|
207
|
+
try {
|
|
208
|
+
const o = e.out ?? "out.mp4";
|
|
209
|
+
console.log(`Rendering ${t} (pure Node, WebGPU)${e.scale && e.scale !== 1 ? ` @ ${e.scale}×` : ""} …`);
|
|
210
|
+
let r = -1;
|
|
211
|
+
const n = await c(i, {
|
|
212
|
+
out: o,
|
|
213
|
+
scale: e.scale,
|
|
214
|
+
// Make gsap (and any other CLI-provided lib) resolvable to the composition
|
|
215
|
+
// in the Node render, same as the browser preview does.
|
|
216
|
+
externals: h(),
|
|
217
|
+
// Resolve `loadAsset('./clip.mp4')` against the project directory on disk,
|
|
218
|
+
// so a local image/video renders the same as it previews (contract #3).
|
|
219
|
+
loadAsset: b(l),
|
|
220
|
+
onProgress: (s) => {
|
|
221
|
+
const a = Math.round(s * 100);
|
|
222
|
+
a !== r && a % 10 === 0 && (r = a, process.stdout.write(`\r ${a}%`));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
if (process.stdout.write("\r \r"), console.log(
|
|
226
|
+
`✅ wrote ${n.out} (${n.frames} frames, ${n.container}/${n.videoCodec}, ${n.bytes} bytes${n.audio ? ", +audio" : ""})`
|
|
227
|
+
), e.verify) {
|
|
228
|
+
const a = (await import("node:fs")).readFileSync(n.out), d = O(a);
|
|
229
|
+
if (!d || a.length < 500)
|
|
230
|
+
throw new Error(`--verify failed: not a valid container (detected=${d}, size=${a.length})`);
|
|
231
|
+
console.log(`✅ verified: valid ${d} container.`);
|
|
232
|
+
}
|
|
233
|
+
return 0;
|
|
234
|
+
} catch (o) {
|
|
235
|
+
return console.error("✖", o instanceof Error ? o.message : String(o)), 1;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function K(t, e = {}) {
|
|
239
|
+
const i = p(t), l = m(f(t)), { renderBundleFrameToFile: c } = await import("@sequio/server/route-b");
|
|
240
|
+
try {
|
|
241
|
+
const o = e.out ?? "frame.png", r = e.time ?? 0;
|
|
242
|
+
console.log(
|
|
243
|
+
`Rendering ${t} frame @ t=${r}s (pure Node, WebGPU)${e.scale && e.scale !== 1 ? ` @ ${e.scale}×` : ""} …`
|
|
244
|
+
);
|
|
245
|
+
const n = await c(i, {
|
|
246
|
+
out: o,
|
|
247
|
+
time: r,
|
|
248
|
+
scale: e.scale,
|
|
249
|
+
// Make gsap (and any other CLI-provided lib) resolvable to the composition
|
|
250
|
+
// in the Node render, same as the browser preview does.
|
|
251
|
+
externals: h(),
|
|
252
|
+
// Resolve `loadAsset('./clip.mp4')` against the project directory on disk,
|
|
253
|
+
// so a local image/video renders the same as it previews (contract #3).
|
|
254
|
+
loadAsset: b(l)
|
|
255
|
+
}), s = n.time !== r ? ` (clamped from ${r}s)` : "";
|
|
256
|
+
return console.log(
|
|
257
|
+
`✅ wrote ${n.out} (${n.width}×${n.height}, t=${n.time}s${s}, ${n.bytes} bytes)`
|
|
258
|
+
), 0;
|
|
259
|
+
} catch (o) {
|
|
260
|
+
return console.error("✖", o instanceof Error ? o.message : String(o)), 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const k = m(U(import.meta.url)), _ = f(k, "../preview"), y = {};
|
|
264
|
+
for (const [t, e] of [
|
|
265
|
+
["@sequio/engine", "../../engine/src/index.ts"],
|
|
266
|
+
["@sequio/runtime", "../../runtime/src/index.ts"],
|
|
267
|
+
["@sequio/cli/externals", "./externals.ts"]
|
|
268
|
+
]) {
|
|
269
|
+
const i = f(k, e);
|
|
270
|
+
w(i) && (y[t] = i);
|
|
271
|
+
}
|
|
272
|
+
async function Q(t, e = {}) {
|
|
273
|
+
const i = f(t), l = m(i);
|
|
274
|
+
p(i);
|
|
275
|
+
const { createServer: c } = await import("vite"), o = await c({
|
|
276
|
+
configFile: !1,
|
|
277
|
+
root: _,
|
|
278
|
+
logLevel: "warn",
|
|
279
|
+
server: {
|
|
280
|
+
port: e.port ?? 6180,
|
|
281
|
+
host: e.host ? !0 : "localhost",
|
|
282
|
+
strictPort: !0
|
|
283
|
+
},
|
|
284
|
+
resolve: {
|
|
285
|
+
alias: y
|
|
286
|
+
},
|
|
287
|
+
plugins: [
|
|
288
|
+
{
|
|
289
|
+
name: "sequio-preview-bundle",
|
|
290
|
+
configureServer(n) {
|
|
291
|
+
if (n.middlewares.use("/__bundle", (s, a) => {
|
|
292
|
+
try {
|
|
293
|
+
const d = p(i);
|
|
294
|
+
a.setHeader("Content-Type", "application/json"), a.setHeader("Cache-Control", "no-store"), a.end(JSON.stringify(d));
|
|
295
|
+
} catch (d) {
|
|
296
|
+
a.statusCode = 500, a.setHeader("Content-Type", "application/json"), a.end(JSON.stringify({ error: d instanceof Error ? d.message : String(d) }));
|
|
297
|
+
}
|
|
298
|
+
}), n.middlewares.use("/__asset", (s, a) => {
|
|
299
|
+
try {
|
|
300
|
+
const d = (s.url ?? "").split("?")[0], g = R(decodeURIComponent(d)), u = f(l, g);
|
|
301
|
+
if (u !== l && !u.startsWith(l + "/"))
|
|
302
|
+
return a.statusCode = 403, a.end("forbidden");
|
|
303
|
+
if (!w(u) || !P(u).isFile())
|
|
304
|
+
return a.statusCode = 404, a.end(`asset not found: ${g}`);
|
|
305
|
+
a.setHeader("Content-Type", W(g)), a.setHeader("Cache-Control", "no-store"), x(u).pipe(a);
|
|
306
|
+
} catch (d) {
|
|
307
|
+
a.statusCode = 400, a.end(d instanceof Error ? d.message : String(d));
|
|
308
|
+
}
|
|
309
|
+
}), e.watch) {
|
|
310
|
+
n.watcher.add(l);
|
|
311
|
+
const s = (a) => {
|
|
312
|
+
f(a).startsWith(l) && n.ws.send({ type: "full-reload", path: "*" });
|
|
313
|
+
};
|
|
314
|
+
n.watcher.on("change", s), n.watcher.on("add", s), n.watcher.on("unlink", s);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
});
|
|
320
|
+
return await o.listen(), {
|
|
321
|
+
url: o.resolvedUrls?.local[0] ?? `http://localhost:${e.port ?? 6180}/`,
|
|
322
|
+
async close() {
|
|
323
|
+
await o.close();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const G = C(import.meta.url), I = G("../package.json"), Z = I.version;
|
|
328
|
+
export {
|
|
329
|
+
v as D,
|
|
330
|
+
z as U,
|
|
331
|
+
K as a,
|
|
332
|
+
Y as b,
|
|
333
|
+
X as p,
|
|
334
|
+
p as r,
|
|
335
|
+
Q as s,
|
|
336
|
+
Z as v
|
|
337
|
+
};
|
|
338
|
+
//# sourceMappingURL=version-B2TKvZen.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sequio/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The sequio command-line: `sequio render <file>` encodes a composition to video (pure Node, PixiJS WebGPU), `sequio frame <file>` writes a single-frame PNG, and `sequio preview <file>` serves a live in-browser preview (with --watch).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "slightc",
|
|
8
|
+
"homepage": "https://sequio-demo.vercel.app/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/slightc/sequio.git",
|
|
12
|
+
"directory": "packages/cli"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/slightc/sequio/issues"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"sequio": "./bin/sequio.js"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./externals": {
|
|
29
|
+
"types": "./dist/externals.d.ts",
|
|
30
|
+
"import": "./dist/externals.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"bin",
|
|
36
|
+
"preview",
|
|
37
|
+
"!dist/**/*.map"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"cli",
|
|
47
|
+
"video-editor",
|
|
48
|
+
"render",
|
|
49
|
+
"preview",
|
|
50
|
+
"pixijs"
|
|
51
|
+
],
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"gsap": "^3.12.0",
|
|
54
|
+
"vite": "^6.0.0",
|
|
55
|
+
"@sequio/engine": "^0.1.1",
|
|
56
|
+
"@sequio/server": "^0.1.0",
|
|
57
|
+
"@sequio/runtime": "^0.1.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"dev": "tsx src/cli.ts",
|
|
61
|
+
"build": "vite build",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"verify:cli": "tsx scripts/verify-cli.ts"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser {@link AssetLoader} for `sequio preview`: fetch a composition's
|
|
3
|
+
* project-relative media file from the dev server's `/__asset/…` static route
|
|
4
|
+
* (see `src/preview.ts`) and return it as a `Blob`, so `await loadAsset('./clip.mp4')`
|
|
5
|
+
* resolves in the in-browser preview exactly as it does in the Node render
|
|
6
|
+
* (contract #3). The runtime normalizes the path before calling this.
|
|
7
|
+
*/
|
|
8
|
+
import type { AssetLoader } from '@sequio/runtime';
|
|
9
|
+
|
|
10
|
+
/** The preview's asset loader: the dev server serves project files under `/__asset/`. */
|
|
11
|
+
export function browserAssetLoader(): AssetLoader {
|
|
12
|
+
return async (path: string): Promise<Blob> => {
|
|
13
|
+
const res = await fetch(`/__asset/${path}`, { cache: 'no-store' });
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`local asset not found: ${path} (HTTP ${res.status}). ` +
|
|
17
|
+
`Drop the file into the composition's folder, or reference a network URL.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return res.blob();
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>sequio preview</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: dark;
|
|
10
|
+
--bg: #0b0b0e;
|
|
11
|
+
--panel: #16161c;
|
|
12
|
+
--border: #2a2a33;
|
|
13
|
+
--fg: #e6e6ea;
|
|
14
|
+
--muted: #9a9aa6;
|
|
15
|
+
--accent: #38bdf8;
|
|
16
|
+
--err: #f87171;
|
|
17
|
+
--ok: #4ade80;
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
color: var(--fg);
|
|
27
|
+
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
28
|
+
}
|
|
29
|
+
header {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
padding: 10px 16px;
|
|
34
|
+
border-bottom: 1px solid var(--border);
|
|
35
|
+
background: var(--panel);
|
|
36
|
+
}
|
|
37
|
+
header .brand { font-weight: 600; letter-spacing: 0.02em; }
|
|
38
|
+
header .file { color: var(--muted); font-family: ui-monospace, monospace; font-size: 12px; }
|
|
39
|
+
main {
|
|
40
|
+
flex: 1;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
padding: 16px;
|
|
45
|
+
overflow: auto;
|
|
46
|
+
}
|
|
47
|
+
#stage {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
max-width: 100%;
|
|
52
|
+
}
|
|
53
|
+
#stage canvas {
|
|
54
|
+
/* Pixi (autoDensity) sets explicit inline width/height on the canvas,
|
|
55
|
+
which makes max-width/max-height clamp each axis independently and
|
|
56
|
+
breaks the aspect ratio when only one binds. Force width/height back
|
|
57
|
+
to auto so the intrinsic buffer ratio is used and the max-* limits
|
|
58
|
+
scale it proportionally. */
|
|
59
|
+
width: auto !important;
|
|
60
|
+
height: auto !important;
|
|
61
|
+
max-width: 100%;
|
|
62
|
+
max-height: 78vh;
|
|
63
|
+
background: #000;
|
|
64
|
+
border: 1px solid var(--border);
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
}
|
|
67
|
+
footer {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 12px;
|
|
71
|
+
padding: 10px 16px;
|
|
72
|
+
border-top: 1px solid var(--border);
|
|
73
|
+
background: var(--panel);
|
|
74
|
+
}
|
|
75
|
+
button {
|
|
76
|
+
background: #23232c;
|
|
77
|
+
color: var(--fg);
|
|
78
|
+
border: 1px solid var(--border);
|
|
79
|
+
border-radius: 6px;
|
|
80
|
+
padding: 6px 12px;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
font: inherit;
|
|
83
|
+
}
|
|
84
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
85
|
+
button:hover:not(:disabled) { border-color: var(--accent); }
|
|
86
|
+
input[type='range'] { flex: 1; accent-color: var(--accent); }
|
|
87
|
+
.time { color: var(--muted); font-family: ui-monospace, monospace; font-size: 12px; min-width: 92px; }
|
|
88
|
+
.log {
|
|
89
|
+
padding: 8px 16px;
|
|
90
|
+
font-family: ui-monospace, monospace;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
white-space: pre-wrap;
|
|
93
|
+
color: var(--muted);
|
|
94
|
+
border-top: 1px solid var(--border);
|
|
95
|
+
background: #101015;
|
|
96
|
+
}
|
|
97
|
+
.log.ok { color: var(--ok); }
|
|
98
|
+
.log.err { color: var(--err); }
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<header>
|
|
103
|
+
<span class="brand">▸ sequio preview</span>
|
|
104
|
+
<span class="file" id="file"></span>
|
|
105
|
+
</header>
|
|
106
|
+
<main><div id="stage"></div></main>
|
|
107
|
+
<footer>
|
|
108
|
+
<button id="play" disabled>▶ Play</button>
|
|
109
|
+
<input id="scrub" type="range" min="0" max="1" step="0.001" value="0" disabled />
|
|
110
|
+
<span class="time" id="time">0.00 / 0.00s</span>
|
|
111
|
+
</footer>
|
|
112
|
+
<div class="log" id="log">Loading…</div>
|
|
113
|
+
<script type="module" src="/preview.ts"></script>
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of `sequio preview`. Fetches `/__bundle` (the project
|
|
3
|
+
* snapshot the dev server serves), compiles + runs it with the {@link Runtime}
|
|
4
|
+
* into a {@link Composer}, and plays the resulting graph live via
|
|
5
|
+
* `composer.preview(stage)` — the same in-browser render core studio's Code Mode
|
|
6
|
+
* uses. With `--watch`, the dev server issues a full-reload on any file change,
|
|
7
|
+
* so this module simply re-runs from scratch on load.
|
|
8
|
+
*/
|
|
9
|
+
import { Runtime, type PreviewHandle, type RuntimeBundle } from '@sequio/runtime';
|
|
10
|
+
// Browser-safe subpath so the preview page resolves the same `cliExternals` in
|
|
11
|
+
// both hosts: from source in this repo (alias in src/preview.ts) and from the
|
|
12
|
+
// published `dist/externals.js` when installed from npm.
|
|
13
|
+
import { cliExternals } from '@sequio/cli/externals';
|
|
14
|
+
import { browserAssetLoader } from './assets';
|
|
15
|
+
|
|
16
|
+
function $<T extends HTMLElement>(id: string): T {
|
|
17
|
+
return document.getElementById(id) as T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const stageEl = $<HTMLDivElement>('stage');
|
|
21
|
+
const fileEl = $<HTMLSpanElement>('file');
|
|
22
|
+
const logEl = $<HTMLDivElement>('log');
|
|
23
|
+
const playBtn = $<HTMLButtonElement>('play');
|
|
24
|
+
const scrub = $<HTMLInputElement>('scrub');
|
|
25
|
+
const timeLabel = $<HTMLSpanElement>('time');
|
|
26
|
+
|
|
27
|
+
function log(message: string, kind: 'info' | 'ok' | 'err' = 'info'): void {
|
|
28
|
+
logEl.textContent = message;
|
|
29
|
+
logEl.className = `log${kind === 'ok' ? ' ok' : kind === 'err' ? ' err' : ''}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fmt(t: number): string {
|
|
33
|
+
return t.toFixed(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let preview: PreviewHandle | null = null;
|
|
37
|
+
|
|
38
|
+
function updateTransport(t: number): void {
|
|
39
|
+
if (!preview) return;
|
|
40
|
+
scrub.value = String(t);
|
|
41
|
+
timeLabel.textContent = `${fmt(t)} / ${fmt(preview.duration)}s`;
|
|
42
|
+
playBtn.textContent = preview.playing ? '⏸ Pause' : '▶ Play';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
playBtn.addEventListener('click', () => {
|
|
46
|
+
if (!preview) return;
|
|
47
|
+
if (preview.playing) preview.pause();
|
|
48
|
+
else preview.play();
|
|
49
|
+
updateTransport(preview.clock.currentTime);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
scrub.addEventListener('input', () => {
|
|
53
|
+
if (!preview) return;
|
|
54
|
+
preview.pause();
|
|
55
|
+
const t = parseFloat(scrub.value);
|
|
56
|
+
preview.seek(t);
|
|
57
|
+
updateTransport(t);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
async function boot(): Promise<void> {
|
|
61
|
+
log('Loading composition…');
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch('/__bundle', { cache: 'no-store' });
|
|
64
|
+
const bundle = (await res.json()) as RuntimeBundle & { error?: string };
|
|
65
|
+
if (!res.ok || bundle.error) throw new Error(bundle.error ?? `bundle request failed (${res.status})`);
|
|
66
|
+
|
|
67
|
+
fileEl.textContent = bundle.entry;
|
|
68
|
+
|
|
69
|
+
const composer = await new Runtime({
|
|
70
|
+
...bundle,
|
|
71
|
+
externals: cliExternals(),
|
|
72
|
+
// Resolve `loadAsset('./clip.mp4')` by fetching the file the dev server
|
|
73
|
+
// serves under /__asset/ (readBundle keeps binary assets out of the bundle).
|
|
74
|
+
loadAsset: browserAssetLoader(),
|
|
75
|
+
}).run();
|
|
76
|
+
stageEl.replaceChildren();
|
|
77
|
+
preview = await composer.preview(stageEl);
|
|
78
|
+
|
|
79
|
+
scrub.min = '0';
|
|
80
|
+
scrub.max = String(preview.duration);
|
|
81
|
+
scrub.disabled = false;
|
|
82
|
+
playBtn.disabled = false;
|
|
83
|
+
preview.clock.onTick((t) => updateTransport(t));
|
|
84
|
+
// Reaching the end auto-pauses the clock but fires no further tick, so
|
|
85
|
+
// refresh the transport (Play button label, scrub) on `ended` too.
|
|
86
|
+
preview.clock.onEnded(() => updateTransport(preview!.clock.currentTime));
|
|
87
|
+
updateTransport(0);
|
|
88
|
+
|
|
89
|
+
const tracks = preview.built.compositor.getTracks();
|
|
90
|
+
const clips = tracks.reduce((n, t) => n + t.clips.length, 0);
|
|
91
|
+
log(
|
|
92
|
+
`Ran ${Object.keys(bundle.files).length} file(s) → ${tracks.length} track(s), ` +
|
|
93
|
+
`${clips} clip(s), ${fmt(preview.duration)}s. Ready.`,
|
|
94
|
+
'ok',
|
|
95
|
+
);
|
|
96
|
+
// Publish a result the `verify:cli` harness can assert on (repo convention).
|
|
97
|
+
(window as unknown as { __PREVIEW_TEST__: unknown }).__PREVIEW_TEST__ = {
|
|
98
|
+
ok: tracks.length > 0 && preview.duration > 0,
|
|
99
|
+
tracks: tracks.length,
|
|
100
|
+
clips,
|
|
101
|
+
duration: preview.duration,
|
|
102
|
+
};
|
|
103
|
+
} catch (err) {
|
|
104
|
+
log(err instanceof Error ? (err.stack ?? err.message) : String(err), 'err');
|
|
105
|
+
(window as unknown as { __PREVIEW_TEST__: unknown }).__PREVIEW_TEST__ = {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: err instanceof Error ? err.message : String(err),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
void boot();
|