@effect-motion/export 0.2.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/README.md +35 -0
- package/dist/Ffmpeg.d.ts +45 -0
- package/dist/Ffmpeg.js +90 -0
- package/dist/Fonts.d.ts +12 -0
- package/dist/Fonts.js +12 -0
- package/dist/Resvg.d.ts +27 -0
- package/dist/Resvg.js +22 -0
- package/dist/Video.d.ts +54 -0
- package/dist/Video.js +53 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @effect-motion/export
|
|
2
|
+
|
|
3
|
+
Node-only export tools for [effect-motion](https://www.npmjs.com/package/effect-motion): rasterize SVG frames to PNG (via [resvg](https://github.com/RazrFalcon/resvg)) and encode them into a video file (via ffmpeg).
|
|
4
|
+
|
|
5
|
+
This package is for **server-side rendering** (Node). Browser playback lives in `@effect-motion/react` and does not depend on this package.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { NodeServices } from "@effect/platform-node";
|
|
11
|
+
import { Effect } from "effect";
|
|
12
|
+
import { Video } from "@effect-motion/export";
|
|
13
|
+
|
|
14
|
+
// scene → MP4, one call
|
|
15
|
+
await Effect.runPromise(
|
|
16
|
+
Video.render(scene, "out.mp4", {
|
|
17
|
+
settings: { width: 1920, height: 1080, frameRate: 60 },
|
|
18
|
+
}).pipe(Effect.provide(NodeServices.layer)),
|
|
19
|
+
);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`Ffmpeg.encode` and `Resvg.rasterize` are the lower-level stages if you want to drive the pipeline yourself.
|
|
23
|
+
|
|
24
|
+
## Bundled ffmpeg
|
|
25
|
+
|
|
26
|
+
Video encoding uses the [`ffmpeg-static`](https://www.npmjs.com/package/ffmpeg-static) binary by default — a full build that includes **libx264**, so H.264 output works out of the box with no system ffmpeg required. Installing this package downloads that binary (~45 MB) for your platform.
|
|
27
|
+
|
|
28
|
+
To use a system or custom ffmpeg instead, pass `binary`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
Video.render(scene, "out.mp4", { binary: "ffmpeg" }); // system ffmpeg on PATH
|
|
32
|
+
Video.render(scene, "out.mp4", { binary: "/path/to/ffmpeg" }); // a specific build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> **License note.** The `ffmpeg-static` build (and libx264) is **GPL-3.0**. It is a standalone executable this package invokes over a process boundary — it is not linked into effect-motion's own code, which stays MIT. If you redistribute the bundled binary, the ffmpeg/libx264 GPL terms apply to that binary; pass your own `binary` to avoid shipping it.
|
package/dist/Ffmpeg.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Stream from "effect/Stream";
|
|
3
|
+
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
4
|
+
type Spawner = ChildProcessSpawner.ChildProcessSpawner;
|
|
5
|
+
/** Options for {@link encode}. */
|
|
6
|
+
export interface EncodeOptions {
|
|
7
|
+
/** Input framerate handed to ffmpeg (`-framerate`). */
|
|
8
|
+
readonly frameRate: number;
|
|
9
|
+
/**
|
|
10
|
+
* ffmpeg binary; defaults to the bundled `ffmpeg-static` build (falling
|
|
11
|
+
* back to `"ffmpeg"` on PATH). Set to `"ffmpeg"` or a path to use a
|
|
12
|
+
* system/custom ffmpeg instead.
|
|
13
|
+
*/
|
|
14
|
+
readonly binary?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Extra ffmpeg arguments appended before the output path — e.g.
|
|
17
|
+
* `["-crf", "18"]` or a `-vf` scale filter. Escape hatch for the codec
|
|
18
|
+
* surface this wrapper does not model.
|
|
19
|
+
*/
|
|
20
|
+
readonly extraArgs?: ReadonlyArray<string> | undefined;
|
|
21
|
+
}
|
|
22
|
+
declare const EncodeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
23
|
+
readonly _tag: "EncodeError";
|
|
24
|
+
} & Readonly<A>;
|
|
25
|
+
/**
|
|
26
|
+
* A failure encoding a video: ffmpeg not spawnable (not installed / not on
|
|
27
|
+
* PATH), or a nonzero exit. `stderr` carries ffmpeg's diagnostics when the
|
|
28
|
+
* process ran; it is empty when the binary could not be spawned at all.
|
|
29
|
+
*/
|
|
30
|
+
export declare class EncodeError extends EncodeError_base<{
|
|
31
|
+
readonly message: string;
|
|
32
|
+
readonly stderr: string;
|
|
33
|
+
readonly cause: unknown;
|
|
34
|
+
}> {
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Encode a stream of PNG frames into a video file at `outPath`.
|
|
38
|
+
*
|
|
39
|
+
* Pipes the PNG bytes into ffmpeg's stdin via the `image2pipe` demuxer;
|
|
40
|
+
* when the stream ends, stdin closes and ffmpeg finalizes the file. A
|
|
41
|
+
* nonzero exit or an unspawnable binary fails with a tagged
|
|
42
|
+
* {@link EncodeError}.
|
|
43
|
+
*/
|
|
44
|
+
export declare const encode: (pngStream: Stream.Stream<Uint8Array>, outPath: string, options: EncodeOptions) => Effect.Effect<void, EncodeError, Spawner>;
|
|
45
|
+
export {};
|
package/dist/Ffmpeg.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as Data from "effect/Data";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Fiber from "effect/Fiber";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
6
|
+
import ffmpegStatic from "ffmpeg-static";
|
|
7
|
+
/**
|
|
8
|
+
* ffmpeg encoding is an export tool, not a renderer: it consumes the PNG
|
|
9
|
+
* frames the rasterizer produces and pipes them into ffmpeg over stdin
|
|
10
|
+
* (`-f image2pipe`), yielding a video file. No frame touches disk.
|
|
11
|
+
*
|
|
12
|
+
* By default the bundled `ffmpeg-static` binary is used (a full build with
|
|
13
|
+
* libx264, so H.264 works out of the box); `binary` overrides it to point
|
|
14
|
+
* at a system or custom ffmpeg. If the bundle is unavailable for the host
|
|
15
|
+
* platform, it falls back to `"ffmpeg"` on PATH. Process spawning goes
|
|
16
|
+
* through the `ChildProcessSpawner` service — the consumer provides it
|
|
17
|
+
* (e.g. `NodeServices.layer` from `@effect/platform-node`), the same way
|
|
18
|
+
* `Resvg.rasterizeToFile` takes a consumer-provided `FileSystem`.
|
|
19
|
+
*/
|
|
20
|
+
// the bundled ffmpeg (null only on a platform ffmpeg-static doesn't ship)
|
|
21
|
+
const bundledFfmpeg = ffmpegStatic;
|
|
22
|
+
/**
|
|
23
|
+
* A failure encoding a video: ffmpeg not spawnable (not installed / not on
|
|
24
|
+
* PATH), or a nonzero exit. `stderr` carries ffmpeg's diagnostics when the
|
|
25
|
+
* process ran; it is empty when the binary could not be spawned at all.
|
|
26
|
+
*/
|
|
27
|
+
export class EncodeError extends Data.TaggedError("EncodeError") {
|
|
28
|
+
}
|
|
29
|
+
// broadly-playable defaults: H.264 8-bit, faststart MP4 for progressive
|
|
30
|
+
// playback. Flags chosen to be stable across every maintained ffmpeg (4.x+).
|
|
31
|
+
const outputArgs = [
|
|
32
|
+
"-c:v",
|
|
33
|
+
"libx264",
|
|
34
|
+
"-pix_fmt",
|
|
35
|
+
"yuv420p",
|
|
36
|
+
"-movflags",
|
|
37
|
+
"+faststart",
|
|
38
|
+
];
|
|
39
|
+
const decoder = new TextDecoder();
|
|
40
|
+
/**
|
|
41
|
+
* Encode a stream of PNG frames into a video file at `outPath`.
|
|
42
|
+
*
|
|
43
|
+
* Pipes the PNG bytes into ffmpeg's stdin via the `image2pipe` demuxer;
|
|
44
|
+
* when the stream ends, stdin closes and ffmpeg finalizes the file. A
|
|
45
|
+
* nonzero exit or an unspawnable binary fails with a tagged
|
|
46
|
+
* {@link EncodeError}.
|
|
47
|
+
*/
|
|
48
|
+
export const encode = (pngStream, outPath, options) => Effect.gen(function* () {
|
|
49
|
+
const binary = options.binary ?? bundledFfmpeg ?? "ffmpeg";
|
|
50
|
+
const args = [
|
|
51
|
+
"-f",
|
|
52
|
+
"image2pipe",
|
|
53
|
+
"-framerate",
|
|
54
|
+
String(options.frameRate),
|
|
55
|
+
"-i",
|
|
56
|
+
"-",
|
|
57
|
+
...outputArgs,
|
|
58
|
+
...(options.extraArgs ?? []),
|
|
59
|
+
"-y",
|
|
60
|
+
outPath,
|
|
61
|
+
];
|
|
62
|
+
const command = ChildProcess.make(binary, args, {
|
|
63
|
+
// PlatformError from the stdin pipe would otherwise leak into the
|
|
64
|
+
// stream's error channel; the stream elements are pure PNG bytes,
|
|
65
|
+
// so this cast only re-labels the (unreachable) error type
|
|
66
|
+
stdin: pngStream,
|
|
67
|
+
stdout: "ignore",
|
|
68
|
+
stderr: "pipe",
|
|
69
|
+
});
|
|
70
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
71
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
72
|
+
const handle = yield* spawner.spawn(command);
|
|
73
|
+
// collect stderr concurrently: an unconsumed stderr pipe can
|
|
74
|
+
// fill and deadlock ffmpeg, and it is the error diagnostic
|
|
75
|
+
const stderrFiber = yield* Effect.forkChild(Stream.runFold(handle.stderr, () => "", (acc, chunk) => acc + decoder.decode(chunk)));
|
|
76
|
+
const code = yield* handle.exitCode;
|
|
77
|
+
const stderr = yield* Fiber.join(stderrFiber);
|
|
78
|
+
if (code !== 0) {
|
|
79
|
+
return yield* new EncodeError({
|
|
80
|
+
message: `ffmpeg exited with code ${code}`,
|
|
81
|
+
stderr,
|
|
82
|
+
cause: code,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
})).pipe(Effect.catchTag("PlatformError", (cause) => Effect.fail(new EncodeError({
|
|
86
|
+
message: `Could not run "${binary}". Install ffmpeg and ensure it is on your PATH (or pass options.binary).`,
|
|
87
|
+
stderr: "",
|
|
88
|
+
cause,
|
|
89
|
+
}))));
|
|
90
|
+
});
|
package/dist/Fonts.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResvgRenderOptions } from "@resvg/resvg-js";
|
|
2
|
+
import type * as Context from "effect/Context";
|
|
3
|
+
/**
|
|
4
|
+
* Map a scene's declared fonts (the `Fonts` annotation) to resvg font
|
|
5
|
+
* options: every entry with a `src.path` becomes a `fontFiles` entry.
|
|
6
|
+
* Url-only entries are a browser concern and are skipped. System fonts
|
|
7
|
+
* stay loaded (resvg's default) — declared fonts ADD faces; pass
|
|
8
|
+
* `loadSystemFonts: false` yourself for fully self-contained output.
|
|
9
|
+
*/
|
|
10
|
+
export declare const resvgOptions: (scene: {
|
|
11
|
+
readonly annotations: Context.Context<never>;
|
|
12
|
+
}) => ResvgRenderOptions;
|
package/dist/Fonts.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Fonts } from "effect-motion";
|
|
2
|
+
/**
|
|
3
|
+
* Map a scene's declared fonts (the `Fonts` annotation) to resvg font
|
|
4
|
+
* options: every entry with a `src.path` becomes a `fontFiles` entry.
|
|
5
|
+
* Url-only entries are a browser concern and are skipped. System fonts
|
|
6
|
+
* stay loaded (resvg's default) — declared fonts ADD faces; pass
|
|
7
|
+
* `loadSystemFonts: false` yourself for fully self-contained output.
|
|
8
|
+
*/
|
|
9
|
+
export const resvgOptions = (scene) => {
|
|
10
|
+
const fontFiles = Fonts.get(scene).flatMap((font) => font.src.path === undefined ? [] : [font.src.path]);
|
|
11
|
+
return fontFiles.length === 0 ? {} : { font: { fontFiles } };
|
|
12
|
+
};
|
package/dist/Resvg.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ResvgRenderOptions } from "@resvg/resvg-js";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import type * as PlatformError from "effect/PlatformError";
|
|
5
|
+
/**
|
|
6
|
+
* Thin Effect wrappers over resvg. Rasterization is an export tool, not a
|
|
7
|
+
* renderer: it consumes the SVG document strings the string sink produces
|
|
8
|
+
* (or any other SVG) and yields PNG bytes.
|
|
9
|
+
*/
|
|
10
|
+
/** resvg's own options, untranslated — font config and fitTo live here */
|
|
11
|
+
export type { ResvgRenderOptions } from "@resvg/resvg-js";
|
|
12
|
+
declare const RasterizeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
13
|
+
readonly _tag: "RasterizeError";
|
|
14
|
+
} & Readonly<A>;
|
|
15
|
+
/** A failure raised by resvg while parsing or rendering an SVG document. */
|
|
16
|
+
export declare class RasterizeError extends RasterizeError_base<{
|
|
17
|
+
readonly cause: unknown;
|
|
18
|
+
}> {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Rasterize an SVG document string to PNG bytes. Output dimensions come
|
|
22
|
+
* from the document itself; the string sink stamps the frame's
|
|
23
|
+
* width/height on the root.
|
|
24
|
+
*/
|
|
25
|
+
export declare const rasterize: (svg: string, options?: ResvgRenderOptions) => Effect.Effect<Uint8Array, RasterizeError>;
|
|
26
|
+
/** `rasterize`, then persist through the FileSystem service. */
|
|
27
|
+
export declare const rasterizeToFile: (svg: string, path: string, options?: ResvgRenderOptions) => Effect.Effect<void, RasterizeError | PlatformError.PlatformError, FileSystem.FileSystem>;
|
package/dist/Resvg.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Resvg as ResvgJs } from "@resvg/resvg-js";
|
|
2
|
+
import * as Data from "effect/Data";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as FileSystem from "effect/FileSystem";
|
|
5
|
+
/** A failure raised by resvg while parsing or rendering an SVG document. */
|
|
6
|
+
export class RasterizeError extends Data.TaggedError("RasterizeError") {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Rasterize an SVG document string to PNG bytes. Output dimensions come
|
|
10
|
+
* from the document itself; the string sink stamps the frame's
|
|
11
|
+
* width/height on the root.
|
|
12
|
+
*/
|
|
13
|
+
export const rasterize = (svg, options) => Effect.try({
|
|
14
|
+
try: () => new Uint8Array(new ResvgJs(svg, options ?? null).render().asPng()),
|
|
15
|
+
catch: (cause) => new RasterizeError({ cause }),
|
|
16
|
+
});
|
|
17
|
+
/** `rasterize`, then persist through the FileSystem service. */
|
|
18
|
+
export const rasterizeToFile = (svg, path, options) => Effect.gen(function* () {
|
|
19
|
+
const bytes = yield* rasterize(svg, options);
|
|
20
|
+
const fs = yield* FileSystem.FileSystem;
|
|
21
|
+
yield* fs.writeFile(path, bytes);
|
|
22
|
+
});
|
package/dist/Video.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Scope from "effect/Scope";
|
|
3
|
+
import type { ChildProcessSpawner } from "effect/unstable/process";
|
|
4
|
+
import { type Entity, Scene } from "effect-motion";
|
|
5
|
+
import * as Ffmpeg from "./Ffmpeg";
|
|
6
|
+
import * as Resvg from "./Resvg";
|
|
7
|
+
type SceneRunner = typeof Scene.tick extends Effect.Effect<unknown, unknown, infer RR> ? RR : never;
|
|
8
|
+
type SceneInternalR = SceneRunner | Scope.Scope;
|
|
9
|
+
/**
|
|
10
|
+
* The one-call export path: a scene becomes a video file. Composes the whole
|
|
11
|
+
* pipeline — `Scene.stream` → SVG string sink → resvg → ffmpeg — reading the
|
|
12
|
+
* framerate and dimensions from the scene's own frame metadata and feeding
|
|
13
|
+
* declared fonts to the rasterizer.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Scene settings honored by the export path — a subset of the runner's
|
|
17
|
+
* settings, passed through to `Scene.stream`. Framerate and dimensions set
|
|
18
|
+
* here become the video's framerate and dimensions.
|
|
19
|
+
*/
|
|
20
|
+
export interface VideoSceneSettings {
|
|
21
|
+
readonly frameRate?: number;
|
|
22
|
+
readonly width?: number;
|
|
23
|
+
readonly height?: number;
|
|
24
|
+
/** Set to `Infinity` for an intentionally infinite scene (with `frames`). */
|
|
25
|
+
readonly maxFrames?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Options for {@link render}. */
|
|
28
|
+
export interface VideoOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Cap the number of frames encoded. WITHOUT it, a scene that never ends
|
|
31
|
+
* produces an encode that never ends — an infinite scene must set this.
|
|
32
|
+
*/
|
|
33
|
+
readonly frames?: number;
|
|
34
|
+
/**
|
|
35
|
+
* ffmpeg binary; defaults to the bundled `ffmpeg-static` build (falls
|
|
36
|
+
* back to `"ffmpeg"` on PATH). Pass `"ffmpeg"` or a path for a
|
|
37
|
+
* system/custom ffmpeg.
|
|
38
|
+
*/
|
|
39
|
+
readonly binary?: string;
|
|
40
|
+
/** Extra ffmpeg arguments, appended before the output path. */
|
|
41
|
+
readonly extraArgs?: ReadonlyArray<string>;
|
|
42
|
+
/** How many frames to rasterize concurrently (order preserved). Default 4. */
|
|
43
|
+
readonly concurrency?: number;
|
|
44
|
+
/** Scene framerate/dimensions/maxFrames for this export. */
|
|
45
|
+
readonly settings?: VideoSceneSettings;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render a scene to a video file at `outPath`.
|
|
49
|
+
*
|
|
50
|
+
* Fails with {@link Ffmpeg.EncodeError} on odd output dimensions (invalid for
|
|
51
|
+
* `yuv420p`) — before any ffmpeg process is spawned — or on an ffmpeg failure.
|
|
52
|
+
*/
|
|
53
|
+
export declare const render: <E, R, Entities extends Entity.AnyEntity>(scene: Scene.Scene<E, R, Entities>, outPath: string, options?: VideoOptions) => Effect.Effect<void, E | Ffmpeg.EncodeError | Resvg.RasterizeError, Exclude<R, Scope.Scope | SceneInternalR> | ChildProcessSpawner.ChildProcessSpawner>;
|
|
54
|
+
export {};
|
package/dist/Video.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Sink from "effect/Sink";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import { Scene, Svg } from "effect-motion";
|
|
6
|
+
import * as Ffmpeg from "./Ffmpeg";
|
|
7
|
+
import { resvgOptions } from "./Fonts";
|
|
8
|
+
import * as Resvg from "./Resvg";
|
|
9
|
+
// SVG string sink + built-in shape renderers, provided internally so callers
|
|
10
|
+
// don't wire renderers for the export path
|
|
11
|
+
const renderLayer = Svg.layer.pipe(Layer.provideMerge(Svg.shapesLayer));
|
|
12
|
+
/**
|
|
13
|
+
* Render a scene to a video file at `outPath`.
|
|
14
|
+
*
|
|
15
|
+
* Fails with {@link Ffmpeg.EncodeError} on odd output dimensions (invalid for
|
|
16
|
+
* `yuv420p`) — before any ffmpeg process is spawned — or on an ffmpeg failure.
|
|
17
|
+
*/
|
|
18
|
+
export const render = (scene, outPath, options = {}) => Effect.scoped(Effect.gen(function* () {
|
|
19
|
+
const renderer = yield* Svg.SvgRenderer.Context;
|
|
20
|
+
const fontOpts = resvgOptions(scene);
|
|
21
|
+
const concurrency = options.concurrency ?? 4;
|
|
22
|
+
let frames = Scene.stream(scene, options.settings ?? {});
|
|
23
|
+
if (options.frames !== undefined) {
|
|
24
|
+
frames = Stream.take(frames, options.frames);
|
|
25
|
+
}
|
|
26
|
+
// peel the first frame for metadata (framerate → ffmpeg -framerate,
|
|
27
|
+
// dimensions → yuv420p even-size check) before spawning ffmpeg
|
|
28
|
+
const [first, rest] = yield* Stream.peel(frames, Sink.head());
|
|
29
|
+
if (first._tag === "None") {
|
|
30
|
+
return; // empty scene: nothing to encode
|
|
31
|
+
}
|
|
32
|
+
const meta = first.value;
|
|
33
|
+
if (meta.width % 2 !== 0 || meta.height % 2 !== 0) {
|
|
34
|
+
const odd = meta.width % 2 !== 0
|
|
35
|
+
? `width ${meta.width}`
|
|
36
|
+
: `height ${meta.height}`;
|
|
37
|
+
return yield* new Ffmpeg.EncodeError({
|
|
38
|
+
message: `Video dimensions must be even for yuv420p, got ${odd}. ` +
|
|
39
|
+
`Use even scene dimensions, or pass extraArgs with a scale filter.`,
|
|
40
|
+
stderr: "",
|
|
41
|
+
cause: meta,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const allFrames = Stream.concat(Stream.make(meta), rest);
|
|
45
|
+
const pngStream = Stream.mapEffect(allFrames, (frame) => renderer
|
|
46
|
+
.render(frame, {})
|
|
47
|
+
.pipe(Effect.flatMap((svg) => Resvg.rasterize(svg, fontOpts))), { concurrency });
|
|
48
|
+
yield* Ffmpeg.encode(pngStream, outPath, {
|
|
49
|
+
frameRate: meta.frameRate,
|
|
50
|
+
binary: options.binary,
|
|
51
|
+
extraArgs: options.extraArgs,
|
|
52
|
+
});
|
|
53
|
+
})).pipe(Effect.provide(renderLayer));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * as Ffmpeg from "./Ffmpeg";
|
|
2
|
+
export { EncodeError, type EncodeOptions } from "./Ffmpeg";
|
|
3
|
+
export * as Fonts from "./Fonts";
|
|
4
|
+
export * as Resvg from "./Resvg";
|
|
5
|
+
export { RasterizeError, type ResvgRenderOptions } from "./Resvg";
|
|
6
|
+
export type { VideoOptions } from "./Video";
|
|
7
|
+
export * as Video from "./Video";
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effect-motion/export",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Node-only export tools for effect-motion: rasterize SVG frames to PNG via resvg",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/julia-script/effect-motion.git",
|
|
10
|
+
"directory": "packages/export"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/julia-script/effect-motion#readme",
|
|
13
|
+
"bugs": "https://github.com/julia-script/effect-motion/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"effect",
|
|
16
|
+
"motion",
|
|
17
|
+
"export",
|
|
18
|
+
"svg",
|
|
19
|
+
"png",
|
|
20
|
+
"ffmpeg",
|
|
21
|
+
"video"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
37
|
+
"ffmpeg-static": "^5.3.0",
|
|
38
|
+
"effect-motion": "^0.2.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"effect": ">=4.0.0-beta.94"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@effect/platform-node": "4.0.0-beta.94",
|
|
45
|
+
"@types/node": "^26.1.1",
|
|
46
|
+
"effect": "4.0.0-beta.94",
|
|
47
|
+
"typescript": "^7.0.2",
|
|
48
|
+
"vitest": "^4.1.10"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
|
+
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"check": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|