@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.
- package/LICENSE +373 -0
- package/README.md +106 -0
- package/dist/bundles/mediabunny-dts.js +5776 -0
- package/dist/bundles/mediabunny-dts.min.js +5404 -0
- package/dist/bundles/mediabunny-dts.min.mjs +5403 -0
- package/dist/bundles/mediabunny-dts.mjs +5741 -0
- package/dist/mediabunny-dts.d.ts +20 -0
- package/dist/modules/build/dts.d.ts +3 -0
- package/dist/modules/build/dts.d.ts.map +1 -0
- package/dist/modules/build/dts.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 +271 -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 +68 -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 +155 -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 +23 -0
- package/dist/modules/src/shared.d.ts +93 -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 +16 -0
- package/dist/modules/src/worker-client.d.ts.map +1 -0
- package/dist/modules/src/worker-client.js +95 -0
- package/dist/modules/tsconfig.tsbuildinfo +1 -0
- package/package.json +59 -0
- package/src/bridge.c +321 -0
- package/src/codec.worker.ts +306 -0
- package/src/decoder.ts +80 -0
- package/src/encoder.ts +197 -0
- package/src/index.ts +23 -0
- package/src/shared.ts +100 -0
- 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
|
+
};
|