diff --git a/README.md b/README.md index f4833e5..064d3ad 100644 --- a/README.md +++ b/README.md @@ -34,15 +34,15 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi Kino +      + + Screen Studio + ### Silver sponsors
- - Pintura Labs - -      Ponder diff --git a/docs/index.md b/docs/index.md index 6ded5c5..7922f13 100644 --- a/docs/index.md +++ b/docs/index.md @@ -95,9 +95,9 @@ const sponsors = { { image: '/sponsors/gling.svg', name: 'Gling AI', url: 'https://www.gling.ai/' }, { image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' }, { image: '/sponsors/kino.jpg', name: 'Kino', url: 'https://kino.ai/' }, + { image: '/sponsors/screen-studio.webp', name: 'Screen Studio', url: 'https://screen.studio/' }, ], silver: [ - { image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' }, { image: '/sponsors/ponder.png', name: 'Ponder', url: 'https://ponder.ai/' }, ], bronze: [ @@ -107,6 +107,7 @@ const sponsors = { { image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' }, ], individual: [ + { image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' }, { image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' }, { image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' }, { image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' }, diff --git a/docs/public/sponsors/screen-studio.webp b/docs/public/sponsors/screen-studio.webp new file mode 100644 index 0000000..dc6c20e Binary files /dev/null and b/docs/public/sponsors/screen-studio.webp differ diff --git a/examples/file-compression/file-compression.ts b/examples/file-compression/file-compression.ts index 975dfa6..ff52ea4 100644 --- a/examples/file-compression/file-compression.ts +++ b/examples/file-compression/file-compression.ts @@ -143,7 +143,7 @@ loadUrlButton.addEventListener('click', () => { const url = prompt( 'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the' + ' right CORS headers set.', - 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + 'https://remotion.media/BigBuckBunny.mp4', ); if (!url) { return; diff --git a/examples/media-player/media-player.ts b/examples/media-player/media-player.ts index d87098c..4de5ba4 100644 --- a/examples/media-player/media-player.ts +++ b/examples/media-player/media-player.ts @@ -712,7 +712,7 @@ loadUrlButton.addEventListener('click', () => { const url = prompt( 'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the' + ' right CORS headers set.', - 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + 'https://remotion.media/BigBuckBunny.mp4', ); if (!url) { return; diff --git a/examples/metadata-extraction/metadata-extraction.ts b/examples/metadata-extraction/metadata-extraction.ts index fe81c1b..61cf332 100644 --- a/examples/metadata-extraction/metadata-extraction.ts +++ b/examples/metadata-extraction/metadata-extraction.ts @@ -221,7 +221,7 @@ loadUrlButton.addEventListener('click', () => { const url = prompt( 'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the' + ' right CORS headers set.', - 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + 'https://remotion.media/BigBuckBunny.mp4', ); if (!url) { return; diff --git a/examples/thumbnail-generation/thumbnail-generation.ts b/examples/thumbnail-generation/thumbnail-generation.ts index 28eeede..a62ad6f 100644 --- a/examples/thumbnail-generation/thumbnail-generation.ts +++ b/examples/thumbnail-generation/thumbnail-generation.ts @@ -137,7 +137,7 @@ loadUrlButton.addEventListener('click', () => { const url = prompt( 'Please enter a URL of a media file. Note that it must be HTTPS and support cross-origin requests, so have the' + ' right CORS headers set.', - 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + 'https://remotion.media/BigBuckBunny.mp4', ); if (!url) { return; diff --git a/package-lock.json b/package-lock.json index a075aa1..e146134 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.34.2", + "version": "1.34.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.34.2", + "version": "1.34.4", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7743,9 +7743,9 @@ } }, "node_modules/mediabunny": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.34.0.tgz", - "integrity": "sha512-mjn/7QVEPbPEakuNKD8OIPDhaoe2NlgWVyyqftuLVWvsgLol7g2MQzXWCjx523CP8Y9jE2H09hbYCuTwSI7pSQ==", + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.34.3.tgz", + "integrity": "sha512-Jb0x3jyvadGVpo8aZSn53NZduPbPdbx9foQsAGOmKBz7Hai9B38Kf+7eXbxXFcXoXxcTyNaZeDhoA0agoBQldA==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12069,7 +12069,7 @@ }, "packages/ac3": { "name": "@mediabunny/ac3", - "version": "1.34.2", + "version": "1.34.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12084,7 +12084,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.34.2", + "version": "1.34.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index b1213d2..6700376 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.34.2", + "version": "1.34.4", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/ac3/package.json b/packages/ac3/package.json index 6e3eda5..225fc05 100644 --- a/packages/ac3/package.json +++ b/packages/ac3/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/ac3", "author": "Vanilagy", - "version": "1.34.2", + "version": "1.34.4", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.", "main": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs", diff --git a/packages/ac3/src/decoder.ts b/packages/ac3/src/decoder.ts index 73cc9fe..a676073 100644 --- a/packages/ac3/src/decoder.ts +++ b/packages/ac3/src/decoder.ts @@ -13,7 +13,7 @@ import { EncodedPacket, registerDecoder, } from 'mediabunny'; -import { sendCommand } from './worker-client'; +import { sendCommand, refWorker, unrefWorker } from './worker-client'; class Ac3Decoder extends CustomAudioDecoder { private ctx = 0; @@ -23,6 +23,8 @@ class Ac3Decoder extends CustomAudioDecoder { } async init() { + await refWorker(); + const result = await sendCommand({ type: 'init-decoder', data: { codec: this.codec }, @@ -53,8 +55,9 @@ class Ac3Decoder extends CustomAudioDecoder { await sendCommand({ type: 'flush-decoder', data: { ctx: this.ctx } }); } - close() { + async close() { void sendCommand({ type: 'close-decoder', data: { ctx: this.ctx } }); + await unrefWorker(); } } diff --git a/packages/ac3/src/encoder.ts b/packages/ac3/src/encoder.ts index 1f567a0..2db9dfa 100644 --- a/packages/ac3/src/encoder.ts +++ b/packages/ac3/src/encoder.ts @@ -13,7 +13,7 @@ import { EncodedPacket, registerEncoder, } from 'mediabunny'; -import { sendCommand } from './worker-client'; +import { sendCommand, refWorker, unrefWorker } from './worker-client'; import { assert } from './shared'; import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../../../shared/ac3-misc'; @@ -38,11 +38,14 @@ class Ac3Encoder extends CustomAudioEncoder { return (codec === 'ac3' || codec === 'eac3') && config.numberOfChannels >= 1 && config.numberOfChannels <= 8 - && sampleRates.includes(config.sampleRate); + && sampleRates.includes(config.sampleRate) + && config.bitrate !== undefined; } async init() { - assert(this.config.bitrate); + await refWorker(); + + assert(this.config.bitrate !== undefined); this.sampleRate = this.config.sampleRate; this.numberOfChannels = this.config.numberOfChannels; @@ -130,6 +133,7 @@ class Ac3Encoder extends CustomAudioEncoder { close() { void sendCommand({ type: 'close-encoder', data: { ctx: this.ctx } }); + void unrefWorker(); } private async encodeOneFrame() { diff --git a/packages/ac3/src/worker-client.ts b/packages/ac3/src/worker-client.ts index d79e1ae..74a8bab 100644 --- a/packages/ac3/src/worker-client.ts +++ b/packages/ac3/src/worker-client.ts @@ -10,13 +10,51 @@ import { assert, type WorkerCommand, type WorkerResponse, type WorkerResponseDat // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand import createWorker from './codec.worker'; -let workerPromise: Promise | null; +type ExtendedWorker = Worker & { + ref?: () => void; + unref?: () => void; +}; + +let workerPromise: Promise | null; let nextMessageId = 0; const pendingMessages = new Map void; reject: (reason?: unknown) => void; }>(); +let refCount = 0; +let keepAliveInterval: ReturnType | null = null; + +export const refWorker = async () => { + refCount++; + if (refCount === 1) { + keepAliveInterval = setInterval(() => {}, 2 ** 31 - 1); + const worker = await ensureWorker(); + worker.ref?.(); + } +}; + +export const unrefWorker = async () => { + refCount--; + if (refCount === 0) { + if (keepAliveInterval !== null) { + clearInterval(keepAliveInterval); + keepAliveInterval = null; + } + + const worker = await workerPromise; + if (worker) { + if (worker.unref) { + worker.unref(); // If we don't do this, then the Node process never terminates by itself + } else if (typeof window === 'undefined') { + // Non-browser environment without unref - terminate instead + worker.terminate(); + workerPromise = null; + } + } + } +}; + export const sendCommand = async ( command: WorkerCommand & { type: T }, transferables?: Transferable[], @@ -41,7 +79,8 @@ export const sendCommand = async ( const ensureWorker = () => { return workerPromise ??= (async () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const worker = (await createWorker()) as Worker; + const worker = (await createWorker()) as ExtendedWorker; + worker.unref?.(); // Start unreffed const onMessage = (data: WorkerResponse) => { const pending = pendingMessages.get(data.id); diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index 8457f35..63e6769 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.34.2", + "version": "1.34.4", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", diff --git a/scripts/esbuild/inlined-workers.ts b/scripts/esbuild/inlined-workers.ts index 0f08ba9..f93098d 100644 --- a/scripts/esbuild/inlined-workers.ts +++ b/scripts/esbuild/inlined-workers.ts @@ -28,7 +28,7 @@ export default function Worker() { const inlineWorkerFunctionCode = ` export default async function inlineWorker(scriptText) { if (typeof Worker !== 'undefined' && typeof Bun === 'undefined') { - // Browser, Deno + // Browser, Deno (Deno can't do dynamic import of worker_threads) const blob = new Blob([scriptText], { type: "text/javascript" }); const url = URL.createObjectURL(blob); @@ -39,10 +39,10 @@ export default async function inlineWorker(scriptText) { // Node, Bun (Bun's Worker is flaky, worker_threads works much better) let Worker; + const workerModule = 'node:worker_threads'; try { - Worker = (await import('worker_threads')).Worker; + Worker = (await import(workerModule)).Worker; } catch { - const workerModule = 'worker_threads'; Worker = require(workerModule).Worker; }