@tongue-core/sdk 0.0.0-stage → 0.1.0-rc.2
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/LICENSE +21 -0
- package/README.md +155 -2
- package/dist/backends.d.ts +24 -0
- package/dist/backends.js +17 -0
- package/dist/build/build.d.ts +48 -0
- package/dist/build/build.js +162 -0
- package/dist/check.d.ts +18 -0
- package/dist/check.js +17 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.js +38 -0
- package/dist/encode/encoders.d.ts +68 -0
- package/dist/encode/encoders.js +158 -0
- package/dist/errors.d.ts +53 -0
- package/dist/errors.js +69 -0
- package/dist/guards.d.ts +1 -0
- package/dist/guards.js +3 -0
- package/dist/import/convert.d.ts +12 -0
- package/dist/import/convert.js +33 -0
- package/dist/import/importer.d.ts +11 -0
- package/dist/import/importer.js +94 -0
- package/dist/import/media.d.ts +52 -0
- package/dist/import/media.js +127 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +19 -0
- package/dist/init.d.ts +30 -0
- package/dist/init.js +44 -0
- package/dist/project.d.ts +47 -0
- package/dist/project.js +101 -0
- package/dist/raster/direct.d.ts +10 -0
- package/dist/raster/direct.js +3 -0
- package/dist/raster/gpu.d.ts +33 -0
- package/dist/raster/gpu.js +112 -0
- package/dist/raster/index.d.ts +4 -0
- package/dist/raster/index.js +7 -0
- package/dist/raster/library.d.ts +9 -0
- package/dist/raster/library.js +47 -0
- package/dist/raster/native.d.ts +37 -0
- package/dist/raster/native.js +128 -0
- package/dist/raster/rasters.d.ts +20 -0
- package/dist/raster/rasters.js +37 -0
- package/dist/render/film-loader.d.ts +20 -0
- package/dist/render/film-loader.js +107 -0
- package/dist/render/frame.d.ts +5 -0
- package/dist/render/frame.js +11 -0
- package/dist/render/pipeline.d.ts +58 -0
- package/dist/render/pipeline.js +150 -0
- package/dist/render/protocol.d.ts +70 -0
- package/dist/render/protocol.js +115 -0
- package/dist/render/soundtrack-process.d.ts +1 -0
- package/dist/render/soundtrack-process.js +27 -0
- package/dist/render/stills.d.ts +29 -0
- package/dist/render/stills.js +66 -0
- package/dist/render/video-frames.d.ts +20 -0
- package/dist/render/video-frames.js +101 -0
- package/dist/render/worker.d.ts +1 -0
- package/dist/render/worker.js +72 -0
- package/dist/studio/server.d.ts +17 -0
- package/dist/studio/server.js +155 -0
- package/dist/tools.d.ts +21 -0
- package/dist/tools.js +42 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +7 -0
- package/package.json +50 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Backpine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tongue-core/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Everything you do with a Tongue film, as a library: import its assets, plan and check it, draw
|
|
4
|
+
frames, render video, open it in the studio, and list what the machine can render with. The
|
|
5
|
+
`tongue` CLI is a thin shell over this package, so anything the CLI does an application can do
|
|
6
|
+
here, with the same results. Built with Effect v4 (orchestration only: the per-frame path is plain
|
|
7
|
+
TypeScript); a Promise facade is included for applications that don't use Effect.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @tongue-core/sdk@next # Bun ≥ 1.3; ffmpeg and ffprobe on the PATH
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Promise API
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createTongue } from "@tongue-core/sdk";
|
|
17
|
+
|
|
18
|
+
const tongue = createTongue();
|
|
19
|
+
await tongue.init("my-film", { template: "social-post" }); // a film project from a template
|
|
20
|
+
const { build } = await tongue.build("my-film"); // my-film/dist: film.json, build.json, bundles
|
|
21
|
+
await tongue.stills("my-film", { frames: [0, 30], outDir: "out/hook", scene: { id: "hook" } }); // one scene alone
|
|
22
|
+
const { text, summary } = await tongue.info("src/film.tsx"); // the timeline, as text and JSON
|
|
23
|
+
const report = await tongue.check("src/film.tsx"); // { ok, problems, warnings }: never rejects for a broken film
|
|
24
|
+
await tongue.sheet("src/film.tsx", { frames: [0, 90, 300], outDir: "out/look" });
|
|
25
|
+
const video = await tongue.render("src/film.tsx", { out: "out/film.mp4", from: 90, to: 300, scale: 0.5 });
|
|
26
|
+
const machine = await tongue.backends(); // rasters and encoders that work here
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Failures reject with the SDK's tagged errors (below).
|
|
30
|
+
|
|
31
|
+
## Effect API
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { BunServices } from "@effect/platform-bun";
|
|
35
|
+
import { openFilm, renderVideo } from "@tongue-core/sdk";
|
|
36
|
+
import { Effect } from "effect";
|
|
37
|
+
|
|
38
|
+
const program = Effect.gen(function* () {
|
|
39
|
+
const film = yield* openFilm("src/film.tsx"); // load, import assets, plan
|
|
40
|
+
return yield* renderVideo(film, { out: "out/film.mp4", encoder: "auto", raster: "auto" });
|
|
41
|
+
});
|
|
42
|
+
await Effect.runPromise(program.pipe(Effect.provide(BunServices.layer)));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Function | Does |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `initProject(dir, { template, name })` | a film project in a new or empty folder, from a built-in template or a template folder |
|
|
48
|
+
| `listTemplates()` / `readTemplate(nameOrPath)` | the built-in templates, or one by name or path |
|
|
49
|
+
| `buildFilm(project, { out })` | builds a film project into `out` (default `dist/`): build.json, film.json, a bundle per scene |
|
|
50
|
+
| `openFilm(path, { scene })` | a film ready to render: a project is built (into `.tongue/builds/<id>`, named by its content) and planned from film.json, a film module is imported and planned; assets imported (cached by size, mtime and options) either way. `scene: { id, props }` opens one scene of a project on its own |
|
|
51
|
+
| `describeFilm(film)` | the plan as text (for agents to read) and as a JSON summary |
|
|
52
|
+
| `checkFilm(path)` | problems and warnings as data |
|
|
53
|
+
| `renderStills(film, { frames, scale, outDir })` / `renderSheet(…)` | PNGs drawn by the wasm raster (the player's), or tiled into one labelled sheet |
|
|
54
|
+
| `renderVideo(film, options)` | an MP4 with sound: the whole film or `[from, to)`, at any scale; `onStart` once the request is checked and the raster and encoder are chosen, `onProgress` while it draws. A frame that throws fails the render with `Frame <n>: <message>` |
|
|
55
|
+
| `listBackends()` | every raster and encoder, each tried for real |
|
|
56
|
+
| `studioServer(path, { port, remote })` | the studio on a project (rebuilt and reloaded on every edit) or on a build served anywhere |
|
|
57
|
+
|
|
58
|
+
## Films and projects
|
|
59
|
+
|
|
60
|
+
A **film project** is a folder with a `tongue.json` ([docs/film-json.md](../../docs/film-json.md)):
|
|
61
|
+
`film.tsx`, `scenes/`, `kit/`, `lib/`, `assets.ts`, `assets/`. It needs nothing installed: the SDK
|
|
62
|
+
reads its files, compiles and bundles it with `@tongue-core/project`, loads the bundles with its own
|
|
63
|
+
`@tongue-core/film`, imports the assets they declare (ffprobe gives media lengths), extracts film.json
|
|
64
|
+
and writes the build. Everything renders from the build: in this process, in the render workers
|
|
65
|
+
(they open build.json and the bundles), and in the studio (`tongue studio` serves the build; the
|
|
66
|
+
browser fetches it). `.tongue/types/film` links to the SDK's `@tongue-core/film`, which the
|
|
67
|
+
project's `tsconfig.json` points editors at.
|
|
68
|
+
|
|
69
|
+
A **film module** (the way films were written before projects) is a `film.tsx` in a folder with a
|
|
70
|
+
`package.json`. The SDK bundles it with Bun (into `.tongue/modules/`, JSX compiled for Tongue) against
|
|
71
|
+
its own `@tongue-core/film`, the copy the engine reads, as it links a project's bundles, then plans
|
|
72
|
+
it. The folder may install `@tongue-core/film` too, for its editor's types
|
|
73
|
+
(`"jsx": "react-jsx", "jsxImportSource": "@tongue-core/film"` in its tsconfig); that copy isn't the one
|
|
74
|
+
that runs.
|
|
75
|
+
|
|
76
|
+
Either way, asset paths are relative to the root, Tongue writes `.tongue/` there (imported assets,
|
|
77
|
+
builds, render work) and renders to `out/`.
|
|
78
|
+
|
|
79
|
+
A film's assets are what its modules declare (`font()`, `image()`, `audio()`, `video()`) while
|
|
80
|
+
the SDK loads it, plus the media its clips play; `<Film assets={[…]}>` replaces the declared ones.
|
|
81
|
+
Importing turns them into what the engine reads: TrueType fonts, QOI images, WAV audio, video facts
|
|
82
|
+
and a poster frame.
|
|
83
|
+
|
|
84
|
+
## How a video renders
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
coordinator ──segments──▶ worker 1: frames ─▶ raster ─▶ encoder ─▶ seg-0000.mp4
|
|
88
|
+
◀─progress── worker 2 … seg-0001.mp4
|
|
89
|
+
soundtrack: sounds + audio clips ─▶ mix ─▶ AAC audio.m4a
|
|
90
|
+
join: concat + audio ─(stream copy)─▶ film.mp4, frame count probed
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **Workers** are processes (`src/render/worker.ts`), one per physical core by default. Each loads
|
|
94
|
+
the film and its raster once and renders every segment it is sent into that segment's own
|
|
95
|
+
encoder, so the JIT stays warm and glyphs and images stay uploaded.
|
|
96
|
+
- **Segments** are one to two seconds of frames, shrinking towards the end so the last ones finish
|
|
97
|
+
together. Each is a closed-GOP encode; the join copies streams without re-encoding.
|
|
98
|
+
- **Video clips** are decoded by ffmpeg in order, frame after frame, and restarted on jumps.
|
|
99
|
+
- **The soundtrack** is mixed in its own process while the workers draw (`soundtrack-process.ts`).
|
|
100
|
+
|
|
101
|
+
Rasters (who draws) and encoders (who encodes) are chosen by name, because the choice is made here
|
|
102
|
+
but runs inside the workers:
|
|
103
|
+
|
|
104
|
+
| Raster | |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `native` | the Rust raster built for x86-64-v3 (`@tongue-core/raster-linux-x64`); byte-identical to wasm, 2–3× faster |
|
|
107
|
+
| `wasm` | the engine's embedded WebAssembly raster, the browser player's; runs anywhere |
|
|
108
|
+
| `gpu` | EGL + GLES 3.1; within a few levels of the CPU rasters, never chosen by `auto` |
|
|
109
|
+
|
|
110
|
+
Encoders: `x264` (veryfast, crf 14), `vaapi-hevc` / `vaapi-h264` (constant QP 18 / 16, with
|
|
111
|
+
CPU-conversion variants), and `nvenc-*` / `qsv-*` (written from vendor documentation, untested).
|
|
112
|
+
`auto` takes the first that works of nvenc-hevc, vaapi-hevc, vaapi-hevc-cpuconv, qsv-hevc, x264.
|
|
113
|
+
|
|
114
|
+
On this machine (16 threads, AMD GPU) the 35.5 s demo renders at 1080p with sound in about 12 s
|
|
115
|
+
(≈ 87 fps) with `native` + `vaapi-hevc`.
|
|
116
|
+
|
|
117
|
+
## Errors
|
|
118
|
+
|
|
119
|
+
Every failure is a `Schema.TaggedError` with a message that says what to change. `exitCodeOf`
|
|
120
|
+
gives the CLI's exit code.
|
|
121
|
+
|
|
122
|
+
| Error | When | Exit |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `FilmFailure` | the module is missing, throws, isn't a `<Film>`, or its timeline doesn't plan | 2 |
|
|
125
|
+
| `AssetFailure` | an asset's file is missing or unreadable | 2 |
|
|
126
|
+
| `InvalidRequest` | a frame outside the film, a bad range or scale | 2 |
|
|
127
|
+
| `ToolFailure` | ffmpeg or ffprobe is missing or failed | 3 |
|
|
128
|
+
| `BackendUnavailable` | the raster or encoder asked for doesn't work here | 3 |
|
|
129
|
+
| `RenderFailure` | a worker died or a frame failed | 3 |
|
|
130
|
+
| `LayoutError`, `CompileError`, `LoadError`, `TimelineError`, `DocError`, `TemplateError` | a film project doesn't build (from `@tongue-core/project`: each names its file, line or path) | 2 |
|
|
131
|
+
|
|
132
|
+
## Environment
|
|
133
|
+
|
|
134
|
+
| Variable | |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `TONGUE_FFMPEG`, `TONGUE_FFPROBE` | the tools' paths (default: `ffmpeg`, `ffprobe` on the PATH) |
|
|
137
|
+
| `TONGUE_NATIVE_RASTER`, `TONGUE_GPU_RASTER` | a raster library to use instead of `@tongue-core/raster-linux-x64`'s |
|
|
138
|
+
| `TONGUE_VAAPI_DEVICE` | the render node for VAAPI and Quick Sync (default `/dev/dri/renderD128`) |
|
|
139
|
+
|
|
140
|
+
## Layout
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
src/
|
|
144
|
+
index.ts, client.ts the Effect API and the Promise facade
|
|
145
|
+
project.ts projects, openFilm and buildFilm
|
|
146
|
+
build/build.ts building a film project on this machine: its files, a module host, the build folder, editor types
|
|
147
|
+
init.ts init from templates
|
|
148
|
+
check.ts, backends.ts checkFilm, listBackends
|
|
149
|
+
import/ asset import: fonts, images, audio, video (ffprobe, posters)
|
|
150
|
+
render/ stills, the video pipeline, its worker and soundtrack processes, video decoding
|
|
151
|
+
encode/encoders.ts the ffmpeg encoders and how each is tried
|
|
152
|
+
raster/ the native and GPU raster bindings (index.ts) and the registry (rasters.ts)
|
|
153
|
+
studio/server.ts the studio's server: the app bundled, a project's build served and rebuilt
|
|
154
|
+
test/ unit tests, end-to-end ones on fixtures/tiny (a film with text, video and sound), and
|
|
155
|
+
projects from every template (project.test.ts: film.json draws the same frames as film.tsx)
|
|
156
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { type EncoderName } from "./encode/encoders.js";
|
|
3
|
+
import { type RasterName } from "./raster/rasters.js";
|
|
4
|
+
export interface BackendsReport {
|
|
5
|
+
readonly rasters: ReadonlyArray<{
|
|
6
|
+
readonly name: RasterName;
|
|
7
|
+
readonly available: boolean;
|
|
8
|
+
readonly exact: boolean;
|
|
9
|
+
readonly summary: string;
|
|
10
|
+
}>;
|
|
11
|
+
readonly encoders: ReadonlyArray<{
|
|
12
|
+
readonly name: EncoderName;
|
|
13
|
+
readonly available: boolean;
|
|
14
|
+
readonly hardware: boolean;
|
|
15
|
+
readonly tested: boolean;
|
|
16
|
+
readonly summary: string;
|
|
17
|
+
}>;
|
|
18
|
+
/** What `auto` picks here. */
|
|
19
|
+
readonly auto: {
|
|
20
|
+
readonly raster: RasterName;
|
|
21
|
+
readonly encoder: EncoderName | null;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare const listBackends: () => Effect.Effect<BackendsReport, import("effect/Config").ConfigError, never>;
|
package/dist/backends.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { AUTO_ENCODERS, DEFAULT_SETTINGS, ENCODER_NAMES, ENCODER_SUMMARIES, encoderSpec, encoderWorks } from "./encode/encoders.js";
|
|
3
|
+
import { autoRaster, RASTERS } from "./raster/rasters.js";
|
|
4
|
+
import { ffmpegPath } from "./tools.js";
|
|
5
|
+
export const listBackends = Effect.fn("listBackends")(function* () {
|
|
6
|
+
const ffmpeg = yield* ffmpegPath;
|
|
7
|
+
const encoders = yield* Effect.forEach(ENCODER_NAMES, (name) => Effect.promise(async () => {
|
|
8
|
+
const spec = encoderSpec(name, DEFAULT_SETTINGS, 30);
|
|
9
|
+
return { name, available: await encoderWorks(ffmpeg, spec), hardware: spec.hardware, tested: spec.tested, summary: ENCODER_SUMMARIES[name] };
|
|
10
|
+
}), { concurrency: "unbounded" });
|
|
11
|
+
const report = {
|
|
12
|
+
rasters: RASTERS.map((r) => ({ name: r.name, available: r.available(), exact: r.exact, summary: r.summary })),
|
|
13
|
+
encoders,
|
|
14
|
+
auto: { raster: autoRaster().name, encoder: AUTO_ENCODERS.find((n) => encoders.some((e) => e.name === n && e.available)) ?? null },
|
|
15
|
+
};
|
|
16
|
+
return report;
|
|
17
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Asset, MediaInfo, SceneDefinition } from "@tongue-core/film";
|
|
2
|
+
import { type Build, type ModuleHost, type ProjectFiles } from "@tongue-core/project";
|
|
3
|
+
import { Effect, FileSystem, type PlatformError } from "effect";
|
|
4
|
+
import { type ImportReport } from "../import/importer.js";
|
|
5
|
+
import type { Project } from "../project.js";
|
|
6
|
+
/** A project's code and data: every .ts, .tsx and .json file outside hidden folders and the ones
|
|
7
|
+
* tools write. */
|
|
8
|
+
export declare const readProjectFiles: (root: string) => Effect.Effect<ProjectFiles, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
9
|
+
/** Loads bundles by writing each to `dir` (named by its hash, so written once) and importing it. */
|
|
10
|
+
export declare const fileModuleHost: (dir: string) => ModuleHost;
|
|
11
|
+
/** Points the project's `.tongue/types/film` at this SDK's @tongue-core/film (tsconfig.json reads it). */
|
|
12
|
+
export declare const linkFilmTypes: (root: string) => Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
13
|
+
export interface BuiltProject {
|
|
14
|
+
readonly build: Build;
|
|
15
|
+
/** Where the build was written. */
|
|
16
|
+
readonly dir: string;
|
|
17
|
+
readonly imported: ImportReport;
|
|
18
|
+
/** The scenes the bundles export, by id: what film.json is assembled with. */
|
|
19
|
+
readonly scenes: ReadonlyMap<string, SceneDefinition>;
|
|
20
|
+
readonly media: MediaInfo;
|
|
21
|
+
}
|
|
22
|
+
/** The imported files a film's assets are (a video is its facts, its poster and its proxy's
|
|
23
|
+
* segments), with their sizes and hashes: what build.json lists. */
|
|
24
|
+
export declare const importedFiles: (project: Project, assets: readonly Asset[]) => Effect.Effect<{
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly bytes: number;
|
|
27
|
+
readonly hash: string;
|
|
28
|
+
}[], PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
29
|
+
/** How a written build reaches its assets: copies of the files (a build to publish), or a link to
|
|
30
|
+
* the project's imported assets (a build for Tongue's own use). */
|
|
31
|
+
export type AssetPlacement = {
|
|
32
|
+
readonly kind: "copy";
|
|
33
|
+
readonly from: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "link";
|
|
36
|
+
readonly to: string;
|
|
37
|
+
};
|
|
38
|
+
/** Writes a build to `dir`, replacing the build that was there (old bundles and assets included). */
|
|
39
|
+
export declare const writeBuildFiles: (dir: string, build: Build, assets: AssetPlacement) => Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
40
|
+
/** Keeps a build in `.tongue/builds/<id>`, named by its content and never rewritten, so renders
|
|
41
|
+
* reading one build are never disturbed by the next. Written to a temporary folder first and moved
|
|
42
|
+
* into place, so a build folder is either whole or not there. */
|
|
43
|
+
export declare const keepBuild: (project: Project, build: Build) => Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
44
|
+
/** Builds a film project into `dir` (what `tongue build` does: dist/ by default), replacing what
|
|
45
|
+
* was there, with copies of its imported assets: a folder any static host can serve. */
|
|
46
|
+
export declare const buildFilmProject: (project: Project, dir: string) => Effect.Effect<BuiltProject, import("../errors.js").AssetFailure | import("../errors.js").ToolFailure | import("@tongue-core/project").LayoutError | import("@tongue-core/project").CompileError | import("@tongue-core/project").LoadError | import("@tongue-core/project").TimelineError | import("@tongue-core/project").DocError | import("effect/Config").ConfigError | PlatformError.PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | import("effect/Path").Path>;
|
|
47
|
+
/** Builds a film project for Tongue's own use (stills, renders, dev): kept in .tongue/builds. */
|
|
48
|
+
export declare const buildForWork: (project: Project) => Effect.Effect<BuiltProject, import("../errors.js").AssetFailure | import("../errors.js").ToolFailure | import("@tongue-core/project").LayoutError | import("@tongue-core/project").CompileError | import("@tongue-core/project").LoadError | import("@tongue-core/project").TimelineError | import("@tongue-core/project").DocError | import("effect/Config").ConfigError | PlatformError.PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | import("effect/Path").Path>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
+
import { importedFiles as filesOf, videoProxyDir } from "@tongue-core/engine";
|
|
13
|
+
import { compileProject, extractFilm, IGNORED_DIRS, loadProject, sha256, TYPES_LINK, writeBuild, } from "@tongue-core/project";
|
|
14
|
+
import { Effect, FileSystem } from "effect";
|
|
15
|
+
import { importAssets } from "../import/importer.js";
|
|
16
|
+
import { sdkVersion } from "../version.js";
|
|
17
|
+
// Building a film project on this machine: read its code, compile and bundle it, run the bundles,
|
|
18
|
+
// import the assets they declare (media lengths go into film.json), extract film.json and write
|
|
19
|
+
// the build, with the imported asset files listed in build.json (and copied beside it, or linked,
|
|
20
|
+
// so a build folder is everything a player needs). Everything but the files and the module
|
|
21
|
+
// loading is @tongue-core/project's.
|
|
22
|
+
/** A project's code and data: every .ts, .tsx and .json file outside hidden folders and the ones
|
|
23
|
+
* tools write. */
|
|
24
|
+
export const readProjectFiles = Effect.fn("readProjectFiles")(function* (root) {
|
|
25
|
+
const fs = yield* FileSystem.FileSystem;
|
|
26
|
+
const files = new Map();
|
|
27
|
+
const walk = (dir) => Effect.gen(function* () {
|
|
28
|
+
for (const name of (yield* fs.readDirectory(dir)).sort()) {
|
|
29
|
+
const path = join(dir, name);
|
|
30
|
+
const info = yield* fs.stat(path);
|
|
31
|
+
if (info.type === "Directory") {
|
|
32
|
+
// What tools write, and anything hidden (.git, .vscode): never the film's.
|
|
33
|
+
if (!IGNORED_DIRS.has(name) && !name.startsWith("."))
|
|
34
|
+
yield* walk(path);
|
|
35
|
+
}
|
|
36
|
+
else if (/\.(ts|tsx|json)$/.test(name)) {
|
|
37
|
+
files.set(relative(root, path).split(sep).join("/"), yield* fs.readFileString(path));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
yield* walk(root);
|
|
42
|
+
const out = files;
|
|
43
|
+
return out;
|
|
44
|
+
});
|
|
45
|
+
/** Loads bundles by writing each to `dir` (named by its hash, so written once) and importing it. */
|
|
46
|
+
export const fileModuleHost = (dir) => ({
|
|
47
|
+
load: async (bundle) => {
|
|
48
|
+
const file = join(dir, bundle.path);
|
|
49
|
+
if (!(await Bun.file(file).exists()))
|
|
50
|
+
await Bun.write(file, bundle.code);
|
|
51
|
+
const module = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
|
|
52
|
+
return module;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
/** The folder of the @tongue-core/film this SDK uses: what a project's editor types come from. */
|
|
56
|
+
const filmPackage = dirname(dirname(fileURLToPath(import.meta.resolve("@tongue-core/film"))));
|
|
57
|
+
/** Points the project's `.tongue/types/film` at this SDK's @tongue-core/film (tsconfig.json reads it). */
|
|
58
|
+
export const linkFilmTypes = Effect.fn("linkFilmTypes")(function* (root) {
|
|
59
|
+
const fs = yield* FileSystem.FileSystem;
|
|
60
|
+
const link = join(root, TYPES_LINK);
|
|
61
|
+
const current = yield* fs.readLink(link).pipe(Effect.orElseSucceed(() => null));
|
|
62
|
+
if (current === filmPackage)
|
|
63
|
+
return;
|
|
64
|
+
yield* fs.remove(link, { force: true });
|
|
65
|
+
yield* fs.makeDirectory(dirname(link), { recursive: true });
|
|
66
|
+
yield* fs.symlink(filmPackage, link);
|
|
67
|
+
});
|
|
68
|
+
/** The imported files a film's assets are (a video is its facts, its poster and its proxy's
|
|
69
|
+
* segments), with their sizes and hashes: what build.json lists. */
|
|
70
|
+
export const importedFiles = Effect.fn("importedFiles")(function* (project, assets) {
|
|
71
|
+
const fs = yield* FileSystem.FileSystem;
|
|
72
|
+
const proxies = yield* Effect.forEach(assets.filter((a) => a.kind === "video"), (a) => {
|
|
73
|
+
const dir = videoProxyDir(a);
|
|
74
|
+
return fs.readDirectory(join(project.assetsDir, dir)).pipe(Effect.map((names) => names.filter((n) => n.endsWith(".mp4")).sort().map((n) => `${dir}${n}`)), Effect.orElseSucceed(() => []));
|
|
75
|
+
});
|
|
76
|
+
const paths = [...new Set([...assets.flatMap(filesOf), ...proxies.flat()])];
|
|
77
|
+
return yield* Effect.forEach(paths, (path) => Effect.map(fs.readFile(join(project.assetsDir, path)), (bytes) => ({
|
|
78
|
+
path,
|
|
79
|
+
bytes: bytes.length,
|
|
80
|
+
hash: createHash("sha256").update(bytes).digest("hex"),
|
|
81
|
+
})), { concurrency: 8 });
|
|
82
|
+
});
|
|
83
|
+
/** Writes a build to `dir`, replacing the build that was there (old bundles and assets included). */
|
|
84
|
+
export const writeBuildFiles = Effect.fn("writeBuildFiles")(function* (dir, build, assets) {
|
|
85
|
+
const fs = yield* FileSystem.FileSystem;
|
|
86
|
+
yield* fs.makeDirectory(dir, { recursive: true });
|
|
87
|
+
for (const name of yield* fs.readDirectory(dir)) {
|
|
88
|
+
if (name === "scenes" || name === build.manifest.assets.dir || name.endsWith(".json"))
|
|
89
|
+
yield* fs.remove(join(dir, name), { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
for (const file of build.files) {
|
|
92
|
+
const path = join(dir, file.path);
|
|
93
|
+
yield* fs.makeDirectory(dirname(path), { recursive: true });
|
|
94
|
+
yield* fs.writeFileString(path, file.text);
|
|
95
|
+
}
|
|
96
|
+
const assetsDir = join(dir, build.manifest.assets.dir);
|
|
97
|
+
if (assets.kind === "link") {
|
|
98
|
+
yield* fs.symlink(assets.to, assetsDir);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const file of build.manifest.assets.files) {
|
|
102
|
+
const path = join(assetsDir, file.path);
|
|
103
|
+
yield* fs.makeDirectory(dirname(path), { recursive: true });
|
|
104
|
+
yield* fs.copyFile(join(assets.from, file.path), path);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
/** Builds kept in .tongue/builds: the newest this many, older ones removed. */
|
|
108
|
+
const KEPT_BUILDS = 8;
|
|
109
|
+
/** Keeps a build in `.tongue/builds/<id>`, named by its content and never rewritten, so renders
|
|
110
|
+
* reading one build are never disturbed by the next. Written to a temporary folder first and moved
|
|
111
|
+
* into place, so a build folder is either whole or not there. */
|
|
112
|
+
export const keepBuild = Effect.fn("keepBuild")(function* (project, build) {
|
|
113
|
+
const fs = yield* FileSystem.FileSystem;
|
|
114
|
+
const builds = join(project.workDir, "builds");
|
|
115
|
+
const id = (yield* Effect.promise(() => sha256(build.files.map((f) => `${f.path}\0${f.text}`).join("\0")))).slice(0, 16);
|
|
116
|
+
const dir = join(builds, id);
|
|
117
|
+
if (yield* fs.exists(join(dir, "build.json")))
|
|
118
|
+
return dir;
|
|
119
|
+
yield* fs.makeDirectory(builds, { recursive: true });
|
|
120
|
+
const temp = yield* fs.makeTempDirectory({ directory: builds, prefix: ".writing-" });
|
|
121
|
+
yield* writeBuildFiles(temp, build, { kind: "link", to: project.assetsDir });
|
|
122
|
+
// Another process may have kept the same build meanwhile: its copy is the same bytes.
|
|
123
|
+
yield* fs.rename(temp, dir).pipe(Effect.catch(() => fs.remove(temp, { recursive: true })));
|
|
124
|
+
yield* pruneBuilds(builds, dir);
|
|
125
|
+
return dir;
|
|
126
|
+
});
|
|
127
|
+
/** Removes all but the newest builds (never `keep`); a build in use elsewhere is old enough to go
|
|
128
|
+
* only after KEPT_BUILDS newer ones. */
|
|
129
|
+
const pruneBuilds = Effect.fn("pruneBuilds")(function* (builds, keep) {
|
|
130
|
+
const fs = yield* FileSystem.FileSystem;
|
|
131
|
+
const dated = yield* Effect.forEach(yield* fs.readDirectory(builds), (name) => fs.stat(join(builds, name)).pipe(Effect.map((info) => ({ dir: join(builds, name), at: info.mtime._tag === "Some" ? info.mtime.value.getTime() : 0, temp: name.startsWith(".") }))));
|
|
132
|
+
const old = dated.filter((b) => !b.temp && b.dir !== keep).sort((a, b) => b.at - a.at).slice(KEPT_BUILDS - 1);
|
|
133
|
+
for (const b of old)
|
|
134
|
+
yield* fs.remove(b.dir, { recursive: true, force: true });
|
|
135
|
+
});
|
|
136
|
+
/** A film project built in memory: compiled, its bundles run, its assets imported (their lengths
|
|
137
|
+
* go into film.json), film.json extracted. */
|
|
138
|
+
const buildInMemory = Effect.fn("buildInMemory")(function* (project) {
|
|
139
|
+
const files = yield* readProjectFiles(project.root);
|
|
140
|
+
const compiled = yield* compileProject(files);
|
|
141
|
+
const loaded = yield* loadProject(compiled, fileModuleHost(join(project.workDir, "modules")));
|
|
142
|
+
const imported = yield* importAssets(project, loaded.assets);
|
|
143
|
+
const media = { duration: (asset) => imported.durations.get(`${asset.kind}:${asset.name}`) ?? null };
|
|
144
|
+
const doc = yield* extractFilm(loaded, { media });
|
|
145
|
+
const build = writeBuild(compiled, doc, { tongue: yield* sdkVersion, assets: yield* importedFiles(project, loaded.assets) });
|
|
146
|
+
yield* linkFilmTypes(project.root);
|
|
147
|
+
return { build, imported, scenes: new Map(loaded.scenes.map((s) => [s.id, s.scene])), media };
|
|
148
|
+
});
|
|
149
|
+
/** Builds a film project into `dir` (what `tongue build` does: dist/ by default), replacing what
|
|
150
|
+
* was there, with copies of its imported assets: a folder any static host can serve. */
|
|
151
|
+
export const buildFilmProject = Effect.fn("buildFilmProject")(function* (project, dir) {
|
|
152
|
+
const built = yield* buildInMemory(project);
|
|
153
|
+
yield* writeBuildFiles(dir, built.build, { kind: "copy", from: project.assetsDir });
|
|
154
|
+
const out = { ...built, dir };
|
|
155
|
+
return out;
|
|
156
|
+
});
|
|
157
|
+
/** Builds a film project for Tongue's own use (stills, renders, dev): kept in .tongue/builds. */
|
|
158
|
+
export const buildForWork = Effect.fn("buildForWork")(function* (project) {
|
|
159
|
+
const built = yield* buildInMemory(project);
|
|
160
|
+
const out = { ...built, dir: yield* keepBuild(project, built.build) };
|
|
161
|
+
return out;
|
|
162
|
+
});
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PlanSummary } from "@tongue-core/film";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
export interface CheckProblem {
|
|
4
|
+
/** layout, compile, load, timeline, doc (film.json), asset. */
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
readonly file: string | null;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CheckReport {
|
|
10
|
+
readonly ok: boolean;
|
|
11
|
+
/** What stops the film from rendering, with what to change. */
|
|
12
|
+
readonly problems: ReadonlyArray<CheckProblem>;
|
|
13
|
+
/** What will render, but probably not as intended. */
|
|
14
|
+
readonly warnings: ReadonlyArray<string>;
|
|
15
|
+
readonly summary: PlanSummary | null;
|
|
16
|
+
readonly text: string | null;
|
|
17
|
+
}
|
|
18
|
+
export declare const checkFilm: (path: string) => Effect.Effect<CheckReport, import("./errors.js").InvalidRequest | import("./errors.js").ToolFailure | import("effect/Config").ConfigError | import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { describeFilm, openFilm } from "./project.js";
|
|
3
|
+
const failed = (problems) => ({ ok: false, problems, warnings: [], summary: null, text: null });
|
|
4
|
+
export const checkFilm = Effect.fn("checkFilm")(function* (path) {
|
|
5
|
+
return yield* openFilm(path).pipe(Effect.map((film) => {
|
|
6
|
+
const { text, summary } = describeFilm(film);
|
|
7
|
+
return { ok: true, problems: [], warnings: film.plan.warnings, summary, text };
|
|
8
|
+
}), Effect.catchTags({
|
|
9
|
+
FilmFailure: (e) => Effect.succeed(failed([{ kind: e.phase === "load" ? "load" : "timeline", file: e.film, message: e.message }])),
|
|
10
|
+
AssetFailure: (e) => Effect.succeed(failed([{ kind: "asset", file: null, message: e.message }])),
|
|
11
|
+
LayoutError: (e) => Effect.succeed(failed(e.problems.map((p) => ({ kind: "layout", file: p.file, message: p.message })))),
|
|
12
|
+
CompileError: (e) => Effect.succeed(failed([{ kind: "compile", file: e.line === null ? e.file : `${e.file}:${e.line}`, message: e.message }])),
|
|
13
|
+
LoadError: (e) => Effect.succeed(failed([{ kind: "load", file: e.file, message: e.message }])),
|
|
14
|
+
TimelineError: (e) => Effect.succeed(failed([{ kind: "timeline", file: null, message: e.message }])),
|
|
15
|
+
DocError: (e) => Effect.succeed(failed([{ kind: "doc", file: "film.json", message: e.message }])),
|
|
16
|
+
}));
|
|
17
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type InitOptions } from "./init.js";
|
|
2
|
+
import { type OpenOptions } from "./project.js";
|
|
3
|
+
import { type VideoOptions } from "./render/pipeline.js";
|
|
4
|
+
import { type StillsOptions } from "./render/stills.js";
|
|
5
|
+
export declare function createTongue(): {
|
|
6
|
+
/** Makes a film project in a new or empty folder from a template. */
|
|
7
|
+
init: (dir: string, options?: InitOptions) => Promise<import("./init.js").InitReport>;
|
|
8
|
+
/** The templates that come with Tongue. */
|
|
9
|
+
templates: () => Promise<readonly {
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly fps: number;
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly description: string;
|
|
16
|
+
readonly tags?: readonly string[] | undefined;
|
|
17
|
+
}[]>;
|
|
18
|
+
/** Builds a film project: build.json, film.json and a bundle per scene, in its dist folder. */
|
|
19
|
+
build: (project: string, options?: {
|
|
20
|
+
readonly out?: string;
|
|
21
|
+
}) => Promise<import("./index.js").BuiltProject>;
|
|
22
|
+
/** Imports the film's assets and plans it; resolves to its timeline as text and JSON. */
|
|
23
|
+
info: (film: string) => Promise<{
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly summary: import("@tongue-core/film").PlanSummary;
|
|
26
|
+
}>;
|
|
27
|
+
/** Problems and warnings as data: never rejects for a broken film. */
|
|
28
|
+
check: (film: string) => Promise<import("./check.js").CheckReport>;
|
|
29
|
+
/** Frames as PNG files. */
|
|
30
|
+
stills: (film: string, options: StillsOptions & OpenOptions) => Promise<import("./index.js").Still[]>;
|
|
31
|
+
/** Frames tiled into one labelled image. */
|
|
32
|
+
sheet: (film: string, options: StillsOptions & OpenOptions & {
|
|
33
|
+
readonly columns?: number;
|
|
34
|
+
}) => Promise<{
|
|
35
|
+
stills: import("./index.js").Still[];
|
|
36
|
+
sheet: string;
|
|
37
|
+
}>;
|
|
38
|
+
/** The film (or a range of it, or one scene of a project) as an MP4. */
|
|
39
|
+
render: (film: string, options: VideoOptions & OpenOptions) => Promise<import("./index.js").VideoResult>;
|
|
40
|
+
/** The rasters and encoders this machine can use. */
|
|
41
|
+
backends: () => Promise<import("./backends.js").BackendsReport>;
|
|
42
|
+
};
|
|
43
|
+
export type Tongue = ReturnType<typeof createTongue>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BunServices } from "@effect/platform-bun";
|
|
2
|
+
import { Cause, Effect, Exit } from "effect";
|
|
3
|
+
import { listBackends } from "./backends.js";
|
|
4
|
+
import { checkFilm } from "./check.js";
|
|
5
|
+
import { initProject, listTemplates } from "./init.js";
|
|
6
|
+
import { buildFilm, describeFilm, openFilm } from "./project.js";
|
|
7
|
+
import { renderVideo } from "./render/pipeline.js";
|
|
8
|
+
import { renderSheet, renderStills } from "./render/stills.js";
|
|
9
|
+
// The SDK as plain Promises, for applications that don't use Effect. Each call runs the same Effect
|
|
10
|
+
// program the CLI runs; a failure rejects with the SDK's tagged error (FilmFailure, ToolFailure…).
|
|
11
|
+
const run = async (effect) => {
|
|
12
|
+
const exit = await Effect.runPromiseExit(effect.pipe(Effect.provide(BunServices.layer)));
|
|
13
|
+
if (Exit.isSuccess(exit))
|
|
14
|
+
return exit.value;
|
|
15
|
+
throw Cause.squash(exit.cause);
|
|
16
|
+
};
|
|
17
|
+
export function createTongue() {
|
|
18
|
+
return {
|
|
19
|
+
/** Makes a film project in a new or empty folder from a template. */
|
|
20
|
+
init: (dir, options) => run(initProject(dir, options)),
|
|
21
|
+
/** The templates that come with Tongue. */
|
|
22
|
+
templates: () => run(listTemplates()),
|
|
23
|
+
/** Builds a film project: build.json, film.json and a bundle per scene, in its dist folder. */
|
|
24
|
+
build: (project, options) => run(buildFilm(project, options)),
|
|
25
|
+
/** Imports the film's assets and plans it; resolves to its timeline as text and JSON. */
|
|
26
|
+
info: (film) => run(openFilm(film).pipe(Effect.map(describeFilm))),
|
|
27
|
+
/** Problems and warnings as data: never rejects for a broken film. */
|
|
28
|
+
check: (film) => run(checkFilm(film)),
|
|
29
|
+
/** Frames as PNG files. */
|
|
30
|
+
stills: (film, options) => run(openFilm(film, options).pipe(Effect.flatMap((f) => renderStills(f, options)))),
|
|
31
|
+
/** Frames tiled into one labelled image. */
|
|
32
|
+
sheet: (film, options) => run(openFilm(film, options).pipe(Effect.flatMap((f) => renderSheet(f, options)))),
|
|
33
|
+
/** The film (or a range of it, or one scene of a project) as an MP4. */
|
|
34
|
+
render: (film, options) => run(openFilm(film, options).pipe(Effect.flatMap((f) => renderVideo(f, options)))),
|
|
35
|
+
/** The rasters and encoders this machine can use. */
|
|
36
|
+
backends: () => run(listBackends()),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type VideoCodec = "h264" | "hevc";
|
|
2
|
+
export interface EncodeTarget {
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly height: number;
|
|
5
|
+
readonly fps: number;
|
|
6
|
+
/** The video-only file to write; audio is muxed in afterwards. */
|
|
7
|
+
readonly out: string;
|
|
8
|
+
}
|
|
9
|
+
/** An open encode. A frame's buffer may be reused once the next `write` has resolved. */
|
|
10
|
+
export interface Encoder {
|
|
11
|
+
write(frame: Uint8Array): Promise<void>;
|
|
12
|
+
finish(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface EncoderSpec {
|
|
15
|
+
/** With its settings: "x264 veryfast crf 14", "vaapi-hevc qp 18". */
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly codec: VideoCodec;
|
|
18
|
+
readonly hardware: boolean;
|
|
19
|
+
/** False for settings written from vendor documentation and never run on a machine of ours. */
|
|
20
|
+
readonly tested: boolean;
|
|
21
|
+
/** Options before the raw input (hardware devices). */
|
|
22
|
+
readonly device: ReadonlyArray<string>;
|
|
23
|
+
/** The filter from the raw input to what the encoder takes (uploads to the GPU). */
|
|
24
|
+
readonly filter: string | null;
|
|
25
|
+
readonly encode: ReadonlyArray<string>;
|
|
26
|
+
/** AMD's HEVC encoder pads the picture and writes no conformance window, or a wrong one, so 1080
|
|
27
|
+
* rows decode as 1088 and 540 columns as 572: the window is set again when muxing. */
|
|
28
|
+
readonly fixCrop: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Where ffmpeg and ffprobe are. */
|
|
31
|
+
export interface Tools {
|
|
32
|
+
readonly ffmpeg: string;
|
|
33
|
+
readonly ffprobe: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class EncoderError extends Error {
|
|
36
|
+
readonly name = "EncoderError";
|
|
37
|
+
}
|
|
38
|
+
/** Opens an encode: frames go to ffmpeg's stdin, one in flight while the next is drawn. */
|
|
39
|
+
export declare function openEncoder(tools: Tools, spec: EncoderSpec, target: EncodeTarget): Encoder;
|
|
40
|
+
/** Whether this machine can run an encoder: a 0.2 s encode of a test pattern through its arguments,
|
|
41
|
+
* at the size it will encode (hardware encoders have a smallest size; VAAPI's is about 130×128). */
|
|
42
|
+
export declare function encoderWorks(ffmpeg: string, spec: EncoderSpec, size?: {
|
|
43
|
+
readonly width: number;
|
|
44
|
+
readonly height: number;
|
|
45
|
+
}): Promise<boolean>;
|
|
46
|
+
export interface EncoderSettings {
|
|
47
|
+
/** x264: preset and constant rate factor. */
|
|
48
|
+
readonly preset: string;
|
|
49
|
+
readonly crf: number;
|
|
50
|
+
/** x264: threads per encoder (two keeps x264 efficient when many encoders share the machine). */
|
|
51
|
+
readonly threads: number;
|
|
52
|
+
/** Hardware: constant QP (lower is better and bigger); null takes each codec's default. */
|
|
53
|
+
readonly qp: number | null;
|
|
54
|
+
/** Frames between keyframes (default: two seconds). */
|
|
55
|
+
readonly gop: number | null;
|
|
56
|
+
/** x264: extra settings, `key=value:key=value`. */
|
|
57
|
+
readonly x264Params: string;
|
|
58
|
+
}
|
|
59
|
+
export declare const DEFAULT_SETTINGS: EncoderSettings;
|
|
60
|
+
export declare const X264_PRESETS: ReadonlyArray<string>;
|
|
61
|
+
export type EncoderName = "x264" | "vaapi-hevc" | "vaapi-h264" | "vaapi-hevc-cpuconv" | "vaapi-h264-cpuconv" | "nvenc-hevc" | "nvenc-h264" | "qsv-hevc" | "qsv-h264";
|
|
62
|
+
export declare const ENCODER_NAMES: ReadonlyArray<EncoderName>;
|
|
63
|
+
export declare const isEncoderName: (v: string) => v is EncoderName;
|
|
64
|
+
/** What "auto" tries, in order: a GPU's encoder when one works, else x264. */
|
|
65
|
+
export declare const AUTO_ENCODERS: ReadonlyArray<EncoderName>;
|
|
66
|
+
export declare const ENCODER_SUMMARIES: Readonly<Record<EncoderName, string>>;
|
|
67
|
+
/** The encoder `name` with `settings`. Throws EncoderError for settings that can't work. */
|
|
68
|
+
export declare function encoderSpec(name: EncoderName, settings: EncoderSettings, fps: number): EncoderSpec;
|