From af43f67835625b96afc0336d930585749c56affb Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 12 May 2026 15:04:32 +0200 Subject: [PATCH] Add license headers, add @mediabunny/server to build process, add doc blocks, small fixes --- .github/workflows/release.yml | 5 +++ docs/api-config.json | 3 +- package.json | 2 +- packages/server/api-extractor.json | 6 ++- packages/server/src/audio-decoder.ts | 19 ++++++-- packages/server/src/audio-encoder.ts | 17 ++++++- packages/server/src/audio-sample.ts | 50 ++++++++++++++++----- packages/server/src/index.ts | 25 +++++++++++ packages/server/src/misc.ts | 8 ++++ packages/server/src/video-decoder.ts | 10 ++++- packages/server/src/video-encoder.ts | 10 ++++- packages/server/src/video-sample.ts | 46 +++++++++++++++++-- packages/server/tsconfig.api-extractor.json | 6 +++ scripts/build.sh | 12 ++++- scripts/ensure-license-headers.ts | 5 ++- 15 files changed, 197 insertions(+), 27 deletions(-) create mode 100644 packages/server/tsconfig.api-extractor.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3020bad..e06b340 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,11 @@ jobs: packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.mjs packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.min.mjs packages/flac-encoder/dist/mediabunny-flac-encoder.d.ts + packages/server/dist/bundles/mediabunny-server.cjs + packages/server/dist/bundles/mediabunny-server.min.cjs + packages/server/dist/bundles/mediabunny-server.mjs + packages/server/dist/bundles/mediabunny-server.min.mjs + packages/server/dist/mediabunny-server.d.ts - name: Publish workspace packages to npm run: npm publish --access public --workspaces ${{ github.event.release.prerelease && '--tag beta' || '' }} diff --git a/docs/api-config.json b/docs/api-config.json index 07644e2..1f39ade 100644 --- a/docs/api-config.json +++ b/docs/api-config.json @@ -24,5 +24,6 @@ "@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny.", "@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny.", "@mediabunny/aac-encoder": "Polyfills AAC encoder support to Mediabunny.", - "@mediabunny/flac-encoder": "Adds FLAC encoder support to Mediabunny." + "@mediabunny/flac-encoder": "Adds FLAC encoder support to Mediabunny.", + "@mediabunny/server": "Adds full video/audio decoder and encoder support to Mediabunny running in server-side environments such as Node, Bun, or Deno." } diff --git a/package.json b/package.json index 79b5890..eaf6a91 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "docs:dev": "vitepress dev docs", "docs:build": "npm run build && npm run docs:generate && vitepress build docs && npm run examples:build && cp dist/mediabunny.d.ts dist-docs/", "docs:preview": "vitepress preview docs", - "docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts packages/aac-encoder/src/index.ts packages/flac-encoder/src/index.ts docs/api-config.json", + "docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts packages/ac3/src/index.ts packages/aac-encoder/src/index.ts packages/flac-encoder/src/index.ts packages/server/src/index.ts docs/api-config.json", "dev": "vite", "examples:build": "vite build", "fix-build-import-paths": "tsx scripts/add-import-extensions.ts", diff --git a/packages/server/api-extractor.json b/packages/server/api-extractor.json index fafe3a5..b8a1bb4 100644 --- a/packages/server/api-extractor.json +++ b/packages/server/api-extractor.json @@ -2,7 +2,9 @@ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "mainEntryPointFilePath": "dist/modules/src/index.d.ts", "bundledPackages": [], - "compiler": {}, + "compiler": { + "tsconfigFilePath": "/tsconfig.api-extractor.json" + }, "apiReport": { "enabled": false }, @@ -11,7 +13,7 @@ }, "dtsRollup": { "enabled": true, - "untrimmedFilePath": "dist/mediabunny-flac-encoder.d.ts" + "untrimmedFilePath": "dist/mediabunny-server.d.ts" }, "tsdocMetadata": { "enabled": false diff --git a/packages/server/src/audio-decoder.ts b/packages/server/src/audio-decoder.ts index 34186ed..a273214 100644 --- a/packages/server/src/audio-decoder.ts +++ b/packages/server/src/audio-decoder.ts @@ -1,4 +1,12 @@ -import { AudioCodec, AudioSample, CustomAudioDecoder, EncodedPacket, MaybePromise } from 'mediabunny'; +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { AudioCodec, AudioSample, CustomAudioDecoder, EncodedPacket, type MaybePromise } from 'mediabunny'; import * as NodeAv from 'node-av'; import { CODEC_TO_CODEC_ID, getChannelLayout } from './misc'; import { assert, toUint8Array } from '../../../src/misc'; @@ -76,8 +84,13 @@ export class NodeAvAudioDecoder extends CustomAudioDecoder { receiveFrame(ret: number) { NodeAv.FFmpegError.throwIfError(ret, 'Receive frame'); - const timestamp = Number(this.frame.pts) / this.config.sampleRate; - this.onSample(new AudioSample(new NodeAvFrameAudioSampleResource(this.frame, timestamp))); + const clone = this.frame.clone(); + if (!clone) { + throw new Error('Allocation failure during frame clone.'); + } + + clone.timeBase = new NodeAv.Rational(1, this.config.sampleRate); + this.onSample(new AudioSample(new NodeAvFrameAudioSampleResource(clone))); } async flush(): Promise { diff --git a/packages/server/src/audio-encoder.ts b/packages/server/src/audio-encoder.ts index db9ad05..a20030a 100644 --- a/packages/server/src/audio-encoder.ts +++ b/packages/server/src/audio-encoder.ts @@ -1,4 +1,19 @@ -import { AudioCodec, AudioSample, CustomAudioEncoder, MaybePromise, QUALITY_MEDIUM, EncodedPacket } from 'mediabunny'; +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { + AudioCodec, + AudioSample, + CustomAudioEncoder, + type MaybePromise, + QUALITY_MEDIUM, + EncodedPacket, +} from 'mediabunny'; import * as NodeAv from 'node-av'; import { CODEC_TO_CODEC_ID, fromAudioSampleFormat, getChannelLayout } from './misc'; import { assert, toUint8Array } from '../../../src/misc'; diff --git a/packages/server/src/audio-sample.ts b/packages/server/src/audio-sample.ts index 1afc9b8..e09d2cb 100644 --- a/packages/server/src/audio-sample.ts +++ b/packages/server/src/audio-sample.ts @@ -1,22 +1,49 @@ +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + import { AudioSampleResource } from 'mediabunny'; import * as NodeAv from 'node-av'; import { toAudioSampleFormat } from './misc'; import { assert, toUint8Array } from '../../../src/misc'; +/** + * A custom `AudioSampleResource` backed by NodeAV's + * [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html), which in turn is backed by FFmpeg's + * [`AVFrame`](https://ffmpeg.org/doxygen/2.7/structAVFrame.html). You can use this resource to create `AudioSample` + * instances that are directly backed by FFmpeg's `AVFrame` without data having to be copied. + * + * @group \@mediabunny/server + * @public + */ export class NodeAvFrameAudioSampleResource extends AudioSampleResource { - frame: NodeAv.Frame; - timestamp: number; + /** @internal */ + _frame: NodeAv.Frame | null; - constructor(frame: NodeAv.Frame, timestamp: number) { - super(); - - const clone = frame.clone(); - if (!clone) { - throw new Error('Allocation failure during frame clone.'); + /** + * The NodeAV [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html) instance backing this resource. + * Access throws if the resource has already been closed. + */ + get frame() { + if (!this._frame) { + throw new Error('NodeAvFrameAudioSampleResource has been closed.'); } - this.frame = clone; - this.timestamp = timestamp; + return this._frame; + } + + constructor(frame: NodeAv.Frame) { + super(); + + if (frame.getMediaType() !== NodeAv.AVMEDIA_TYPE_AUDIO) { + throw new Error('NodeAvFrameAudioSampleResource must be initialized with an audio frame.'); + } + + this._frame = frame; } getFormat(): AudioSampleFormat { @@ -41,11 +68,12 @@ export class NodeAvFrameAudioSampleResource extends AudioSampleResource { } getTimestamp(): number { - return this.timestamp; + return Number(this.frame.pts) / this.frame.timeBase.den; } close(): void { this.frame.free(); + this._frame = null; } getDataPlane(planeIndex: number): Uint8Array { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 770de18..c0eada0 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,3 +1,11 @@ +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + import { registerDecoder, registerEncoder, registerVideoSampleTransformer } from 'mediabunny'; import * as NodeAv from 'node-av'; import { NodeAvVideoDecoder } from './video-decoder'; @@ -18,6 +26,20 @@ if ((globalThis as Record)[SERVER_LOADED_SYMBOL]) { (globalThis as Record)[SERVER_LOADED_SYMBOL] = true; let registered = false; + +/** + * Registers video and audio decoders and encoders for all codecs, using FFmpeg's libavcodec under the hood. + * Additionally, a custom `VideoSample` transformer based on libavfilter is registered to enable resizing, rotation and + * cropping of video frames. + * + * Make sure to call this function before interacting with Mediabunny. + * + * The decoders and encoders will automatically detect hardware acceleration support for each codec and platform and + * make use of it if applicable. + * + * @group \@mediabunny/server + * @public + */ export const registerMediabunnyServer = () => { if (registered) { return; @@ -36,3 +58,6 @@ export const registerMediabunnyServer = () => { registerVideoSampleTransformer(transformVideoSample); }; + +export { NodeAvFrameVideoSampleResource } from './video-sample'; +export { NodeAvFrameAudioSampleResource } from './audio-sample'; diff --git a/packages/server/src/misc.ts b/packages/server/src/misc.ts index 193673a..4e73b4b 100644 --- a/packages/server/src/misc.ts +++ b/packages/server/src/misc.ts @@ -1,3 +1,11 @@ +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + import { VideoSamplePixelFormat, MediaCodec } from 'mediabunny'; import * as NodeAv from 'node-av'; diff --git a/packages/server/src/video-decoder.ts b/packages/server/src/video-decoder.ts index 12e7502..e234a3b 100644 --- a/packages/server/src/video-decoder.ts +++ b/packages/server/src/video-decoder.ts @@ -1,4 +1,12 @@ -import { CustomVideoDecoder, VideoCodec, EncodedPacket, VideoSample, MaybePromise, Rational } from 'mediabunny'; +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { CustomVideoDecoder, VideoCodec, EncodedPacket, VideoSample, type MaybePromise, Rational } from 'mediabunny'; import * as NodeAv from 'node-av'; import { CODEC_TO_CODEC_ID, getHardwareDecoderCodec, LIBVPX_VP9 } from './misc'; import { assert, binarySearchLessOrEqual, simplifyRational, toUint8Array } from '../../../src/misc'; diff --git a/packages/server/src/video-encoder.ts b/packages/server/src/video-encoder.ts index bbe853c..d2c4055 100644 --- a/packages/server/src/video-encoder.ts +++ b/packages/server/src/video-encoder.ts @@ -1,6 +1,14 @@ +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + import { CustomVideoEncoder, - MaybePromise, + type MaybePromise, QUALITY_MEDIUM, VideoCodec, VideoSample, diff --git a/packages/server/src/video-sample.ts b/packages/server/src/video-sample.ts index c9723fd..549470e 100644 --- a/packages/server/src/video-sample.ts +++ b/packages/server/src/video-sample.ts @@ -1,4 +1,13 @@ +/*! + * Copyright (c) 2026-present, Vanilagy and contributors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + import { + type MaybePromise, VideoSamplePixelFormat, VideoSampleResource, VideoSampleColorSpace, @@ -9,7 +18,7 @@ import { VideoSampleTransformationDescription, } from 'mediabunny'; import * as NodeAv from 'node-av'; -import { assert, MaybePromise, toUint8Array } from '../../../src/misc'; +import { assert, toUint8Array } from '../../../src/misc'; import { toPixelFormat, unmapColorPrimaries, @@ -29,13 +38,43 @@ const JPEG_RANGE_PIX_FORMATS = new Set([ NodeAv.AV_PIX_FMT_YUVJ444P, ]); +/** + * A custom `VideoSampleResource` backed by NodeAV's + * [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html), which in turn is backed by FFmpeg's + * [`AVFrame`](https://ffmpeg.org/doxygen/2.7/structAVFrame.html). You can use this resource to create `VideoSample` + * instances that are directly backed by FFmpeg's `AVFrame` without data having to be copied. Since `AVFrame`s can + * themselves be backed by data on the GPU, this enables zero-copy hardware-accelerated decode and encode paths. + * + * When using Electron, you can directly create `Frame` instances without the data having to leave the GPU. For more, + * see [NodeAV's docs](https://seydx.github.io/node-av/api/lib/classes/Frame.html). + * + * @group \@mediabunny/server + * @public + */ export class NodeAvFrameVideoSampleResource extends VideoSampleResource { - frame: NodeAv.Frame; + /** @internal */ + _frame: NodeAv.Frame | null; + + /** + * The NodeAV [`Frame`](https://seydx.github.io/node-av/api/lib/classes/Frame.html) instance backing this resource. + * Access throws if the resource has already been closed. + */ + get frame() { + if (!this._frame) { + throw new Error('NodeAvFrameVideoSampleResource has been closed.'); + } + + return this._frame; + } constructor(frame: NodeAv.Frame) { super(); - this.frame = frame; + if (frame.getMediaType() !== NodeAv.AVMEDIA_TYPE_VIDEO) { + throw new Error('NodeAvFrameVideoSampleResource must be initialized with a video frame.'); + } + + this._frame = frame; } getFormat(): VideoSamplePixelFormat | null { @@ -82,6 +121,7 @@ export class NodeAvFrameVideoSampleResource extends VideoSampleResource { close(): void { this.frame.free(); + this._frame = null; } getDataPlanes(): MaybePromise { diff --git a/packages/server/tsconfig.api-extractor.json b/packages/server/tsconfig.api-extractor.json new file mode 100644 index 0000000..9d9606d --- /dev/null +++ b/packages/server/tsconfig.api-extractor.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": {} // Fails if we don't override it due to the Mediabunny imports + } +} diff --git a/scripts/build.sh b/scripts/build.sh index 3ffb1cb..f33e33a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -9,16 +9,22 @@ rm -rf packages/mp3-encoder/dist rm -rf packages/ac3/dist rm -rf packages/aac-encoder/dist rm -rf packages/flac-encoder/dist +rm -rf packages/server/dist # Ensure license headers on all source files tsx scripts/ensure-license-headers.ts # Type check & generate .js and .d.ts files -tsc -p src +tsc -p src --stripInternal false # Don't strip internals since the packages may use them tsc -p packages/mp3-encoder tsc -p packages/ac3 tsc -p packages/aac-encoder tsc -p packages/flac-encoder +tsc -p packages/server + +# Generate the root again, now with internals properly stripped +rm -rf dist +tsc -p src # So that the resulting files use valid ESM imports with file extension. This only runs for the core Mediabunny as only # it ships the individual files to npm (for tree shaking, because it's large) @@ -33,6 +39,7 @@ api-extractor run -c packages/mp3-encoder/api-extractor.json api-extractor run -c packages/ac3/api-extractor.json api-extractor run -c packages/aac-encoder/api-extractor.json api-extractor run -c packages/flac-encoder/api-extractor.json +api-extractor run -c packages/server/api-extractor.json # Checks that all symbols are documented tsx scripts/check-docblocks.ts dist/mediabunny.d.ts @@ -40,6 +47,7 @@ tsx scripts/check-docblocks.ts packages/mp3-encoder/dist/mediabunny-mp3-encoder. tsx scripts/check-docblocks.ts packages/ac3/dist/mediabunny-ac3.d.ts tsx scripts/check-docblocks.ts packages/aac-encoder/dist/mediabunny-aac-encoder.d.ts tsx scripts/check-docblocks.ts packages/flac-encoder/dist/mediabunny-flac-encoder.d.ts +tsx scripts/check-docblocks.ts packages/server/dist/mediabunny-server.d.ts # Checks that API docs are generatable npm run docs:generate -- --dry @@ -50,4 +58,4 @@ echo 'export as namespace MediabunnyMp3Encoder;' >> packages/mp3-encoder/dist/me echo 'export as namespace MediabunnyAc3;' >> packages/ac3/dist/mediabunny-ac3.d.ts echo 'export as namespace MediabunnyAacEncoder;' >> packages/aac-encoder/dist/mediabunny-aac-encoder.d.ts echo 'export as namespace MediabunnyFlacEncoder;' >> packages/flac-encoder/dist/mediabunny-flac-encoder.d.ts - +echo 'export as namespace MediabunnyServer;' >> packages/server/dist/mediabunny-server.d.ts diff --git a/scripts/ensure-license-headers.ts b/scripts/ensure-license-headers.ts index 37720ed..91921dc 100644 --- a/scripts/ensure-license-headers.ts +++ b/scripts/ensure-license-headers.ts @@ -35,9 +35,12 @@ const checkDirectory = (dirPath: string) => { }; checkDirectory(path.join(__dirname, '..', 'src')); +checkDirectory(path.join(__dirname, '..', 'shared')); checkDirectory(path.join(__dirname, '..', 'packages', 'mp3-encoder', 'src')); checkDirectory(path.join(__dirname, '..', 'packages', 'ac3', 'src')); -checkDirectory(path.join(__dirname, '..', 'shared')); +checkDirectory(path.join(__dirname, '..', 'packages', 'flac-encoder', 'src')); +checkDirectory(path.join(__dirname, '..', 'packages', 'aac-encoder', 'src')); +checkDirectory(path.join(__dirname, '..', 'packages', 'server', 'src')); if (missingFiles.length > 0) { console.error('Files missing license header:');