xbintsc 0.5.21 → 0.5.23
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/AGENTS.md +1 -1
- package/README.md +15 -5
- package/README.zh-CN.md +11 -4
- package/dist/src/cli/hints.js +11 -0
- package/dist/src/cli/hints.js.map +1 -1
- package/dist/src/cli/main.js +1 -1
- package/dist/src/driver/compiler.js +1 -0
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/catalog.js +2 -1
- package/dist/src/extensions/catalog.js.map +1 -1
- package/dist/src/extensions/media/index.d.ts +27 -0
- package/dist/src/extensions/media/index.js +60 -0
- package/dist/src/extensions/media/index.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +12 -0
- package/dist/src/extensions/registry.js +16 -0
- package/dist/src/extensions/registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tests/cli/hints.test.js +15 -0
- package/dist/tests/cli/hints.test.js.map +1 -1
- package/dist/tests/cli/process.test.js +12 -0
- package/dist/tests/cli/process.test.js.map +1 -1
- package/dist/tests/e2e/gui-canvas.test.js +159 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
- package/dist/tests/e2e/gui-example-canvas.test.d.ts +11 -0
- package/dist/tests/e2e/gui-example-canvas.test.js +72 -0
- package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +2 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-media-disabled.test.d.ts +9 -0
- package/dist/tests/e2e/gui-media-disabled.test.js +42 -0
- package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -0
- package/dist/tests/e2e/gui-media.test.d.ts +6 -4
- package/dist/tests/e2e/gui-media.test.js +15 -12
- package/dist/tests/e2e/gui-media.test.js.map +1 -1
- package/dist/tests/e2e/unicode-output.test.d.ts +16 -0
- package/dist/tests/e2e/unicode-output.test.js +65 -0
- package/dist/tests/e2e/unicode-output.test.js.map +1 -0
- package/dist/tests/extensions/media.test.d.ts +1 -0
- package/dist/tests/extensions/media.test.js +58 -0
- package/dist/tests/extensions/media.test.js.map +1 -0
- package/dist/tests/extensions/registry.test.js +8 -0
- package/dist/tests/extensions/registry.test.js.map +1 -1
- package/dist/tests/lexer/identifiers.test.js +12 -0
- package/dist/tests/lexer/identifiers.test.js.map +1 -1
- package/dist/tests/lexer/scanner.test.js +8 -0
- package/dist/tests/lexer/scanner.test.js.map +1 -1
- package/dist/tests/lexer/strings.test.js +30 -0
- package/dist/tests/lexer/strings.test.js.map +1 -1
- package/doc/ai/cli.md +1 -1
- package/doc/ai/extensions.md +35 -22
- package/doc/ai/troubleshooting.md +9 -0
- package/doc/ai/zh-CN/cli.md +1 -1
- package/doc/ai/zh-CN/extensions.md +22 -9
- package/doc/ai/zh-CN/troubleshooting.md +8 -0
- package/doc/gui/build.md +37 -21
- package/doc/gui/canvas.md +47 -21
- package/doc/gui/known-issues.md +7 -6
- package/doc/gui/media.md +26 -11
- package/doc/gui.md +50 -34
- package/doc/zh-CN/gui/build.md +28 -16
- package/doc/zh-CN/gui/canvas.md +36 -17
- package/doc/zh-CN/gui/known-issues.md +6 -5
- package/doc/zh-CN/gui/media.md +21 -10
- package/doc/zh-CN/gui.md +43 -34
- package/package.json +2 -1
- package/runtime/ext_gui/canvas.cpp +114 -199
- package/runtime/ext_gui/canvas.h +101 -2
- package/runtime/ext_gui/canvas_gradient.cpp +112 -0
- package/runtime/ext_gui/canvas_internal.h +6 -6
- package/runtime/ext_gui/canvas_raster.cpp +27 -5
- package/runtime/ext_gui/canvas_stroke.cpp +252 -0
- package/runtime/ext_gui/canvas_style.cpp +240 -0
- package/runtime/ext_gui/dom_api.cpp +1 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +38 -4
- package/runtime/ext_gui/dom_api_canvas_extra.cpp +232 -0
- package/runtime/ext_gui/dom_api_internal.h +31 -0
- package/runtime/ext_gui/dom_api_proto.cpp +24 -0
- package/runtime/ext_gui/gui_media.cpp +69 -2
- package/runtime/ext_gui/media.cpp +34 -488
- package/runtime/ext_gui/media.h +54 -19
- package/runtime/ext_gui/media_api.h +65 -0
- package/runtime/ext_media/media.cpp +507 -0
- package/runtime/ext_media/media_bridge.c +35 -0
- package/scripts/build-gui.ts +6 -72
- package/scripts/build-media.ts +245 -0
- package/src/cli/hints.ts +11 -0
- package/src/cli/main.ts +1 -1
- package/src/driver/compiler.ts +1 -0
- package/src/driver/version.ts +1 -1
- package/src/extensions/catalog.ts +2 -1
- package/src/extensions/media/index.ts +67 -0
- package/src/extensions/registry.ts +24 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc media extension — registration bridge.
|
|
3
|
+
*
|
|
4
|
+
* The decoders live in `media.a`, a prebuilt C++ archive. A static archive is
|
|
5
|
+
* only searched for members that satisfy an undefined symbol, so a backend that
|
|
6
|
+
* merely had a constructor would never be extracted. This tiny C translation
|
|
7
|
+
* unit is compiled and linked *directly* (it is the media extension's
|
|
8
|
+
* `runtimeSources()` entry): its constructor references every backend entry
|
|
9
|
+
* point, which pulls the archive member in, and installs the table into the GUI
|
|
10
|
+
* engine before `main` runs.
|
|
11
|
+
*
|
|
12
|
+
* The bridge is intentionally C-only and has no FFmpeg (or SDL) dependency. If
|
|
13
|
+
* the `gui` extension is not enabled, `xt_media_install` is unresolved; the
|
|
14
|
+
* registry rejects that combination first with a "media requires gui"
|
|
15
|
+
* diagnostic (see `src/extensions/registry.ts`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
#include "../ext_gui/media_api.h"
|
|
19
|
+
|
|
20
|
+
static const XtMediaApi kMediaApi = {
|
|
21
|
+
xt_media_backend_open,
|
|
22
|
+
xt_media_backend_by_id,
|
|
23
|
+
xt_media_backend_shutdown,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
27
|
+
__attribute__((constructor)) static void xt_media_register(void) {
|
|
28
|
+
xt_media_install(&kMediaApi);
|
|
29
|
+
}
|
|
30
|
+
#elif defined(_MSC_VER)
|
|
31
|
+
/* COFF has no `constructor` attribute. `media.lib` is not produced on Windows
|
|
32
|
+
* yet; keep the table referenced so the translation unit is not empty. */
|
|
33
|
+
void xt_media_register(void);
|
|
34
|
+
void xt_media_register(void) { xt_media_install(&kMediaApi); }
|
|
35
|
+
#endif
|
package/scripts/build-gui.ts
CHANGED
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
* 1. fetch the pinned SDL3 source into `vendor/SDL` (gitignored),
|
|
14
14
|
* 2. build `libSDL3.a` with CMake,
|
|
15
15
|
* 2b. fetch + build static FreeType and HarfBuzz for text,
|
|
16
|
-
* 2c. fetch + build a minimal static FFmpeg for `<video>`/`<audio>`
|
|
17
|
-
* (skipped on Windows, where media support is compiled out),
|
|
18
16
|
* 3. compile the engine translation units,
|
|
19
17
|
* 4. merge everything into `gui.a` (libtool on macOS, `ar -M` elsewhere;
|
|
20
18
|
* `gui.lib` on Windows, built from MSVC-compatible COFF objects).
|
|
21
19
|
*
|
|
20
|
+
* Media decoding is *not* part of this archive: it lives in the separate
|
|
21
|
+
* `media` extension (`npm run media`, `runtime/ext_media/`), so a program that
|
|
22
|
+
* only renders HTML/CSS never links FFmpeg.
|
|
23
|
+
*
|
|
22
24
|
* Environment:
|
|
23
25
|
* xbintsc_CXX C++ compiler override (default: clang++ next to the resolved clang)
|
|
24
26
|
* xbintsc_CMAKE CMake override (default: a vendored CMake, then `cmake`)
|
|
@@ -35,7 +37,6 @@
|
|
|
35
37
|
*/
|
|
36
38
|
|
|
37
39
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
38
|
-
import { availableParallelism } from "node:os";
|
|
39
40
|
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
|
|
40
41
|
import { dirname, join, resolve } from "node:path";
|
|
41
42
|
import { fileURLToPath } from "node:url";
|
|
@@ -62,9 +63,6 @@ const freetypeSrc = join(vendorDir, "freetype");
|
|
|
62
63
|
const freetypeBuild = join(vendorDir, "freetype-build");
|
|
63
64
|
const harfbuzzSrc = join(vendorDir, "harfbuzz");
|
|
64
65
|
const harfbuzzBuild = join(vendorDir, "harfbuzz-build");
|
|
65
|
-
const ffmpegSrc = join(vendorDir, "ffmpeg");
|
|
66
|
-
const ffmpegBuild = join(vendorDir, "ffmpeg-build");
|
|
67
|
-
const ffmpegInstall = join(ffmpegBuild, "dist");
|
|
68
66
|
const objDir = join(root, "build", "gui-obj");
|
|
69
67
|
const outDir = join(runtimeDir, "lib", platformSlug());
|
|
70
68
|
|
|
@@ -110,24 +108,8 @@ function cmakeToolchainArgs(): string[] {
|
|
|
110
108
|
const SDL_TAG = process.env.SDL3_TAG || "release-3.2.10";
|
|
111
109
|
const FREETYPE_VERSION = process.env.FREETYPE_VERSION || "2.13.3";
|
|
112
110
|
const HARFBUZZ_VERSION = process.env.HARFBUZZ_VERSION || "10.1.0";
|
|
113
|
-
const FFMPEG_VERSION = process.env.FFMPEG_VERSION || "7.1";
|
|
114
111
|
const FREETYPE_URL = `https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.xz`;
|
|
115
112
|
const HARFBUZZ_URL = `https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz`;
|
|
116
|
-
const FFMPEG_URL = `https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz`;
|
|
117
|
-
|
|
118
|
-
/* A deliberately minimal FFmpeg: containers and decoders for the common web
|
|
119
|
-
* media formats, file access only, no network, no encoder, no filter graph.
|
|
120
|
-
* Disabling everything first keeps the static libraries small and the build
|
|
121
|
-
* short. `--disable-x86asm` drops the nasm/yasm build dependency (the C
|
|
122
|
-
* fallbacks are plenty for playback). This configuration is LGPL-only, so the
|
|
123
|
-
* resulting `gui.a` stays redistributable under the engine's terms. */
|
|
124
|
-
const FFMPEG_COMPONENTS: readonly string[] = [
|
|
125
|
-
"--enable-protocol=file",
|
|
126
|
-
"--enable-demuxer=mov,matroska,avi,flv,mpegts,mp3,ogg,wav,flac,aac,m4v,asf,mpeg,mpegvideo,h264,hevc,vc1",
|
|
127
|
-
"--enable-decoder=h264,hevc,vp8,vp9,av1,aac,aac_latm,mp3,mp3float,opus,vorbis,flac,alac,pcm_s16le,pcm_s24le,pcm_s32le,pcm_u8,pcm_f32le,pcm_alaw,pcm_mulaw,ac3,eac3,wmav2,mjpeg",
|
|
128
|
-
"--enable-parser=h264,hevc,vp8,vp9,av1,aac,aac_latm,mpegaudio,opus,vorbis,mpeg4video,mjpeg,flac",
|
|
129
|
-
"--enable-bsf=h264_mp4toannexb,hevc_mp4toannexb,aac_adtstoasc,vp9_superframe,vp9_superframe_split,extract_extradata",
|
|
130
|
-
];
|
|
131
113
|
|
|
132
114
|
function fail(message: string): never {
|
|
133
115
|
console.error(`xbintsc: ${message}`);
|
|
@@ -345,16 +327,6 @@ function harfbuzzArchive(): string | undefined {
|
|
|
345
327
|
return builtLibrary(harfbuzzBuild, ["libharfbuzz.a", "harfbuzz.lib"], /^harfbuzz[-\d.]*\.lib$/);
|
|
346
328
|
}
|
|
347
329
|
|
|
348
|
-
/* FFmpeg's release tarball has no CMake build, so it uses its own configure +
|
|
349
|
-
* make. It also installs its headers under a prefix (rather than in the source
|
|
350
|
-
* tree) because the *source* root contains a file literally named `version`
|
|
351
|
-
* that would shadow the C++ standard header for any `.cpp` that includes it. */
|
|
352
|
-
const FFMPEG_LIBRARY_NAMES = ["libavformat.a", "libavcodec.a", "libswscale.a", "libswresample.a", "libavutil.a"];
|
|
353
|
-
function ffmpegStaticLibraries(): string[] | undefined {
|
|
354
|
-
const libraries = FFMPEG_LIBRARY_NAMES.map((name) => join(ffmpegInstall, "lib", name));
|
|
355
|
-
return libraries.every((library) => existsSync(library)) ? libraries : undefined;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
330
|
/* -- 1. SDL3 source ------------------------------------------------------- */
|
|
359
331
|
|
|
360
332
|
if (!existsSync(join(sdlSrc, "CMakeLists.txt"))) {
|
|
@@ -407,8 +379,7 @@ if (!existingSdlArchive) {
|
|
|
407
379
|
/** Download a release tarball into `vendor/` and extract it under `name`.
|
|
408
380
|
* Release tarballs are used (not git clones) because they ship generated
|
|
409
381
|
* sources, so no ragel/autotools step is needed. `marker` is a file that only
|
|
410
|
-
* exists once the tarball has been extracted (
|
|
411
|
-
* `CMakeLists.txt`; FFmpeg ships `configure`). */
|
|
382
|
+
* exists once the tarball has been extracted (e.g. `CMakeLists.txt`). */
|
|
412
383
|
function ensureTarballSource(name: string, url: string, extractedName: string, marker = "CMakeLists.txt"): string {
|
|
413
384
|
const destination = join(vendorDir, name);
|
|
414
385
|
if (existsSync(join(destination, marker))) return destination;
|
|
@@ -463,41 +434,6 @@ if (!existsSync(harfbuzzArchive() ?? "")) {
|
|
|
463
434
|
console.log(`xbintsc: reusing existing ${harfbuzzArchive()}`);
|
|
464
435
|
}
|
|
465
436
|
|
|
466
|
-
/* -- 2c. FFmpeg (media decoding) ------------------------------------------ */
|
|
467
|
-
|
|
468
|
-
/* Windows builds have no `configure`/`make`, so media support is compiled out
|
|
469
|
-
* there; the element still works and reports an `error` event. */
|
|
470
|
-
let ffmpegLibraries: string[] | undefined;
|
|
471
|
-
if (isWindows) {
|
|
472
|
-
console.log("xbintsc: skipping FFmpeg on Windows; <video>/<audio> will report an error");
|
|
473
|
-
} else {
|
|
474
|
-
ensureTarballSource("ffmpeg", FFMPEG_URL, `ffmpeg-${FFMPEG_VERSION}`, "configure");
|
|
475
|
-
ffmpegLibraries = ffmpegStaticLibraries();
|
|
476
|
-
if (!ffmpegLibraries) {
|
|
477
|
-
console.log(`xbintsc: building minimal static FFmpeg ${FFMPEG_VERSION} with ${clang}`);
|
|
478
|
-
mkdirSync(ffmpegBuild, { recursive: true });
|
|
479
|
-
run(join(ffmpegSrc, "configure"), [
|
|
480
|
-
`--prefix=${ffmpegInstall}`,
|
|
481
|
-
"--enable-static", "--disable-shared",
|
|
482
|
-
"--disable-programs", "--disable-doc",
|
|
483
|
-
"--disable-htmlpages", "--disable-manpages", "--disable-podpages", "--disable-txtpages",
|
|
484
|
-
"--disable-avdevice", "--disable-avfilter", "--disable-postproc",
|
|
485
|
-
"--disable-everything",
|
|
486
|
-
"--enable-avformat", "--enable-avcodec", "--enable-avutil", "--enable-swscale", "--enable-swresample",
|
|
487
|
-
"--disable-network", "--disable-autodetect", "--disable-x86asm", "--disable-debug", "--enable-pic",
|
|
488
|
-
`--cc=${clang}`,
|
|
489
|
-
`--cxx=${clangxx}`,
|
|
490
|
-
...FFMPEG_COMPONENTS,
|
|
491
|
-
], { cwd: ffmpegBuild });
|
|
492
|
-
run("make", ["-j", String(availableParallelism())], { cwd: ffmpegBuild });
|
|
493
|
-
run("make", ["install"], { cwd: ffmpegBuild });
|
|
494
|
-
ffmpegLibraries = ffmpegStaticLibraries();
|
|
495
|
-
} else {
|
|
496
|
-
console.log(`xbintsc: reusing existing ${ffmpegInstall}/lib`);
|
|
497
|
-
}
|
|
498
|
-
if (!ffmpegLibraries) fail("FFmpeg build did not produce the expected static libraries");
|
|
499
|
-
}
|
|
500
|
-
|
|
501
437
|
/* -- 3. engine objects ---------------------------------------------------- */
|
|
502
438
|
|
|
503
439
|
mkdirSync(objDir, { recursive: true });
|
|
@@ -508,14 +444,12 @@ const includeFlags = [
|
|
|
508
444
|
"-I", join(freetypeBuild, "include"),
|
|
509
445
|
"-I", join(harfbuzzSrc, "src"),
|
|
510
446
|
];
|
|
511
|
-
if (ffmpegLibraries) includeFlags.push("-I", join(ffmpegInstall, "include"));
|
|
512
447
|
const sources = readdirSync(engineDir)
|
|
513
448
|
.filter((entry) => entry.endsWith(".cpp"))
|
|
514
449
|
.sort();
|
|
515
450
|
|
|
516
451
|
const objects: string[] = [];
|
|
517
452
|
const cxxFlags = ["-std=c++17", "-O2", "-Wall", "-Wextra", ...includeFlags];
|
|
518
|
-
if (ffmpegLibraries) cxxFlags.push("-DXT_GUI_WITH_FFMPEG=1");
|
|
519
453
|
/* The engine is C++ and the MSVC C++ runtime must match the static C CRT that
|
|
520
454
|
* clang links by default, so let every engine object record `libcpmt` as a
|
|
521
455
|
* default library. The extension itself only contributes the DLL import library
|
|
@@ -539,7 +473,7 @@ const harfbuzz = harfbuzzArchive();
|
|
|
539
473
|
if (!sdl || !freetype || !harfbuzz) {
|
|
540
474
|
fail("one of the SDL3/FreeType/HarfBuzz archives was not found after building");
|
|
541
475
|
}
|
|
542
|
-
const libraries = [sdl, freetype, harfbuzz
|
|
476
|
+
const libraries = [sdl, freetype, harfbuzz];
|
|
543
477
|
|
|
544
478
|
mkdirSync(outDir, { recursive: true });
|
|
545
479
|
rmSync(output, { force: true });
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build the media extension archive:
|
|
4
|
+
*
|
|
5
|
+
* runtime/lib/<os>-<arch>/media.a
|
|
6
|
+
*
|
|
7
|
+
* The GUI engine (`gui.a`) deliberately contains no decoder. This archive adds
|
|
8
|
+
* `<video>`/`<audio>` support: it bundles the FFmpeg-backed player from
|
|
9
|
+
* `runtime/ext_media/media.cpp` together with a minimal static FFmpeg, and the
|
|
10
|
+
* `media` extension links it only when the program asks for it. A program built
|
|
11
|
+
* with `--ext gui` alone therefore stays FFmpeg-free.
|
|
12
|
+
*
|
|
13
|
+
* Steps:
|
|
14
|
+
* 1. fetch the pinned FFmpeg release tarball into `vendor/ffmpeg`,
|
|
15
|
+
* 2. configure and build a minimal static FFmpeg into
|
|
16
|
+
* `vendor/ffmpeg-build/dist` (reused on later runs),
|
|
17
|
+
* 3. compile the player against those headers,
|
|
18
|
+
* 4. merge the player object and the FFmpeg libraries into `media.a`
|
|
19
|
+
* (libtool on macOS, `ar -M` elsewhere).
|
|
20
|
+
*
|
|
21
|
+
* Environment:
|
|
22
|
+
* xbintsc_CXX C++ compiler override (default: clang++ next to clang)
|
|
23
|
+
* xbintsc_AR archiver override (default: llvm-ar next to clang, then ar)
|
|
24
|
+
* FFMPEG_VERSION FFmpeg release (default: 7.1)
|
|
25
|
+
*
|
|
26
|
+
* Windows is not supported yet: FFmpeg needs its own `configure`/`make`, and an
|
|
27
|
+
* MSVC-compatible build has not been produced (see `doc/gui/known-issues.md`).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
31
|
+
import { availableParallelism } from "node:os";
|
|
32
|
+
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
|
|
33
|
+
import { dirname, join, resolve } from "node:path";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
import { platformSlug } from "../src/driver/paths.js";
|
|
36
|
+
import { probeClang, realRunner } from "../src/driver/toolchain.js";
|
|
37
|
+
import { resolveToolchain } from "../src/driver/toolchain-provider.js";
|
|
38
|
+
|
|
39
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
40
|
+
const runtimeDir = join(root, "runtime");
|
|
41
|
+
const sourceDir = join(runtimeDir, "ext_media");
|
|
42
|
+
const vendorDir = join(root, "vendor");
|
|
43
|
+
const ffmpegSrc = join(vendorDir, "ffmpeg");
|
|
44
|
+
const ffmpegBuild = join(vendorDir, "ffmpeg-build");
|
|
45
|
+
const ffmpegInstall = join(ffmpegBuild, "dist");
|
|
46
|
+
const objDir = join(root, "build", "media-obj");
|
|
47
|
+
const outDir = join(runtimeDir, "lib", platformSlug());
|
|
48
|
+
const output = join(outDir, "media.a");
|
|
49
|
+
|
|
50
|
+
const FFMPEG_VERSION = process.env.FFMPEG_VERSION || "7.1";
|
|
51
|
+
const FFMPEG_URL = `https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz`;
|
|
52
|
+
|
|
53
|
+
/* A deliberately minimal FFmpeg: containers and decoders for the common web
|
|
54
|
+
* media formats, file access only, no network, no encoder, no filter graph.
|
|
55
|
+
* Disabling everything first keeps the static libraries small and the build
|
|
56
|
+
* short. `--disable-x86asm` drops the nasm/yasm build dependency (the C
|
|
57
|
+
* fallbacks are plenty for playback). This configuration is LGPL-only, so the
|
|
58
|
+
* resulting archive stays redistributable. */
|
|
59
|
+
const FFMPEG_COMPONENTS: readonly string[] = [
|
|
60
|
+
"--enable-protocol=file",
|
|
61
|
+
"--enable-demuxer=mov,matroska,avi,flv,mpegts,mp3,ogg,wav,flac,aac,m4v,asf,mpeg,mpegvideo,h264,hevc,vc1",
|
|
62
|
+
"--enable-decoder=h264,hevc,vp8,vp9,av1,aac,aac_latm,mp3,mp3float,opus,vorbis,flac,alac,pcm_s16le,pcm_s24le,pcm_s32le,pcm_u8,pcm_f32le,pcm_alaw,pcm_mulaw,ac3,eac3,wmav2,mjpeg",
|
|
63
|
+
"--enable-parser=h264,hevc,vp8,vp9,av1,aac,aac_latm,mpegaudio,opus,vorbis,mpeg4video,mjpeg,flac",
|
|
64
|
+
"--enable-bsf=h264_mp4toannexb,hevc_mp4toannexb,aac_adtstoasc,vp9_superframe,vp9_superframe_split,extract_extradata",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const FFMPEG_LIBRARY_NAMES = ["libavformat.a", "libavcodec.a", "libswscale.a", "libswresample.a", "libavutil.a"];
|
|
68
|
+
|
|
69
|
+
function fail(message: string): never {
|
|
70
|
+
console.error(`xbintsc: ${message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function run(
|
|
75
|
+
command: string,
|
|
76
|
+
args: readonly string[],
|
|
77
|
+
options: { input?: string; quietStderr?: boolean; cwd?: string } = {},
|
|
78
|
+
): SpawnSyncReturns<string> {
|
|
79
|
+
const quiet = options.quietStderr === true;
|
|
80
|
+
const result = spawnSync(command, args, {
|
|
81
|
+
stdio: options.input !== undefined ? ["pipe", "inherit", "inherit"] : quiet ? ["ignore", "inherit", "pipe"] : "inherit",
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
input: options.input,
|
|
84
|
+
cwd: options.cwd,
|
|
85
|
+
});
|
|
86
|
+
if (result.status !== 0) {
|
|
87
|
+
if (quiet && result.stderr) process.stderr.write(result.stderr);
|
|
88
|
+
fail(`${command} ${args.join(" ")} failed with status ${result.status ?? "?"}`);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRunnable(candidate: string): boolean {
|
|
94
|
+
return !spawnSync(candidate, ["--version"], { encoding: "utf8" }).error;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function firstAvailable(candidates: readonly (string | undefined)[]): string | undefined {
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
if (candidate && isRunnable(candidate)) return candidate;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The tool that sits next to `clang` in the same LLVM installation. */
|
|
105
|
+
function siblingTool(clangPath: string, name: string): string | undefined {
|
|
106
|
+
if (!clangPath.includes("/") && !clangPath.includes("\\")) return undefined;
|
|
107
|
+
const candidate = join(dirname(clangPath), `${name}.exe`);
|
|
108
|
+
if (existsSync(candidate)) return candidate;
|
|
109
|
+
const bare = join(dirname(clangPath), name);
|
|
110
|
+
return existsSync(bare) ? bare : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Resolve `clang++` from the same LLVM as the driver's clang. */
|
|
114
|
+
function findClangxx(clangPath: string): string {
|
|
115
|
+
const override = process.env.xbintsc_CXX;
|
|
116
|
+
if (override) {
|
|
117
|
+
const probe = probeClang(realRunner, override);
|
|
118
|
+
if (!probe.responds) fail(`xbintsc_CXX ${override} does not respond to --version`);
|
|
119
|
+
return override;
|
|
120
|
+
}
|
|
121
|
+
const sibling = siblingTool(clangPath, "clang++");
|
|
122
|
+
if (sibling) return sibling;
|
|
123
|
+
const onPath = firstAvailable(["clang++"]);
|
|
124
|
+
if (onPath) return onPath;
|
|
125
|
+
fail("no clang++ found next to the resolved clang or on PATH; install LLVM or set xbintsc_CXX");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Resolve the archiver that matches the compiler, preferring LLVM's own. */
|
|
129
|
+
function findArchiver(clangPath: string): string {
|
|
130
|
+
const sibling = siblingTool(clangPath, "llvm-ar");
|
|
131
|
+
const archiver = firstAvailable([process.env.xbintsc_AR, sibling, "llvm-ar", "ar"]);
|
|
132
|
+
if (!archiver) fail("no archiver found; install llvm-ar or set xbintsc_AR");
|
|
133
|
+
return archiver;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function driverClang(): string {
|
|
137
|
+
try {
|
|
138
|
+
return resolveToolchain().clang;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Download the FFmpeg release tarball and extract it under `vendor/ffmpeg`. */
|
|
145
|
+
function ensureFfmpegSource(): void {
|
|
146
|
+
if (existsSync(join(ffmpegSrc, "configure"))) return;
|
|
147
|
+
const tarball = join(vendorDir, "ffmpeg.tar.xz");
|
|
148
|
+
console.log(`xbintsc: fetching FFmpeg ${FFMPEG_VERSION} from ${FFMPEG_URL}`);
|
|
149
|
+
mkdirSync(vendorDir, { recursive: true });
|
|
150
|
+
run("curl", ["-sL", "--fail", "-o", tarball, FFMPEG_URL]);
|
|
151
|
+
run("tar", ["-xf", tarball, "-C", vendorDir]);
|
|
152
|
+
const extracted = join(vendorDir, `ffmpeg-${FFMPEG_VERSION}`);
|
|
153
|
+
if (!existsSync(extracted)) fail(`expected ffmpeg-${FFMPEG_VERSION} after extracting`);
|
|
154
|
+
rmSync(ffmpegSrc, { recursive: true, force: true });
|
|
155
|
+
renameSync(extracted, ffmpegSrc);
|
|
156
|
+
rmSync(tarball, { force: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ffmpegStaticLibraries(): string[] | undefined {
|
|
160
|
+
const libraries = FFMPEG_LIBRARY_NAMES.map((name) => join(ffmpegInstall, "lib", name));
|
|
161
|
+
return libraries.every((library) => existsSync(library)) ? libraries : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (process.platform === "win32") {
|
|
165
|
+
fail(
|
|
166
|
+
"the media extension is not available on Windows yet: FFmpeg needs a POSIX " +
|
|
167
|
+
"configure/make build (see doc/gui/known-issues.md)",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* One toolchain for the player and FFmpeg: the same clang the driver uses, its
|
|
172
|
+
* sibling clang++, and the matching llvm-ar. Mixing another C++ runtime or
|
|
173
|
+
* object format would break the final link. */
|
|
174
|
+
const clang = driverClang();
|
|
175
|
+
const clangxx = findClangxx(clang);
|
|
176
|
+
const archiver = findArchiver(clang);
|
|
177
|
+
|
|
178
|
+
/* -- 1. FFmpeg source ------------------------------------------------------ */
|
|
179
|
+
|
|
180
|
+
ensureFfmpegSource();
|
|
181
|
+
|
|
182
|
+
/* -- 2. minimal static FFmpeg --------------------------------------------- */
|
|
183
|
+
|
|
184
|
+
let ffmpegLibraries = ffmpegStaticLibraries();
|
|
185
|
+
if (!ffmpegLibraries) {
|
|
186
|
+
console.log(`xbintsc: building minimal static FFmpeg ${FFMPEG_VERSION} with ${clang}`);
|
|
187
|
+
mkdirSync(ffmpegBuild, { recursive: true });
|
|
188
|
+
run(join(ffmpegSrc, "configure"), [
|
|
189
|
+
`--prefix=${ffmpegInstall}`,
|
|
190
|
+
"--enable-static", "--disable-shared",
|
|
191
|
+
"--disable-programs", "--disable-doc",
|
|
192
|
+
"--disable-htmlpages", "--disable-manpages", "--disable-podpages", "--disable-txtpages",
|
|
193
|
+
"--disable-avdevice", "--disable-avfilter", "--disable-postproc",
|
|
194
|
+
"--disable-everything",
|
|
195
|
+
"--enable-avformat", "--enable-avcodec", "--enable-avutil", "--enable-swscale", "--enable-swresample",
|
|
196
|
+
"--disable-network", "--disable-autodetect", "--disable-x86asm", "--disable-debug", "--enable-pic",
|
|
197
|
+
`--cc=${clang}`,
|
|
198
|
+
`--cxx=${clangxx}`,
|
|
199
|
+
...FFMPEG_COMPONENTS,
|
|
200
|
+
], { cwd: ffmpegBuild });
|
|
201
|
+
run("make", ["-j", String(availableParallelism())], { cwd: ffmpegBuild });
|
|
202
|
+
run("make", ["install"], { cwd: ffmpegBuild });
|
|
203
|
+
ffmpegLibraries = ffmpegStaticLibraries();
|
|
204
|
+
if (!ffmpegLibraries) fail("FFmpeg build did not produce the expected static libraries");
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`xbintsc: reusing existing ${ffmpegInstall}/lib`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* -- 3. player object ------------------------------------------------------ */
|
|
210
|
+
|
|
211
|
+
/* FFmpeg installs its headers under a prefix (rather than in the source tree)
|
|
212
|
+
* because the *source* root contains a file literally named `version` that
|
|
213
|
+
* would shadow the C++ standard header for any `.cpp` that includes it. */
|
|
214
|
+
mkdirSync(objDir, { recursive: true });
|
|
215
|
+
const object = join(objDir, "media.o");
|
|
216
|
+
const playerSource = join(sourceDir, "media.cpp");
|
|
217
|
+
console.log("xbintsc: compiling media.cpp");
|
|
218
|
+
run(clangxx, [
|
|
219
|
+
"-std=c++17", "-O2", "-Wall", "-Wextra", "-fPIC",
|
|
220
|
+
"-I", runtimeDir,
|
|
221
|
+
"-I", join(ffmpegInstall, "include"),
|
|
222
|
+
"-c", playerSource,
|
|
223
|
+
"-o", object,
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
/* -- 4. merge into media.a ------------------------------------------------- */
|
|
227
|
+
|
|
228
|
+
mkdirSync(outDir, { recursive: true });
|
|
229
|
+
rmSync(output, { force: true });
|
|
230
|
+
if (process.platform === "darwin") {
|
|
231
|
+
run("libtool", ["-static", "-o", output, object, ...ffmpegLibraries], { quietStderr: true });
|
|
232
|
+
} else {
|
|
233
|
+
/* `ar -M` (GNU/LLVM) merges archives through an MRI script. */
|
|
234
|
+
const mri = (path: string): string => path.replace(/\\/g, "/");
|
|
235
|
+
const script = [
|
|
236
|
+
`create ${mri(output)}`,
|
|
237
|
+
`addmod ${mri(object)}`,
|
|
238
|
+
...ffmpegLibraries.map((library) => `addlib ${mri(library)}`),
|
|
239
|
+
"save",
|
|
240
|
+
"end",
|
|
241
|
+
].join("\n");
|
|
242
|
+
run(archiver, ["-M"], { input: script });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(`xbintsc: wrote ${output}`);
|
package/src/cli/hints.ts
CHANGED
|
@@ -77,6 +77,17 @@ const THROWN_FAILURES: readonly ThrownFailure[] = [
|
|
|
77
77
|
path: DOC_EXTENSIONS,
|
|
78
78
|
},
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
/* A dependent extension (`media` needs `gui`) or a missing prebuilt archive
|
|
82
|
+
* (`gui.a`/`media.a`) both mean "the extension set is not ready", and the
|
|
83
|
+
* extensions guide explains both the flags and `npm run gui`/`media`. */
|
|
84
|
+
match: /requires the '.*' extension|native library not found/i,
|
|
85
|
+
code: DiagnosticCode.IOError,
|
|
86
|
+
pointer: {
|
|
87
|
+
text: "enable the extension's dependencies together, and build its prebuilt archive first",
|
|
88
|
+
path: DOC_EXTENSIONS,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
80
91
|
{
|
|
81
92
|
match: /config|JSON/i,
|
|
82
93
|
code: DiagnosticCode.IOError,
|
package/src/cli/main.ts
CHANGED
|
@@ -255,7 +255,7 @@ Options:
|
|
|
255
255
|
--out <dir> Output directory (default: build/)
|
|
256
256
|
--emit <kind> exe | obj | ir (default: exe)
|
|
257
257
|
-O0..-O3 Optimization level (default: -O2)
|
|
258
|
-
--ext <names> Comma separated extensions (e.g. node)
|
|
258
|
+
--ext <names> Comma separated extensions (e.g. node,gui,media)
|
|
259
259
|
--ext-native <m> Register a C++/Rust extension from a JSON manifest
|
|
260
260
|
(comma separated for several)
|
|
261
261
|
--config <path> Use a project config (default: auto-detect
|
package/src/driver/compiler.ts
CHANGED
|
@@ -69,6 +69,7 @@ function executableName(entry: string, outDir: string, explicit: string | undefi
|
|
|
69
69
|
export function build(entryPath: string, options: BuildOptions = {}): BuildResult {
|
|
70
70
|
const runner = options.runner ?? realRunner;
|
|
71
71
|
const registry = options.extensions ?? createDefaultRegistry();
|
|
72
|
+
registry.validate();
|
|
72
73
|
const outDir = resolve(options.outDir ?? join(process.cwd(), "build"));
|
|
73
74
|
const cacheDir = resolve(options.cacheDir ?? defaultCacheDir());
|
|
74
75
|
const emit: EmitKind = options.emit ?? "exe";
|
package/src/driver/version.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
import type { Extension } from "./registry.js";
|
|
14
14
|
import { nodeExtension } from "./node/index.js";
|
|
15
15
|
import { guiExtension } from "./gui/index.js";
|
|
16
|
+
import { mediaExtension } from "./media/index.js";
|
|
16
17
|
|
|
17
18
|
/** Every extension bundled with xbintsc, in registration order. */
|
|
18
19
|
export function bundledExtensions(): readonly Extension[] {
|
|
19
|
-
return [nodeExtension, guiExtension];
|
|
20
|
+
return [nodeExtension, guiExtension, mediaExtension];
|
|
20
21
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media extension: `<video>` / `<audio>` decoding for the GUI engine.
|
|
3
|
+
*
|
|
4
|
+
* The GUI extension is deliberately decoder-free: it models media elements,
|
|
5
|
+
* renders their frames and routes the DOM API, but keeps FFmpeg out of `gui.a`
|
|
6
|
+
* so a program that only needs windows and HTML/CSS does not pay for a media
|
|
7
|
+
* stack it never uses. This extension supplies the decoding half:
|
|
8
|
+
*
|
|
9
|
+
* - `media.a` (built by `npm run media`) contains the FFmpeg-linked player
|
|
10
|
+
* from `runtime/ext_media/media.cpp` and the static FFmpeg libraries;
|
|
11
|
+
* - `runtime/ext_media/media_bridge.c` is compiled and linked directly. Its
|
|
12
|
+
* constructor installs the player table into the GUI engine and references
|
|
13
|
+
* the backend entry points, which makes the linker extract the archive
|
|
14
|
+
* member — a static archive whose only entry point is a constructor would
|
|
15
|
+
* otherwise never be pulled in.
|
|
16
|
+
*
|
|
17
|
+
* There is no importable TypeScript module: the feature is the DOM media
|
|
18
|
+
* surface that the `gui` module already exposes. Enable both extensions:
|
|
19
|
+
*
|
|
20
|
+
* xbintsc run app.ts --ext gui,media
|
|
21
|
+
*
|
|
22
|
+
* The archive is POSIX-only for now (the build needs FFmpeg's `configure` and
|
|
23
|
+
* an MSVC-compatible FFmpeg has not been built), so Windows reports an
|
|
24
|
+
* actionable error instead of linking an inert element.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import type { Extension } from "../registry.js";
|
|
29
|
+
import { findRuntimeDir, platformSlug } from "../../driver/paths.js";
|
|
30
|
+
import { findRuntimeLibrary } from "../../driver/runtime-lib.js";
|
|
31
|
+
|
|
32
|
+
/** Absolute path to the per-platform prebuilt media archive. */
|
|
33
|
+
function mediaArchive(): string {
|
|
34
|
+
return join(findRuntimeDir(), "lib", platformSlug(), "media.a");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const mediaExtension: Extension = {
|
|
38
|
+
name: "media",
|
|
39
|
+
description: "Video/audio decoding for <video>/<audio> (requires gui, links FFmpeg)",
|
|
40
|
+
requires: ["gui"],
|
|
41
|
+
/**
|
|
42
|
+
* The registration bridge is a tiny C file linked as a normal object, so its
|
|
43
|
+
* constructor always runs. Keeping it out of `media.a` is what guarantees the
|
|
44
|
+
* archive is searched (see the header comment).
|
|
45
|
+
*/
|
|
46
|
+
runtimeSources: () => [join(findRuntimeDir(), "ext_media", "media_bridge.c")],
|
|
47
|
+
nativeObjects: () => {
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"the media extension is not available on Windows yet: it needs an MSVC-compatible " +
|
|
51
|
+
"FFmpeg build (see doc/gui/known-issues.md). Build without --ext media; media " +
|
|
52
|
+
"elements then report an 'error' event.",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const archive = findRuntimeLibrary("media");
|
|
56
|
+
if (archive === undefined) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`media native library not found at ${mediaArchive()} (or media.lib).\n` +
|
|
59
|
+
`Build it for ${platformSlug()} first with \`npm run media\` (see doc/gui/build.md).`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return [archive];
|
|
63
|
+
},
|
|
64
|
+
/** `media.a` is C++ and FFmpeg itself needs libm; the GUI extension already
|
|
65
|
+
* contributes the C++ runtime and the OS frameworks. */
|
|
66
|
+
linkerFlags: () => (process.platform === "win32" ? [] : ["-lm"]),
|
|
67
|
+
};
|
|
@@ -74,6 +74,12 @@ export interface Extension {
|
|
|
74
74
|
readonly name: string;
|
|
75
75
|
/** Description shown by `xbintsc ext list`. */
|
|
76
76
|
readonly description?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Other extensions this one needs. The registry rejects a build that enables
|
|
79
|
+
* it without them, so a dependent extension fails with an actionable message
|
|
80
|
+
* instead of an unresolved symbol at link time (e.g. `media` needs `gui`).
|
|
81
|
+
*/
|
|
82
|
+
readonly requires?: readonly string[];
|
|
77
83
|
/** C/asm sources compiled and linked alongside the generated module. */
|
|
78
84
|
runtimeSources?(): readonly string[];
|
|
79
85
|
/**
|
|
@@ -145,6 +151,24 @@ export class ExtensionRegistry {
|
|
|
145
151
|
return this.extensions.delete(name);
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Throw when a registered extension requires one that is not registered.
|
|
156
|
+
* Called before a build so `--ext media` without `--ext gui` reports which
|
|
157
|
+
* flags to pass instead of failing later with an unresolved symbol.
|
|
158
|
+
*/
|
|
159
|
+
validate(): this {
|
|
160
|
+
for (const extension of this.extensions.values()) {
|
|
161
|
+
for (const dependency of extension.requires ?? []) {
|
|
162
|
+
if (this.extensions.has(dependency)) continue;
|
|
163
|
+
throw new Error(
|
|
164
|
+
`extension '${extension.name}' requires the '${dependency}' extension; ` +
|
|
165
|
+
`pass --ext ${dependency},${extension.name}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
|
|
148
172
|
get(name: string): Extension | undefined {
|
|
149
173
|
return this.extensions.get(name);
|
|
150
174
|
}
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
export { ExtensionRegistry, createDefaultRegistry, coreExtension, type Extension, type ExtensionModule, type ModuleExport, type ModuleExports, type AssetLoader, type AssetLoadResult } from "./extensions/registry.js";
|
|
33
33
|
export { bundledExtensions } from "./extensions/catalog.js";
|
|
34
34
|
export { nodeExtension } from "./extensions/node/index.js";
|
|
35
|
+
export { mediaExtension } from "./extensions/media/index.js";
|
|
35
36
|
export type { NodeModule } from "./extensions/node/module.js";
|
|
36
37
|
export {
|
|
37
38
|
NativeExtensionError,
|