@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.
- package/LICENSE +373 -0
- package/README.md +103 -0
- package/dist/bundles/mediabunny-ac3.js +3829 -0
- package/dist/bundles/mediabunny-ac3.min.js +3510 -0
- package/dist/bundles/mediabunny-ac3.min.mjs +3509 -0
- package/dist/bundles/mediabunny-ac3.mjs +3792 -0
- package/dist/mediabunny-ac3.d.ts +20 -0
- package/dist/modules/build/ac3.d.ts +3 -0
- package/dist/modules/build/ac3.d.ts.map +1 -0
- package/dist/modules/build/ac3.js +0 -0
- package/dist/modules/src/codec.worker.d.ts +9 -0
- package/dist/modules/src/codec.worker.d.ts.map +1 -0
- package/dist/modules/src/codec.worker.js +269 -0
- package/dist/modules/src/decoder.d.ts +16 -0
- package/dist/modules/src/decoder.d.ts.map +1 -0
- package/dist/modules/src/decoder.js +61 -0
- package/dist/modules/src/encoder.d.ts +16 -0
- package/dist/modules/src/encoder.d.ts.map +1 -0
- package/dist/modules/src/encoder.js +151 -0
- package/dist/modules/src/index.d.ts +10 -0
- package/dist/modules/src/index.d.ts.map +1 -0
- package/dist/modules/src/index.js +22 -0
- package/dist/modules/src/shared.d.ts +96 -0
- package/dist/modules/src/shared.d.ts.map +1 -0
- package/dist/modules/src/shared.js +15 -0
- package/dist/modules/src/worker-client.d.ts +14 -0
- package/dist/modules/src/worker-client.d.ts.map +1 -0
- package/dist/modules/src/worker-client.js +61 -0
- package/dist/modules/tsconfig.tsbuildinfo +1 -0
- package/package.json +56 -0
- package/src/bridge.c +289 -0
- package/src/codec.worker.ts +306 -0
- package/src/decoder.ts +70 -0
- package/src/encoder.ts +191 -0
- package/src/index.ts +21 -0
- package/src/shared.ts +103 -0
- 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
|
+
};
|