@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/bridge.c ADDED
@@ -0,0 +1,321 @@
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
+ #include <emscripten.h>
10
+ #include <stdint.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include "libavcodec/avcodec.h"
14
+ #include "libavutil/opt.h"
15
+ #include "libavutil/channel_layout.h"
16
+
17
+ typedef struct {
18
+ AVCodecContext *codec_ctx;
19
+ AVPacket *packet;
20
+ AVFrame *frame;
21
+ } DecoderContext;
22
+
23
+ EMSCRIPTEN_KEEPALIVE
24
+ DecoderContext *init_decoder() {
25
+ const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_DTS);
26
+ if (!codec) return NULL;
27
+
28
+ AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
29
+ if (!codec_ctx) return NULL;
30
+
31
+ if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
32
+ avcodec_free_context(&codec_ctx);
33
+ return NULL;
34
+ }
35
+
36
+ AVPacket *packet = av_packet_alloc();
37
+ if (!packet) {
38
+ avcodec_free_context(&codec_ctx);
39
+ return NULL;
40
+ }
41
+
42
+ AVFrame *frame = av_frame_alloc();
43
+ if (!frame) {
44
+ av_packet_free(&packet);
45
+ avcodec_free_context(&codec_ctx);
46
+ return NULL;
47
+ }
48
+
49
+ DecoderContext *ctx = malloc(sizeof(DecoderContext));
50
+ if (!ctx) {
51
+ av_frame_free(&frame);
52
+ av_packet_free(&packet);
53
+ avcodec_free_context(&codec_ctx);
54
+ return NULL;
55
+ }
56
+
57
+ ctx->codec_ctx = codec_ctx;
58
+ ctx->packet = packet;
59
+ ctx->frame = frame;
60
+
61
+ return ctx;
62
+ }
63
+
64
+ EMSCRIPTEN_KEEPALIVE
65
+ uint8_t *configure_decode_packet(DecoderContext *ctx, int size) {
66
+ if (av_new_packet(ctx->packet, size) < 0) {
67
+ return NULL;
68
+ }
69
+
70
+ return ctx->packet->data;
71
+ }
72
+
73
+ EMSCRIPTEN_KEEPALIVE
74
+ int decode_packet(DecoderContext *ctx, int64_t pts) {
75
+ ctx->packet->pts = pts;
76
+ int ret = avcodec_send_packet(ctx->codec_ctx, ctx->packet);
77
+ av_packet_unref(ctx->packet);
78
+ if (ret < 0) return ret;
79
+
80
+ ret = avcodec_receive_frame(ctx->codec_ctx, ctx->frame);
81
+ if (ret < 0) return ret;
82
+
83
+ return 0;
84
+ }
85
+
86
+ EMSCRIPTEN_KEEPALIVE
87
+ int get_decoded_format(DecoderContext *ctx) {
88
+ return ctx->frame->format;
89
+ }
90
+
91
+ EMSCRIPTEN_KEEPALIVE
92
+ uint8_t *get_decoded_plane_ptr(DecoderContext *ctx, int plane) {
93
+ return ctx->frame->data[plane];
94
+ }
95
+
96
+ EMSCRIPTEN_KEEPALIVE
97
+ int get_decoded_channels(DecoderContext *ctx) {
98
+ return ctx->frame->ch_layout.nb_channels;
99
+ }
100
+
101
+ EMSCRIPTEN_KEEPALIVE
102
+ int get_decoded_sample_rate(DecoderContext *ctx) {
103
+ return ctx->frame->sample_rate;
104
+ }
105
+
106
+ EMSCRIPTEN_KEEPALIVE
107
+ int get_decoded_sample_count(DecoderContext *ctx) {
108
+ return ctx->frame->nb_samples;
109
+ }
110
+
111
+ EMSCRIPTEN_KEEPALIVE
112
+ int64_t get_decoded_pts(DecoderContext *ctx) {
113
+ return ctx->frame->pts;
114
+ }
115
+
116
+ EMSCRIPTEN_KEEPALIVE
117
+ void flush_decoder(DecoderContext *ctx) {
118
+ avcodec_send_packet(ctx->codec_ctx, NULL);
119
+ while (avcodec_receive_frame(ctx->codec_ctx, ctx->frame) == 0) {}
120
+ avcodec_flush_buffers(ctx->codec_ctx);
121
+ }
122
+
123
+ EMSCRIPTEN_KEEPALIVE
124
+ void close_decoder(DecoderContext *ctx) {
125
+ av_frame_free(&ctx->frame);
126
+ av_packet_free(&ctx->packet);
127
+ avcodec_free_context(&ctx->codec_ctx);
128
+ free(ctx);
129
+ }
130
+
131
+ typedef struct {
132
+ AVCodecContext *codec_ctx;
133
+ AVPacket *packet;
134
+ AVFrame *frame;
135
+ float *input_buffer;
136
+ int input_buffer_size;
137
+ int64_t encoded_pts;
138
+ int encoded_duration;
139
+ } EncoderContext;
140
+
141
+ /**
142
+ * DTS insists on the side-based surround layouts and rejects the back-based ones that av_channel_layout_default hands
143
+ * out for 4, 5 and 6 channels.
144
+ */
145
+ static int set_dts_channel_layout(AVChannelLayout *layout, int channels) {
146
+ switch (channels) {
147
+ case 1: {
148
+ AVChannelLayout mono = AV_CHANNEL_LAYOUT_MONO;
149
+ return av_channel_layout_copy(layout, &mono);
150
+ }
151
+ case 2: {
152
+ AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO;
153
+ return av_channel_layout_copy(layout, &stereo);
154
+ }
155
+ case 4: {
156
+ AVChannelLayout quad_side = AV_CHANNEL_LAYOUT_2_2;
157
+ return av_channel_layout_copy(layout, &quad_side);
158
+ }
159
+ case 5: {
160
+ AVChannelLayout five_zero = AV_CHANNEL_LAYOUT_5POINT0;
161
+ return av_channel_layout_copy(layout, &five_zero);
162
+ }
163
+ case 6: {
164
+ AVChannelLayout five_one = AV_CHANNEL_LAYOUT_5POINT1;
165
+ return av_channel_layout_copy(layout, &five_one);
166
+ }
167
+ default:
168
+ return -1;
169
+ }
170
+ }
171
+
172
+ EMSCRIPTEN_KEEPALIVE
173
+ EncoderContext *init_encoder(int channels, int sample_rate, int bitrate) {
174
+ const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_DTS);
175
+ if (!codec) return NULL;
176
+
177
+ AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
178
+ if (!codec_ctx) return NULL;
179
+
180
+ codec_ctx->sample_fmt = AV_SAMPLE_FMT_S32;
181
+ codec_ctx->sample_rate = sample_rate;
182
+ codec_ctx->bit_rate = bitrate;
183
+ codec_ctx->time_base = (AVRational){1, sample_rate};
184
+
185
+ // FFmpeg marks its DTS encoder experimental, so it refuses to open at the default compliance level
186
+ codec_ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
187
+
188
+ if (set_dts_channel_layout(&codec_ctx->ch_layout, channels) < 0) {
189
+ avcodec_free_context(&codec_ctx);
190
+ return NULL;
191
+ }
192
+
193
+ if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
194
+ avcodec_free_context(&codec_ctx);
195
+ return NULL;
196
+ }
197
+
198
+ AVPacket *packet = av_packet_alloc();
199
+ if (!packet) {
200
+ avcodec_free_context(&codec_ctx);
201
+ return NULL;
202
+ }
203
+
204
+ AVFrame *frame = av_frame_alloc();
205
+ if (!frame) {
206
+ av_packet_free(&packet);
207
+ avcodec_free_context(&codec_ctx);
208
+ return NULL;
209
+ }
210
+
211
+ // The frame has a fixed format, so let's create it now:
212
+ frame->format = AV_SAMPLE_FMT_S32;
213
+ frame->sample_rate = sample_rate;
214
+ frame->nb_samples = codec_ctx->frame_size;
215
+ av_channel_layout_copy(&frame->ch_layout, &codec_ctx->ch_layout);
216
+
217
+ if (av_frame_get_buffer(frame, 0) < 0) {
218
+ av_frame_free(&frame);
219
+ av_packet_free(&packet);
220
+ avcodec_free_context(&codec_ctx);
221
+ return NULL;
222
+ }
223
+
224
+ EncoderContext *ctx = malloc(sizeof(EncoderContext));
225
+ if (!ctx) {
226
+ av_frame_free(&frame);
227
+ av_packet_free(&packet);
228
+ avcodec_free_context(&codec_ctx);
229
+ return NULL;
230
+ }
231
+
232
+ ctx->codec_ctx = codec_ctx;
233
+ ctx->packet = packet;
234
+ ctx->frame = frame;
235
+ ctx->input_buffer = NULL;
236
+ ctx->input_buffer_size = 0;
237
+ ctx->encoded_pts = 0;
238
+ ctx->encoded_duration = 0;
239
+
240
+ return ctx;
241
+ }
242
+
243
+ EMSCRIPTEN_KEEPALIVE
244
+ int get_encoder_frame_size(EncoderContext *ctx) {
245
+ return ctx->codec_ctx->frame_size;
246
+ }
247
+
248
+ EMSCRIPTEN_KEEPALIVE
249
+ float *get_encode_input_ptr(EncoderContext *ctx, int size) {
250
+ if (ctx->input_buffer_size < size) {
251
+ free(ctx->input_buffer);
252
+ ctx->input_buffer = malloc(size);
253
+ if (!ctx->input_buffer) {
254
+ ctx->input_buffer_size = 0;
255
+ return NULL;
256
+ }
257
+ ctx->input_buffer_size = size;
258
+ }
259
+ return ctx->input_buffer;
260
+ }
261
+
262
+ EMSCRIPTEN_KEEPALIVE
263
+ int encode_frame(EncoderContext *ctx, int64_t pts) {
264
+ int channels = ctx->codec_ctx->ch_layout.nb_channels;
265
+ int frame_size = ctx->frame->nb_samples;
266
+
267
+ ctx->frame->pts = pts;
268
+
269
+ // DTS encodes from s32, which is a packed format, so the samples stay interleaved and all land in data[0]
270
+ float *input = ctx->input_buffer;
271
+ int32_t *output = (int32_t *)ctx->frame->data[0];
272
+ for (int i = 0; i < frame_size * channels; i++) {
273
+ float sample = input[i];
274
+ if (sample > 1.0f) sample = 1.0f;
275
+ if (sample < -1.0f) sample = -1.0f;
276
+ output[i] = (int32_t)(sample * 2147483647.0f);
277
+ }
278
+
279
+ int ret = avcodec_send_frame(ctx->codec_ctx, ctx->frame);
280
+ if (ret < 0) return ret;
281
+
282
+ ret = avcodec_receive_packet(ctx->codec_ctx, ctx->packet);
283
+ if (ret < 0) return ret;
284
+
285
+ ctx->encoded_pts = ctx->packet->pts;
286
+ ctx->encoded_duration = ctx->packet->duration;
287
+
288
+ return ctx->packet->size;
289
+ }
290
+
291
+ EMSCRIPTEN_KEEPALIVE
292
+ void flush_encoder(EncoderContext *ctx) {
293
+ avcodec_send_frame(ctx->codec_ctx, NULL);
294
+ while (avcodec_receive_packet(ctx->codec_ctx, ctx->packet) == 0) {
295
+ av_packet_unref(ctx->packet);
296
+ }
297
+ }
298
+
299
+ EMSCRIPTEN_KEEPALIVE
300
+ uint8_t *get_encoded_data(EncoderContext *ctx) {
301
+ return ctx->packet->data;
302
+ }
303
+
304
+ EMSCRIPTEN_KEEPALIVE
305
+ int64_t get_encoded_pts(EncoderContext *ctx) {
306
+ return ctx->encoded_pts;
307
+ }
308
+
309
+ EMSCRIPTEN_KEEPALIVE
310
+ int get_encoded_duration(EncoderContext *ctx) {
311
+ return ctx->encoded_duration;
312
+ }
313
+
314
+ EMSCRIPTEN_KEEPALIVE
315
+ void close_encoder(EncoderContext *ctx) {
316
+ free(ctx->input_buffer);
317
+ av_frame_free(&ctx->frame);
318
+ av_packet_free(&ctx->packet);
319
+ avcodec_free_context(&ctx->codec_ctx);
320
+ free(ctx);
321
+ }
@@ -0,0 +1,306 @@
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 createModule from '../build/dts';
10
+ import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
11
+
12
+ type ExtendedEmscriptenModule = EmscriptenModule & {
13
+ cwrap: typeof cwrap;
14
+ };
15
+
16
+ let module: ExtendedEmscriptenModule;
17
+ let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
18
+
19
+ let initDecoderFn: () => number;
20
+ let configureDecodePacket: (ctx: number, size: number) => number;
21
+ let decodePacket: (ctx: number, pts: bigint) => number;
22
+ let getDecodedFormat: (ctx: number) => number;
23
+ let getDecodedPlanePtr: (ctx: number, plane: number) => number;
24
+ let getDecodedChannels: (ctx: number) => number;
25
+ let getDecodedSampleRate: (ctx: number) => number;
26
+ let getDecodedSampleCount: (ctx: number) => number;
27
+ let getDecodedPts: (ctx: number) => bigint;
28
+ let flushDecoderFn: (ctx: number) => void;
29
+ let closeDecoderFn: (ctx: number) => void;
30
+
31
+ let initEncoderFn: (channels: number, sampleRate: number, bitrate: number) => number;
32
+ let getEncoderFrameSize: (ctx: number) => number;
33
+ let getEncodeInputPtr: (ctx: number, size: number) => number;
34
+ let encodeFrameFn: (ctx: number, pts: bigint) => number;
35
+ let flushEncoderFn: (ctx: number) => void;
36
+ let getEncodedData: (ctx: number) => number;
37
+ let getEncodedPts: (ctx: number) => bigint;
38
+ let getEncodedDuration: (ctx: number) => number;
39
+ let closeEncoderFn: (ctx: number) => void;
40
+
41
+ const ensureModule = async () => {
42
+ if (!module) {
43
+ if (modulePromise) {
44
+ // If we don't do this we can have a race condition
45
+ return modulePromise;
46
+ }
47
+
48
+ modulePromise = createModule() as Promise<ExtendedEmscriptenModule>;
49
+ module = await modulePromise;
50
+ modulePromise = null;
51
+
52
+ initDecoderFn = module.cwrap('init_decoder', 'number', []);
53
+ configureDecodePacket = module.cwrap('configure_decode_packet', 'number', ['number', 'number']);
54
+ decodePacket = module.cwrap('decode_packet', 'number', ['number', 'number']) as unknown as typeof decodePacket;
55
+ getDecodedFormat = module.cwrap('get_decoded_format', 'number', ['number']);
56
+ getDecodedPlanePtr = module.cwrap('get_decoded_plane_ptr', 'number', ['number', 'number']);
57
+ getDecodedChannels = module.cwrap('get_decoded_channels', 'number', ['number']);
58
+ getDecodedSampleRate = module.cwrap('get_decoded_sample_rate', 'number', ['number']);
59
+ getDecodedSampleCount = module.cwrap('get_decoded_sample_count', 'number', ['number']);
60
+ getDecodedPts = module.cwrap('get_decoded_pts', 'number', ['number']) as unknown as typeof getDecodedPts;
61
+ flushDecoderFn = module.cwrap('flush_decoder', null, ['number']);
62
+ closeDecoderFn = module.cwrap('close_decoder', null, ['number']);
63
+
64
+ initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number']);
65
+ getEncoderFrameSize = module.cwrap('get_encoder_frame_size', 'number', ['number']);
66
+ getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
67
+ encodeFrameFn = module.cwrap('encode_frame', 'number', ['number', 'number']) as unknown as typeof encodeFrameFn;
68
+ flushEncoderFn = module.cwrap('flush_encoder', null, ['number']);
69
+ getEncodedData = module.cwrap('get_encoded_data', 'number', ['number']);
70
+ getEncodedPts = module.cwrap('get_encoded_pts', 'number', ['number']) as unknown as typeof getEncodedPts;
71
+ getEncodedDuration = module.cwrap('get_encoded_duration', 'number', ['number']);
72
+ closeEncoderFn = module.cwrap('close_encoder', null, ['number']);
73
+ }
74
+ };
75
+
76
+ const initDecoder = async () => {
77
+ await ensureModule();
78
+
79
+ const ctx = initDecoderFn();
80
+ if (ctx === 0) {
81
+ throw new Error('Failed to initialize DTS decoder.');
82
+ }
83
+
84
+ return { ctx, frameSize: 0 };
85
+ };
86
+
87
+ // Keys are AVSampleFormat enum values
88
+ const AV_FORMAT_MAP: Record<number, { format: AudioSampleFormat; bytesPerSample: number; planar: boolean }> = {
89
+ 0: { format: 'u8', bytesPerSample: 1, planar: false },
90
+ 1: { format: 's16', bytesPerSample: 2, planar: false },
91
+ 2: { format: 's32', bytesPerSample: 4, planar: false },
92
+ 3: { format: 'f32', bytesPerSample: 4, planar: false },
93
+ 5: { format: 'u8-planar', bytesPerSample: 1, planar: true },
94
+ 6: { format: 's16-planar', bytesPerSample: 2, planar: true },
95
+ 7: { format: 's32-planar', bytesPerSample: 4, planar: true },
96
+ 8: { format: 'f32-planar', bytesPerSample: 4, planar: true },
97
+ };
98
+
99
+ const decode = (ctx: number, encodedData: ArrayBuffer, timestamp: number) => {
100
+ const bytes = new Uint8Array(encodedData);
101
+
102
+ const dataPtr = configureDecodePacket(ctx, bytes.length);
103
+ if (dataPtr === 0) {
104
+ throw new Error('Failed to configure decode packet.');
105
+ }
106
+
107
+ module.HEAPU8.set(bytes, dataPtr);
108
+
109
+ const ret = decodePacket(ctx, BigInt(timestamp));
110
+ if (ret < 0) {
111
+ throw new Error(`Decode failed with error code ${ret}.`);
112
+ }
113
+
114
+ const avFormat = getDecodedFormat(ctx);
115
+ const info = AV_FORMAT_MAP[avFormat];
116
+ if (!info) {
117
+ throw new Error(`Unsupported AVSampleFormat: ${avFormat}`);
118
+ }
119
+
120
+ const channels = getDecodedChannels(ctx);
121
+ const sampleRate = getDecodedSampleRate(ctx);
122
+ const sampleCount = getDecodedSampleCount(ctx);
123
+ const pts = Number(getDecodedPts(ctx));
124
+
125
+ let pcmData: ArrayBuffer;
126
+ if (info.planar) {
127
+ const planeSize = sampleCount * info.bytesPerSample;
128
+ const buffer = new Uint8Array(planeSize * channels);
129
+
130
+ for (let ch = 0; ch < channels; ch++) {
131
+ const ptr = getDecodedPlanePtr(ctx, ch);
132
+ buffer.set(module.HEAPU8.subarray(ptr, ptr + planeSize), ch * planeSize);
133
+ }
134
+
135
+ pcmData = buffer.buffer;
136
+ } else {
137
+ const totalSize = sampleCount * channels * info.bytesPerSample;
138
+ const ptr = getDecodedPlanePtr(ctx, 0);
139
+ pcmData = module.HEAPU8.slice(ptr, ptr + totalSize).buffer;
140
+ }
141
+
142
+ return { pcmData, format: info.format, channels, sampleRate, sampleCount, pts };
143
+ };
144
+
145
+ const initEncoder = async (
146
+ numberOfChannels: number,
147
+ sampleRate: number,
148
+ bitrate: number,
149
+ ) => {
150
+ await ensureModule();
151
+
152
+ const ctx = initEncoderFn(numberOfChannels, sampleRate, bitrate);
153
+ if (ctx === 0) {
154
+ throw new Error('Failed to initialize DTS encoder.');
155
+ }
156
+
157
+ return { ctx, frameSize: getEncoderFrameSize(ctx) };
158
+ };
159
+
160
+ const encode = (ctx: number, audioData: ArrayBuffer, timestamp: number) => {
161
+ const audioBytes = new Uint8Array(audioData);
162
+
163
+ const inputPtr = getEncodeInputPtr(ctx, audioBytes.length);
164
+ if (inputPtr === 0) {
165
+ throw new Error('Failed to allocate encoder input buffer.');
166
+ }
167
+ module.HEAPU8.set(audioBytes, inputPtr);
168
+
169
+ const bytesWritten = encodeFrameFn(ctx, BigInt(timestamp));
170
+ if (bytesWritten < 0) {
171
+ throw new Error(`Encode failed with error code ${bytesWritten}.`);
172
+ }
173
+
174
+ const ptr = getEncodedData(ctx);
175
+ const encodedData = module.HEAPU8.slice(ptr, ptr + bytesWritten).buffer;
176
+ const pts = Number(getEncodedPts(ctx));
177
+ const duration = getEncodedDuration(ctx);
178
+
179
+ return { encodedData, pts, duration };
180
+ };
181
+
182
+ const flushEncoder = (ctx: number) => {
183
+ flushEncoderFn(ctx);
184
+ };
185
+
186
+ const onMessage = (data: { id: number; command: WorkerCommand }) => {
187
+ const { id, command } = data;
188
+
189
+ const handleCommand = async (): Promise<void> => {
190
+ try {
191
+ let result: WorkerResponseData;
192
+ const transferables: Transferable[] = [];
193
+
194
+ switch (command.type) {
195
+ case 'init-decoder': {
196
+ const { ctx, frameSize } = await initDecoder();
197
+ result = { type: command.type, ctx, frameSize };
198
+ }; break;
199
+
200
+ case 'decode': {
201
+ const decoded = decode(command.data.ctx, command.data.encodedData, command.data.timestamp);
202
+ result = {
203
+ type: command.type,
204
+ pcmData: decoded.pcmData,
205
+ format: decoded.format,
206
+ channels: decoded.channels,
207
+ sampleRate: decoded.sampleRate,
208
+ sampleCount: decoded.sampleCount,
209
+ pts: decoded.pts,
210
+ };
211
+ transferables.push(decoded.pcmData);
212
+ }; break;
213
+
214
+ case 'flush-decoder': {
215
+ flushDecoderFn(command.data.ctx);
216
+ result = { type: command.type };
217
+ }; break;
218
+
219
+ case 'close-decoder': {
220
+ closeDecoderFn(command.data.ctx);
221
+ result = { type: command.type };
222
+ }; break;
223
+
224
+ case 'init-encoder': {
225
+ const { ctx, frameSize } = await initEncoder(
226
+ command.data.numberOfChannels,
227
+ command.data.sampleRate,
228
+ command.data.bitrate,
229
+ );
230
+ result = { type: command.type, ctx, frameSize };
231
+ }; break;
232
+
233
+ case 'encode': {
234
+ const encoded = encode(
235
+ command.data.ctx,
236
+ command.data.audioData,
237
+ command.data.timestamp,
238
+ );
239
+ result = {
240
+ type: command.type,
241
+ encodedData: encoded.encodedData,
242
+ pts: encoded.pts,
243
+ duration: encoded.duration,
244
+ };
245
+ transferables.push(encoded.encodedData);
246
+ }; break;
247
+
248
+ case 'flush-encoder': {
249
+ flushEncoder(command.data.ctx);
250
+ result = { type: command.type };
251
+ }; break;
252
+
253
+ case 'close-encoder': {
254
+ closeEncoderFn(command.data.ctx);
255
+ result = { type: command.type };
256
+ }; break;
257
+ }
258
+
259
+ const response: WorkerResponse = {
260
+ id,
261
+ success: true,
262
+ data: result,
263
+ };
264
+ sendMessage(response, transferables);
265
+ } catch (error: unknown) {
266
+ const response: WorkerResponse = {
267
+ id,
268
+ success: false,
269
+ error,
270
+ };
271
+ sendMessage(response);
272
+ }
273
+ };
274
+
275
+ void handleCommand();
276
+ };
277
+
278
+ const sendMessage = (data: unknown, transferables?: Transferable[]) => {
279
+ if (parentPort) {
280
+ parentPort.postMessage(data, transferables ?? []);
281
+ } else {
282
+ self.postMessage(data, { transfer: transferables ?? [] });
283
+ }
284
+ };
285
+
286
+ let parentPort: {
287
+ postMessage: (data: unknown, transferables?: Transferable[]) => void;
288
+ on: (event: string, listener: (data: never) => void) => void;
289
+ } | null = null;
290
+
291
+ if (typeof self === 'undefined') {
292
+ const workerModule = 'worker_threads';
293
+ // eslint-disable-next-line @stylistic/max-len
294
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access
295
+ parentPort = require(workerModule).parentPort;
296
+ }
297
+
298
+ if (parentPort) {
299
+ parentPort.on('message', onMessage);
300
+ } else {
301
+ self.addEventListener('message', event => onMessage(event.data as { id: number; command: WorkerCommand }));
302
+ }
303
+
304
+ // Prevents the worker for being randomly closed by Firefox
305
+ // https://github.com/Vanilagy/mediabunny/issues/435
306
+ setInterval(() => {}, 1000);
package/src/decoder.ts ADDED
@@ -0,0 +1,80 @@
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
+ CustomAudioDecoder,
11
+ AudioCodec,
12
+ AudioSample,
13
+ EncodedPacket,
14
+ registerDecoder,
15
+ } from 'mediabunny';
16
+ import { sendCommand, refWorker, unrefWorker } from './worker-client';
17
+
18
+ class DtsDecoder extends CustomAudioDecoder {
19
+ private ctx = 0;
20
+
21
+ static override supports(codec: AudioCodec): boolean {
22
+ return codec === 'dts';
23
+ }
24
+
25
+ async init() {
26
+ await refWorker();
27
+
28
+ const result = await sendCommand({
29
+ type: 'init-decoder',
30
+ data: {},
31
+ });
32
+ this.ctx = result.ctx;
33
+ }
34
+
35
+ async decode(packet: EncodedPacket) {
36
+ const encodedData = packet.data.slice().buffer;
37
+ const timestamp = Math.round(packet.timestamp * this.config.sampleRate);
38
+
39
+ const result = await sendCommand({
40
+ type: 'decode',
41
+ data: { ctx: this.ctx, encodedData, timestamp },
42
+ }, [encodedData]);
43
+
44
+ const sample = new AudioSample({
45
+ data: result.pcmData,
46
+ format: result.format,
47
+ numberOfChannels: result.channels,
48
+ sampleRate: result.sampleRate,
49
+ timestamp: result.pts / result.sampleRate,
50
+ });
51
+ this.onSample(sample);
52
+ }
53
+
54
+ async flush() {
55
+ await sendCommand({ type: 'flush-decoder', data: { ctx: this.ctx } });
56
+ }
57
+
58
+ async close() {
59
+ void sendCommand({ type: 'close-decoder', data: { ctx: this.ctx } });
60
+ await unrefWorker();
61
+ }
62
+ }
63
+
64
+ let registered = false;
65
+
66
+ /**
67
+ * Registers a DTS audio decoder, which Mediabunny will then use automatically when applicable. Make sure to call this
68
+ * function before starting any decoding task.
69
+ *
70
+ * @group \@mediabunny/dts
71
+ * @public
72
+ */
73
+ export const registerDtsDecoder = () => {
74
+ if (registered) {
75
+ return;
76
+ }
77
+ registered = true;
78
+
79
+ registerDecoder(DtsDecoder);
80
+ };