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