@mediabunny/dts 1.55.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.
Files changed (37) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +106 -0
  3. package/dist/bundles/mediabunny-dts.js +5776 -0
  4. package/dist/bundles/mediabunny-dts.min.js +5404 -0
  5. package/dist/bundles/mediabunny-dts.min.mjs +5403 -0
  6. package/dist/bundles/mediabunny-dts.mjs +5741 -0
  7. package/dist/mediabunny-dts.d.ts +20 -0
  8. package/dist/modules/build/dts.d.ts +3 -0
  9. package/dist/modules/build/dts.d.ts.map +1 -0
  10. package/dist/modules/build/dts.js +0 -0
  11. package/dist/modules/src/codec.worker.d.ts +9 -0
  12. package/dist/modules/src/codec.worker.d.ts.map +1 -0
  13. package/dist/modules/src/codec.worker.js +271 -0
  14. package/dist/modules/src/decoder.d.ts +16 -0
  15. package/dist/modules/src/decoder.d.ts.map +1 -0
  16. package/dist/modules/src/decoder.js +68 -0
  17. package/dist/modules/src/encoder.d.ts +16 -0
  18. package/dist/modules/src/encoder.d.ts.map +1 -0
  19. package/dist/modules/src/encoder.js +155 -0
  20. package/dist/modules/src/index.d.ts +10 -0
  21. package/dist/modules/src/index.d.ts.map +1 -0
  22. package/dist/modules/src/index.js +23 -0
  23. package/dist/modules/src/shared.d.ts +93 -0
  24. package/dist/modules/src/shared.d.ts.map +1 -0
  25. package/dist/modules/src/shared.js +15 -0
  26. package/dist/modules/src/worker-client.d.ts +16 -0
  27. package/dist/modules/src/worker-client.d.ts.map +1 -0
  28. package/dist/modules/src/worker-client.js +95 -0
  29. package/dist/modules/tsconfig.tsbuildinfo +1 -0
  30. package/package.json +59 -0
  31. package/src/bridge.c +321 -0
  32. package/src/codec.worker.ts +306 -0
  33. package/src/decoder.ts +80 -0
  34. package/src/encoder.ts +197 -0
  35. package/src/index.ts +23 -0
  36. package/src/shared.ts +100 -0
  37. package/src/worker-client.ts +109 -0
package/src/encoder.ts ADDED
@@ -0,0 +1,197 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ import {
10
+ CustomAudioEncoder,
11
+ AudioCodec,
12
+ AudioSample,
13
+ EncodedPacket,
14
+ registerEncoder,
15
+ } from 'mediabunny';
16
+ import { sendCommand, refWorker, unrefWorker } from './worker-client';
17
+ import { assert } from './shared';
18
+ import { DTS_CHANNEL_COUNTS, DTS_SAMPLE_RATES, dtsBitrateFits } from '../../../shared/dts-misc';
19
+
20
+ class DtsEncoder extends CustomAudioEncoder {
21
+ private ctx = 0;
22
+ private encoderFrameSize = 0;
23
+ private sampleRate = 0;
24
+ private numberOfChannels = 0;
25
+ private chunkMetadata: EncodedAudioChunkMetadata = {};
26
+
27
+ // Accumulate interleaved f32 samples until we have a full frame
28
+ private pendingBuffer = new Float32Array(2 ** 16);
29
+ private pendingFrames = 0;
30
+ private nextSampleTimestampInSamples: number | null = null;
31
+ private nextPacketTimestampInSamples: number | null = null;
32
+
33
+ static override supports(codec: AudioCodec, config: AudioEncoderConfig): boolean {
34
+ return codec === 'dts'
35
+ && DTS_CHANNEL_COUNTS.includes(config.numberOfChannels)
36
+ && DTS_SAMPLE_RATES.includes(config.sampleRate)
37
+ && config.bitrate !== undefined
38
+ && dtsBitrateFits(config.bitrate, config.sampleRate, config.numberOfChannels);
39
+ }
40
+
41
+ async init() {
42
+ await refWorker();
43
+
44
+ assert(this.config.bitrate !== undefined);
45
+ this.sampleRate = this.config.sampleRate;
46
+ this.numberOfChannels = this.config.numberOfChannels;
47
+
48
+ const result = await sendCommand({
49
+ type: 'init-encoder',
50
+ data: {
51
+ numberOfChannels: this.config.numberOfChannels,
52
+ sampleRate: this.config.sampleRate,
53
+ bitrate: this.config.bitrate,
54
+ },
55
+ });
56
+
57
+ this.ctx = result.ctx;
58
+ this.encoderFrameSize = result.frameSize;
59
+
60
+ this.resetInternalState();
61
+ }
62
+
63
+ private resetInternalState() {
64
+ this.pendingFrames = 0;
65
+ this.nextSampleTimestampInSamples = null;
66
+ this.nextPacketTimestampInSamples = null;
67
+
68
+ this.chunkMetadata = {
69
+ decoderConfig: {
70
+ codec: 'dtsc',
71
+ numberOfChannels: this.config.numberOfChannels,
72
+ sampleRate: this.config.sampleRate,
73
+ },
74
+ };
75
+ }
76
+
77
+ async encode(audioSample: AudioSample) {
78
+ if (this.nextSampleTimestampInSamples === null) {
79
+ this.nextSampleTimestampInSamples = Math.round(audioSample.timestamp * this.sampleRate);
80
+ this.nextPacketTimestampInSamples = this.nextSampleTimestampInSamples;
81
+ }
82
+
83
+ const channels = this.numberOfChannels;
84
+ const incomingFrames = audioSample.numberOfFrames;
85
+
86
+ // Extract interleaved f32 data
87
+ const totalBytes = audioSample.allocationSize({ format: 'f32', planeIndex: 0 });
88
+ const audioBytes = new Uint8Array(totalBytes);
89
+ audioSample.copyTo(audioBytes, { format: 'f32', planeIndex: 0 });
90
+ const incomingData = new Float32Array(audioBytes.buffer);
91
+
92
+ const requiredSamples = (this.pendingFrames + incomingFrames) * channels;
93
+ if (requiredSamples > this.pendingBuffer.length) {
94
+ let newSize = this.pendingBuffer.length;
95
+ while (newSize < requiredSamples) {
96
+ newSize *= 2;
97
+ }
98
+ const newBuffer = new Float32Array(newSize);
99
+ newBuffer.set(this.pendingBuffer.subarray(0, this.pendingFrames * channels));
100
+ this.pendingBuffer = newBuffer;
101
+ }
102
+ this.pendingBuffer.set(incomingData, this.pendingFrames * channels);
103
+ this.pendingFrames += incomingFrames;
104
+
105
+ while (this.pendingFrames >= this.encoderFrameSize) {
106
+ await this.encodeOneFrame();
107
+ }
108
+ }
109
+
110
+ async flush() {
111
+ // Pad remaining samples with silence to fill a full frame
112
+ if (this.pendingFrames > 0) {
113
+ const channels = this.numberOfChannels;
114
+ const frameSize = this.encoderFrameSize;
115
+ const usedSamples = this.pendingFrames * channels;
116
+ const frameSamples = frameSize * channels;
117
+
118
+ this.pendingBuffer.fill(0, usedSamples, frameSamples);
119
+ this.pendingFrames = frameSize;
120
+
121
+ await this.encodeOneFrame();
122
+ }
123
+
124
+ await sendCommand({ type: 'flush-encoder', data: { ctx: this.ctx } });
125
+
126
+ this.resetInternalState();
127
+ }
128
+
129
+ close() {
130
+ void sendCommand({ type: 'close-encoder', data: { ctx: this.ctx } });
131
+ void unrefWorker();
132
+ }
133
+
134
+ private async encodeOneFrame() {
135
+ assert(this.nextSampleTimestampInSamples !== null);
136
+ assert(this.nextPacketTimestampInSamples !== null);
137
+
138
+ const channels = this.numberOfChannels;
139
+ const frameSize = this.encoderFrameSize;
140
+ const frameSamples = frameSize * channels;
141
+
142
+ const frameData = this.pendingBuffer.slice(0, frameSamples);
143
+
144
+ // Shift remaining using copyWithin
145
+ this.pendingFrames -= frameSize;
146
+ if (this.pendingFrames > 0) {
147
+ this.pendingBuffer.copyWithin(0, frameSamples, frameSamples + this.pendingFrames * channels);
148
+ }
149
+
150
+ const audioData = frameData.buffer;
151
+ const result = await sendCommand({
152
+ type: 'encode',
153
+ data: {
154
+ ctx: this.ctx,
155
+ audioData,
156
+ timestamp: this.nextSampleTimestampInSamples,
157
+ },
158
+ }, [audioData]);
159
+
160
+ this.nextSampleTimestampInSamples += frameSize;
161
+
162
+ // We always get exactly one packet because we encode the correct frame size
163
+ const packet = new EncodedPacket(
164
+ new Uint8Array(result.encodedData),
165
+ 'key',
166
+ this.nextPacketTimestampInSamples / this.sampleRate,
167
+ result.duration / this.sampleRate,
168
+ );
169
+
170
+ this.nextPacketTimestampInSamples += result.duration;
171
+
172
+ this.onPacket(
173
+ packet,
174
+ this.chunkMetadata,
175
+ );
176
+
177
+ this.chunkMetadata = {};
178
+ }
179
+ }
180
+
181
+ let registered = false;
182
+
183
+ /**
184
+ * Registers a DTS audio encoder, which Mediabunny will then use automatically when applicable. Make sure to call this
185
+ * function before starting any encoding task.
186
+ *
187
+ * @group \@mediabunny/dts
188
+ * @public
189
+ */
190
+ export const registerDtsEncoder = () => {
191
+ if (registered) {
192
+ return;
193
+ }
194
+ registered = true;
195
+
196
+ registerEncoder(DtsEncoder);
197
+ };
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ import { Logging } from 'mediabunny';
10
+
11
+ const DTS_LOADED_SYMBOL = Symbol.for('@mediabunny/dts loaded');
12
+ if ((globalThis as Record<symbol, unknown>)[DTS_LOADED_SYMBOL]) {
13
+ Logging._error(
14
+ '[WARNING]\n@mediabunny/dts was loaded twice.'
15
+ + ' This will likely cause the encoder/decoder not to work correctly.'
16
+ + ' Check if multiple dependencies are importing different versions of @mediabunny/dts,'
17
+ + ' or if something is being bundled incorrectly.',
18
+ );
19
+ }
20
+ (globalThis as Record<symbol, unknown>)[DTS_LOADED_SYMBOL] = true;
21
+
22
+ export { registerDtsDecoder } from './decoder';
23
+ export { registerDtsEncoder } from './encoder';
package/src/shared.ts ADDED
@@ -0,0 +1,100 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ export type WorkerCommand = {
10
+ type: 'init-decoder';
11
+ data: Record<string, never>;
12
+ } | {
13
+ type: 'decode';
14
+ data: {
15
+ ctx: number;
16
+ encodedData: ArrayBuffer;
17
+ timestamp: number;
18
+ };
19
+ } | {
20
+ type: 'flush-decoder';
21
+ data: {
22
+ ctx: number;
23
+ };
24
+ } | {
25
+ type: 'close-decoder';
26
+ data: {
27
+ ctx: number;
28
+ };
29
+ } | {
30
+ type: 'init-encoder';
31
+ data: {
32
+ numberOfChannels: number;
33
+ sampleRate: number;
34
+ bitrate: number;
35
+ };
36
+ } | {
37
+ type: 'encode';
38
+ data: {
39
+ ctx: number;
40
+ audioData: ArrayBuffer;
41
+ timestamp: number;
42
+ };
43
+ } | {
44
+ type: 'flush-encoder';
45
+ data: {
46
+ ctx: number;
47
+ };
48
+ } | {
49
+ type: 'close-encoder';
50
+ data: {
51
+ ctx: number;
52
+ };
53
+ };
54
+
55
+ export type WorkerResponseData = {
56
+ type: 'init-decoder';
57
+ ctx: number;
58
+ frameSize: number;
59
+ } | {
60
+ type: 'decode';
61
+ pcmData: ArrayBuffer;
62
+ format: AudioSampleFormat;
63
+ channels: number;
64
+ sampleRate: number;
65
+ sampleCount: number;
66
+ pts: number;
67
+ } | {
68
+ type: 'flush-decoder';
69
+ } | {
70
+ type: 'close-decoder';
71
+ } | {
72
+ type: 'init-encoder';
73
+ ctx: number;
74
+ frameSize: number;
75
+ } | {
76
+ type: 'encode';
77
+ encodedData: ArrayBuffer;
78
+ pts: number;
79
+ duration: number;
80
+ } | {
81
+ type: 'flush-encoder';
82
+ } | {
83
+ type: 'close-encoder';
84
+ };
85
+
86
+ export type WorkerResponse = {
87
+ id: number;
88
+ } & ({
89
+ success: true;
90
+ data: WorkerResponseData;
91
+ } | {
92
+ success: false;
93
+ error: unknown;
94
+ });
95
+
96
+ export function assert(x: unknown): asserts x {
97
+ if (!x) {
98
+ throw new Error('Assertion failed.');
99
+ }
100
+ }
@@ -0,0 +1,109 @@
1
+ /*!
2
+ * Copyright (c) 2026-present, Vanilagy and contributors
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ import { assert, type WorkerCommand, type WorkerResponse, type WorkerResponseData } from './shared';
10
+ // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
11
+ import createWorker from './codec.worker';
12
+
13
+ type ExtendedWorker = Worker & {
14
+ ref?: () => void;
15
+ unref?: () => void;
16
+ };
17
+
18
+ let workerPromise: Promise<ExtendedWorker> | null;
19
+ let nextMessageId = 0;
20
+ const pendingMessages = new Map<number, {
21
+ resolve: (value: WorkerResponseData) => void;
22
+ reject: (reason?: unknown) => void;
23
+ }>();
24
+
25
+ let refCount = 0;
26
+ let keepAliveInterval: ReturnType<typeof setInterval> | null = null;
27
+
28
+ export const refWorker = async () => {
29
+ refCount++;
30
+ if (refCount === 1) {
31
+ keepAliveInterval = setInterval(() => {}, 2 ** 31 - 1);
32
+ const worker = await ensureWorker();
33
+ worker.ref?.();
34
+ }
35
+ };
36
+
37
+ export const unrefWorker = async () => {
38
+ refCount--;
39
+ if (refCount === 0) {
40
+ if (keepAliveInterval !== null) {
41
+ clearInterval(keepAliveInterval);
42
+ keepAliveInterval = null;
43
+ }
44
+
45
+ const worker = await workerPromise;
46
+ if (worker) {
47
+ if (worker.unref) {
48
+ worker.unref(); // If we don't do this, then the Node process never terminates by itself
49
+ // Keep the worker around tho
50
+ } else if (typeof window === 'undefined') {
51
+ // Non-browser environment without unref - terminate instead
52
+ worker.terminate();
53
+ workerPromise = null;
54
+ }
55
+ }
56
+ }
57
+ };
58
+
59
+ export const sendCommand = async <T extends string>(
60
+ command: WorkerCommand & { type: T },
61
+ transferables?: Transferable[],
62
+ ) => {
63
+ const worker = await ensureWorker();
64
+
65
+ return new Promise<WorkerResponseData & { type: T }>((resolve, reject) => {
66
+ const id = nextMessageId++;
67
+ pendingMessages.set(id, {
68
+ resolve: resolve as (value: WorkerResponseData) => void,
69
+ reject,
70
+ });
71
+
72
+ if (transferables) {
73
+ worker.postMessage({ id, command }, transferables);
74
+ } else {
75
+ worker.postMessage({ id, command });
76
+ }
77
+ });
78
+ };
79
+
80
+ const ensureWorker = () => {
81
+ return workerPromise ??= (async () => {
82
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
83
+ const worker = (await createWorker()) as ExtendedWorker;
84
+ worker.unref?.(); // Start unreffed
85
+
86
+ const onMessage = (data: WorkerResponse) => {
87
+ const pending = pendingMessages.get(data.id);
88
+ assert(pending !== undefined);
89
+
90
+ pendingMessages.delete(data.id);
91
+ if (data.success) {
92
+ pending.resolve(data.data);
93
+ } else {
94
+ pending.reject(data.error);
95
+ }
96
+ };
97
+
98
+ if (worker.addEventListener) {
99
+ worker.addEventListener('message', event => onMessage(event.data as WorkerResponse));
100
+ } else {
101
+ const nodeWorker = worker as unknown as {
102
+ on: (event: string, listener: (data: never) => void) => void;
103
+ };
104
+ nodeWorker.on('message', onMessage);
105
+ }
106
+
107
+ return worker;
108
+ })();
109
+ };