webcodecs-utils 0.2.4 → 0.2.6
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/README.md +142 -0
- package/dist/audio/extract-channels.d.ts +45 -0
- package/dist/audio/extract-channels.d.ts.map +1 -0
- package/dist/audio/extract-channels.js +25 -0
- package/dist/audio/get-sample-rate.d.ts +19 -0
- package/dist/audio/get-sample-rate.d.ts.map +1 -0
- package/dist/audio/get-sample-rate.js +14 -0
- package/dist/audio/mp3.d.ts +162 -0
- package/dist/audio/mp3.d.ts.map +1 -0
- package/dist/audio/mp3.js +173 -0
- package/dist/demux/example-muxer.d.ts +74 -0
- package/dist/demux/example-muxer.d.ts.map +1 -0
- package/dist/demux/example-muxer.js +40 -0
- package/dist/demux/get-chunks.d.ts +81 -0
- package/dist/demux/get-chunks.d.ts.map +1 -0
- package/dist/demux/get-chunks.js +88 -0
- package/dist/demux/mp4-demuxer.d.ts +84 -0
- package/dist/demux/mp4-demuxer.d.ts.map +1 -0
- package/dist/demux/mp4-demuxer.js +215 -0
- package/dist/demux/simple-demuxer.d.ts +49 -0
- package/dist/demux/simple-demuxer.d.ts.map +1 -0
- package/dist/demux/simple-demuxer.js +87 -0
- package/dist/in-memory-storage.d.ts +30 -0
- package/dist/in-memory-storage.d.ts.map +1 -0
- package/dist/in-memory-storage.js +54 -0
- package/dist/index.cjs +5 -298
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -13873
- package/dist/mux/simple-muxer.d.ts +47 -0
- package/dist/mux/simple-muxer.d.ts.map +1 -0
- package/dist/mux/simple-muxer.js +83 -0
- package/dist/polyfills/media-stream-track-processor.d.ts +5 -0
- package/dist/polyfills/media-stream-track-processor.d.ts.map +1 -0
- package/dist/polyfills/media-stream-track-processor.js +109 -0
- package/dist/streams/video-decode-stream.d.ts +11 -0
- package/dist/streams/video-decode-stream.d.ts.map +1 -0
- package/dist/streams/video-decode-stream.js +39 -0
- package/dist/streams/video-encode-stream.d.ts +15 -0
- package/dist/streams/video-encode-stream.d.ts.map +1 -0
- package/dist/streams/video-encode-stream.js +40 -0
- package/dist/streams/video-process-stream.d.ts +22 -0
- package/dist/streams/video-process-stream.d.ts.map +1 -0
- package/dist/streams/video-process-stream.js +21 -0
- package/dist/video/get-bitrate.d.ts +35 -0
- package/dist/video/get-bitrate.d.ts.map +1 -0
- package/dist/video/get-bitrate.js +12 -0
- package/dist/video/get-codec-string.d.ts +46 -0
- package/dist/video/get-codec-string.d.ts.map +1 -0
- package/dist/video/get-codec-string.js +195 -0
- package/dist/video/gpu-renderer.d.ts +108 -0
- package/dist/video/gpu-renderer.d.ts.map +1 -0
- package/dist/video/gpu-renderer.js +266 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ npm install webcodecs-utils
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
+
### Individual Utilities
|
|
14
|
+
|
|
13
15
|
```typescript
|
|
14
16
|
import { getBitrate, GPUFrameRenderer, extractChannels, MP4Demuxer } from 'webcodecs-utils';
|
|
15
17
|
|
|
@@ -37,6 +39,35 @@ await demuxer.load();
|
|
|
37
39
|
const videoChunks = await demuxer.extractSegment('video', 0, 10);
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
### Streaming Pipeline
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import {
|
|
46
|
+
SimpleDemuxer,
|
|
47
|
+
VideoDecodeStream,
|
|
48
|
+
VideoProcessStream,
|
|
49
|
+
VideoEncodeStream,
|
|
50
|
+
SimpleMuxer
|
|
51
|
+
} from 'webcodecs-utils';
|
|
52
|
+
|
|
53
|
+
// Build a composable streaming pipeline with automatic backpressure
|
|
54
|
+
const demuxer = new SimpleDemuxer(file);
|
|
55
|
+
await demuxer.load();
|
|
56
|
+
|
|
57
|
+
const muxer = new SimpleMuxer({ video: 'avc' });
|
|
58
|
+
|
|
59
|
+
await demuxer.videoStream()
|
|
60
|
+
.pipeThrough(new VideoDecodeStream(await demuxer.getVideoDecoderConfig()))
|
|
61
|
+
.pipeThrough(new VideoProcessStream(async (frame) => {
|
|
62
|
+
// Custom processing: upscaling, filters, etc.
|
|
63
|
+
return frame;
|
|
64
|
+
}))
|
|
65
|
+
.pipeThrough(new VideoEncodeStream(encoderConfig))
|
|
66
|
+
.pipeTo(muxer.videoSink());
|
|
67
|
+
|
|
68
|
+
const blob = await muxer.finalize();
|
|
69
|
+
```
|
|
70
|
+
|
|
40
71
|
## Utilities
|
|
41
72
|
|
|
42
73
|
### Video
|
|
@@ -197,6 +228,117 @@ console.log(`Video: ${videoTrack.codec}, ${videoTrack.codedWidth}x${videoTrack.c
|
|
|
197
228
|
const videoChunks = await demuxer.extractSegment('video', 0, 10);
|
|
198
229
|
```
|
|
199
230
|
|
|
231
|
+
### Streaming Pipelines
|
|
232
|
+
|
|
233
|
+
Build production-ready video processing pipelines using the Streams API with automatic backpressure management.
|
|
234
|
+
|
|
235
|
+
- 📄 [Documentation](./src/streams/README.md)
|
|
236
|
+
- 🎮 [Transcode Demo](./demos/transcode-pipeline.html)
|
|
237
|
+
- 🎮 [AI Upscaling Demo](./demos/upscale-pipeline.html)
|
|
238
|
+
|
|
239
|
+
#### **VideoDecodeStream**
|
|
240
|
+
TransformStream that decodes EncodedVideoChunks into VideoFrames with automatic backpressure.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
class VideoDecodeStream extends TransformStream<EncodedVideoChunk, VideoFrame> {
|
|
244
|
+
constructor(
|
|
245
|
+
config: VideoDecoderConfig,
|
|
246
|
+
options?: {
|
|
247
|
+
highWaterMark?: number; // default: 10
|
|
248
|
+
maxDecodeQueueSize?: number; // default: 20
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### **VideoEncodeStream**
|
|
255
|
+
TransformStream that encodes VideoFrames into EncodedVideoChunks with automatic backpressure.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
class VideoEncodeStream extends TransformStream<
|
|
259
|
+
VideoFrame,
|
|
260
|
+
{ chunk: EncodedVideoChunk; meta?: EncodedVideoChunkMetadata }
|
|
261
|
+
> {
|
|
262
|
+
constructor(
|
|
263
|
+
config: VideoEncoderConfig,
|
|
264
|
+
options?: {
|
|
265
|
+
highWaterMark?: number; // default: 10
|
|
266
|
+
maxEncodeQueueSize?: number; // default: 20
|
|
267
|
+
keyFrameInterval?: number; // default: 60
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### **VideoProcessStream**
|
|
274
|
+
TransformStream that applies a custom processing function to each VideoFrame.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
class VideoProcessStream extends TransformStream<VideoFrame, VideoFrame> {
|
|
278
|
+
constructor(
|
|
279
|
+
transformFn: (frame: VideoFrame) => Promise<VideoFrame> | VideoFrame,
|
|
280
|
+
options?: {
|
|
281
|
+
highWaterMark?: number; // default: 5
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Example - AI Upscaling:**
|
|
288
|
+
```typescript
|
|
289
|
+
import WebSR from '@websr/websr';
|
|
290
|
+
|
|
291
|
+
const websr = new WebSR({ resolution, network, weights, gpu, canvas });
|
|
292
|
+
|
|
293
|
+
const upscaleStream = new VideoProcessStream(async (frame) => {
|
|
294
|
+
await websr.render(frame); // AI upscaling with WebGPU
|
|
295
|
+
return new VideoFrame(canvas, {
|
|
296
|
+
timestamp: frame.timestamp,
|
|
297
|
+
duration: frame.duration
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Use in pipeline
|
|
302
|
+
await videoStream
|
|
303
|
+
.pipeThrough(new VideoDecodeStream(config))
|
|
304
|
+
.pipeThrough(upscaleStream)
|
|
305
|
+
.pipeThrough(new VideoEncodeStream(config))
|
|
306
|
+
.pipeTo(muxer.videoSink());
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### **SimpleDemuxer** ⚠️ Demo/Learning Only
|
|
310
|
+
Simple wrapper around web-demuxer for easier usage in demos. For production, use [web-demuxer](https://github.com/bilibili/web-demuxer) or [MediaBunny](https://mediabunny.dev/) directly.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
class SimpleDemuxer {
|
|
314
|
+
constructor(file: File, options?: { wasmFilePath?: string })
|
|
315
|
+
|
|
316
|
+
async load(): Promise<void>
|
|
317
|
+
videoStream(startTime?: number): ReadableStream<EncodedVideoChunk>
|
|
318
|
+
audioStream(startTime?: number): ReadableStream<EncodedAudioChunk>
|
|
319
|
+
async getVideoDecoderConfig(): Promise<VideoDecoderConfig>
|
|
320
|
+
async getAudioDecoderConfig(): Promise<AudioDecoderConfig>
|
|
321
|
+
async getSegment(type: 'video' | 'audio', start: number, end: number): Promise<EncodedVideoChunk[] | EncodedAudioChunk[]>
|
|
322
|
+
async getMediaInfo(): Promise<MediaInfo>
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### **SimpleMuxer** ⚠️ Demo/Learning Only
|
|
327
|
+
Simple wrapper around MediaBunny's Output for easier muxing in demos. For production, use [MediaBunny](https://mediabunny.dev/) directly.
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
class SimpleMuxer {
|
|
331
|
+
constructor(config: {
|
|
332
|
+
video?: 'avc' | 'hevc' | 'vp8' | 'vp9' | 'av1';
|
|
333
|
+
audio?: 'aac' | 'opus' | 'mp3' | 'vorbis' | 'flac';
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
videoSink(): WritableStream<{ chunk: EncodedVideoChunk; meta?: EncodedVideoChunkMetadata }>
|
|
337
|
+
audioSink(): WritableStream<EncodedAudioChunk>
|
|
338
|
+
async finalize(): Promise<Blob>
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
200
342
|
## Browser Support
|
|
201
343
|
|
|
202
344
|
These utilities require:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract audio channels from AudioData as separate Float32Array buffers.
|
|
3
|
+
*
|
|
4
|
+
* AudioData can store samples in two formats:
|
|
5
|
+
* - **Planar (f32-planar)**: Each channel is stored separately (most common)
|
|
6
|
+
* - **Interleaved (f32)**: All channels are mixed together (L, R, L, R, L, R...)
|
|
7
|
+
*
|
|
8
|
+
* This function handles both formats automatically and returns an array of Float32Array,
|
|
9
|
+
* where each array represents one channel (typically [left, right] for stereo).
|
|
10
|
+
*
|
|
11
|
+
* @param audioData - The AudioData object to extract channels from
|
|
12
|
+
* @returns Array of Float32Array, one per channel (e.g., [leftChannel, rightChannel])
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Extract channels from decoded audio
|
|
17
|
+
* const decoder = new AudioDecoder({
|
|
18
|
+
* output: (audioData) => {
|
|
19
|
+
* const channels = extractChannels(audioData);
|
|
20
|
+
* const leftChannel = channels[0];
|
|
21
|
+
* const rightChannel = channels[1]; // if stereo
|
|
22
|
+
*
|
|
23
|
+
* // Process audio samples...
|
|
24
|
+
* for (let i = 0; i < leftChannel.length; i++) {
|
|
25
|
+
* leftChannel[i] *= 0.5; // Reduce volume by 50%
|
|
26
|
+
* }
|
|
27
|
+
* },
|
|
28
|
+
* error: (e) => console.error(e)
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* // Mix two audio sources
|
|
35
|
+
* const source1Channels = extractChannels(audioData1);
|
|
36
|
+
* const source2Channels = extractChannels(audioData2);
|
|
37
|
+
*
|
|
38
|
+
* const mixedLeft = new Float32Array(source1Channels[0].length);
|
|
39
|
+
* for (let i = 0; i < mixedLeft.length; i++) {
|
|
40
|
+
* mixedLeft[i] = source1Channels[0][i] + source2Channels[0][i];
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function extractChannels(audioData: AudioData): Float32Array[];
|
|
45
|
+
//# sourceMappingURL=extract-channels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-channels.d.ts","sourceRoot":"","sources":["../../src/audio/extract-channels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,YAAY,EAAE,CA8BpE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function c(e) {
|
|
2
|
+
var s;
|
|
3
|
+
const l = [];
|
|
4
|
+
if ((s = e.format) != null && s.includes("planar"))
|
|
5
|
+
for (let n = 0; n < e.numberOfChannels; n++) {
|
|
6
|
+
const r = new Float32Array(e.numberOfFrames);
|
|
7
|
+
e.copyTo(r, { frameOffset: 0, planeIndex: n }), l.push(r);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
const n = new Float32Array(
|
|
11
|
+
e.numberOfFrames * e.numberOfChannels
|
|
12
|
+
);
|
|
13
|
+
e.copyTo(n, { frameOffset: 0, planeIndex: 0 });
|
|
14
|
+
for (let r = 0; r < e.numberOfChannels; r++) {
|
|
15
|
+
const m = new Float32Array(e.numberOfFrames);
|
|
16
|
+
for (let f = 0; f < e.numberOfFrames; f++)
|
|
17
|
+
m[f] = n[f * e.numberOfChannels + r];
|
|
18
|
+
l.push(m);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return l;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
c as extractChannels
|
|
25
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the actual sample rate from an audio MediaStreamTrack.
|
|
3
|
+
*
|
|
4
|
+
* Firefox doesn't always expose sampleRate in track.getSettings(), so this
|
|
5
|
+
* function creates a temporary AudioContext to determine the actual sample rate.
|
|
6
|
+
*
|
|
7
|
+
* @param track - The audio MediaStreamTrack to get the sample rate from
|
|
8
|
+
* @returns The actual sample rate in Hz
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
13
|
+
* const audioTrack = stream.getAudioTracks()[0];
|
|
14
|
+
* const sampleRate = await getSampleRate(audioTrack);
|
|
15
|
+
* console.log(`Sample rate: ${sampleRate} Hz`);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function getSampleRate(track: MediaStreamTrack): Promise<number>;
|
|
19
|
+
//# sourceMappingURL=get-sample-rate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-sample-rate.d.ts","sourceRoot":"","sources":["../../src/audio/get-sample-rate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB5E"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
async function i(e) {
|
|
2
|
+
if (e.kind !== "audio")
|
|
3
|
+
throw new Error("Track must be an audio track");
|
|
4
|
+
const a = e.getSettings();
|
|
5
|
+
if (a.sampleRate)
|
|
6
|
+
return a.sampleRate;
|
|
7
|
+
const t = new AudioContext(), n = new MediaStreamAudioSourceNode(t, {
|
|
8
|
+
mediaStream: new MediaStream([e])
|
|
9
|
+
}), o = t.sampleRate;
|
|
10
|
+
return n.disconnect(), await t.close(), o;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
i as getSampleRate
|
|
14
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
interface MP3EncoderConfig {
|
|
2
|
+
sampleRate: number;
|
|
3
|
+
bitRate: number;
|
|
4
|
+
channels: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Encode audio to MP3 format using LameJS.
|
|
8
|
+
*
|
|
9
|
+
* This encoder converts AudioData objects to MP3 format. It handles both
|
|
10
|
+
* planar (f32-planar) and interleaved (f32) audio formats automatically.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Create encoder
|
|
15
|
+
* const encoder = new MP3Encoder({
|
|
16
|
+
* sampleRate: 48000,
|
|
17
|
+
* bitRate: 192, // 192 kbps
|
|
18
|
+
* channels: 2 // Stereo
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Encode AudioData objects
|
|
22
|
+
* for (const audioData of audioDataArray) {
|
|
23
|
+
* const mp3Chunk = encoder.processBatch(audioData);
|
|
24
|
+
* encoder.encodedData.push(mp3Chunk);
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // Get final MP3 file
|
|
28
|
+
* const mp3Blob = encoder.finish();
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* This encoder uses LameJS for encoding. For production use, consider using
|
|
33
|
+
* the native AudioEncoder API with opus codec, or a server-side encoder.
|
|
34
|
+
*/
|
|
35
|
+
declare class MP3Encoder {
|
|
36
|
+
private mp3encoder;
|
|
37
|
+
private config;
|
|
38
|
+
encodedData: Uint8Array[];
|
|
39
|
+
/**
|
|
40
|
+
* Create a new MP3Encoder.
|
|
41
|
+
*
|
|
42
|
+
* @param config - Encoder configuration
|
|
43
|
+
* @param config.sampleRate - Audio sample rate in Hz (e.g., 48000)
|
|
44
|
+
* @param config.bitRate - MP3 bitrate in kbps (e.g., 192)
|
|
45
|
+
* @param config.channels - Number of audio channels (1 = mono, 2 = stereo)
|
|
46
|
+
*/
|
|
47
|
+
constructor(config: MP3EncoderConfig);
|
|
48
|
+
private convertAudioDataToInt16;
|
|
49
|
+
/**
|
|
50
|
+
* Encode a single AudioData object to MP3.
|
|
51
|
+
*
|
|
52
|
+
* @param audioData - The AudioData object to encode
|
|
53
|
+
* @returns Encoded MP3 data as Uint8Array (add this to encodedData array)
|
|
54
|
+
*/
|
|
55
|
+
processBatch(audioData: AudioData): Uint8Array;
|
|
56
|
+
/**
|
|
57
|
+
* Finalize encoding and get the complete MP3 file as a Blob.
|
|
58
|
+
*
|
|
59
|
+
* This method flushes any remaining data and combines all encoded chunks
|
|
60
|
+
* into a single MP3 file blob.
|
|
61
|
+
*
|
|
62
|
+
* @returns Complete MP3 file as a Blob
|
|
63
|
+
*/
|
|
64
|
+
finish(): Blob;
|
|
65
|
+
getEncodedSize(): number;
|
|
66
|
+
}
|
|
67
|
+
export interface MP3DecoderOutput {
|
|
68
|
+
channels: Float32Array[];
|
|
69
|
+
sampleRate: number;
|
|
70
|
+
numberOfChannels: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decode MP3 files to raw PCM samples or AudioData objects.
|
|
74
|
+
*
|
|
75
|
+
* This decoder uses mpg123-decoder (WebAssembly) to decode MP3 files.
|
|
76
|
+
* It can output either raw Float32Array samples or AudioData objects
|
|
77
|
+
* ready for use with WebCodecs APIs.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // Decode to raw samples
|
|
82
|
+
* const decoder = new MP3Decoder();
|
|
83
|
+
* await decoder.initialize();
|
|
84
|
+
*
|
|
85
|
+
* const mp3Buffer = await file.arrayBuffer();
|
|
86
|
+
* const { channels, sampleRate, numberOfChannels } = await decoder.toSamples(mp3Buffer);
|
|
87
|
+
*
|
|
88
|
+
* // channels[0] = left channel (Float32Array)
|
|
89
|
+
* // channels[1] = right channel (Float32Array) if stereo
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // Decode to AudioData objects
|
|
95
|
+
* const decoder = new MP3Decoder();
|
|
96
|
+
* await decoder.initialize();
|
|
97
|
+
*
|
|
98
|
+
* const mp3Buffer = await file.arrayBuffer();
|
|
99
|
+
* const audioDataArray = await decoder.toAudioData(mp3Buffer);
|
|
100
|
+
*
|
|
101
|
+
* // Use with AudioEncoder or other WebCodecs APIs
|
|
102
|
+
* for (const audioData of audioDataArray) {
|
|
103
|
+
* encoder.encode(audioData);
|
|
104
|
+
* audioData.close();
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* await decoder.destroy();
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
declare class MP3Decoder {
|
|
111
|
+
private decoder;
|
|
112
|
+
private isReady;
|
|
113
|
+
/**
|
|
114
|
+
* Create a new MP3Decoder.
|
|
115
|
+
*
|
|
116
|
+
* The decoder auto-detects sample rate and channel configuration from the MP3 file.
|
|
117
|
+
* Call initialize() before using.
|
|
118
|
+
*/
|
|
119
|
+
constructor();
|
|
120
|
+
/**
|
|
121
|
+
* Initialize the decoder (async)
|
|
122
|
+
*/
|
|
123
|
+
initialize(): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Decode MP3 buffer to raw PCM samples
|
|
126
|
+
* @param mp3Buffer - The MP3 data as ArrayBuffer
|
|
127
|
+
* @returns Promise<{channels: Float32Array[], sampleRate: number, numberOfChannels: number}>
|
|
128
|
+
*/
|
|
129
|
+
toSamples(mp3Buffer: ArrayBuffer): Promise<{
|
|
130
|
+
channels: Float32Array[];
|
|
131
|
+
sampleRate: number;
|
|
132
|
+
numberOfChannels: number;
|
|
133
|
+
}>;
|
|
134
|
+
/**
|
|
135
|
+
* Decode MP3 to AudioData objects.
|
|
136
|
+
* Internally calls decodeMP3ToSamples and converts the Float32Array channels to AudioData.
|
|
137
|
+
*
|
|
138
|
+
* @param mp3Buffer - The MP3 data as ArrayBuffer
|
|
139
|
+
* @returns Promise<AudioData[]> - Array of AudioData objects
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const decoder = new MP3Decoder();
|
|
144
|
+
* await decoder.initialize();
|
|
145
|
+
* const mp3Buffer = await file.arrayBuffer();
|
|
146
|
+
* const audioDataArray = await decoder.decode(mp3Buffer);
|
|
147
|
+
*
|
|
148
|
+
* // Use with AudioEncoder or other WebCodecs APIs
|
|
149
|
+
* for (const audioData of audioDataArray) {
|
|
150
|
+
* encoder.encode(audioData);
|
|
151
|
+
* audioData.close();
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
toAudioData(mp3Buffer: ArrayBuffer): Promise<AudioData[]>;
|
|
156
|
+
/**
|
|
157
|
+
* Clean up decoder resources
|
|
158
|
+
*/
|
|
159
|
+
destroy(): Promise<void>;
|
|
160
|
+
}
|
|
161
|
+
export { MP3Encoder, MP3Decoder };
|
|
162
|
+
//# sourceMappingURL=mp3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mp3.d.ts","sourceRoot":"","sources":["../../src/audio/mp3.ts"],"names":[],"mappings":"AAmBA,UAAU,gBAAgB;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,cAAM,UAAU;IACZ,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,MAAM,CAAmB;IACjC,WAAW,EAAE,UAAU,EAAE,CAAC;IAE1B;;;;;;;OAOG;gBACS,MAAM,EAAE,gBAAgB;IAapC,OAAO,CAAC,uBAAuB;IAmB/B;;;;;OAKG;IACH,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU;IAwB9C;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;IAuBd,cAAc,IAAI,MAAM;CAG3B;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,cAAM,UAAU;IACZ,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,OAAO,CAAkB;IAEjC;;;;;OAKG;;IAKH;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAajC;;;;OAIG;IACG,SAAS,CAAC,SAAS,EAAE,WAAW,GAAG,OAAO,CAAC;QAC7C,QAAQ,EAAE,YAAY,EAAE,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,gBAAgB,EAAE,MAAM,CAAA;KAC3B,CAAC;IAmCF;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,WAAW,CAAC,SAAS,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAiD/D;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAOjC;AAED,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import g from "lamejs";
|
|
2
|
+
import w from "lamejs/src/js/MPEGMode";
|
|
3
|
+
import D from "lamejs/src/js/Lame";
|
|
4
|
+
import M from "lamejs/src/js/BitStream";
|
|
5
|
+
import { MPEGDecoderWebWorker as b } from "mpg123-decoder";
|
|
6
|
+
import { extractChannels as A } from "./extract-channels.js";
|
|
7
|
+
globalThis.MPEGMode = w;
|
|
8
|
+
globalThis.Lame = D;
|
|
9
|
+
globalThis.BitStream = M;
|
|
10
|
+
class E {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new MP3Encoder.
|
|
13
|
+
*
|
|
14
|
+
* @param config - Encoder configuration
|
|
15
|
+
* @param config.sampleRate - Audio sample rate in Hz (e.g., 48000)
|
|
16
|
+
* @param config.bitRate - MP3 bitrate in kbps (e.g., 192)
|
|
17
|
+
* @param config.channels - Number of audio channels (1 = mono, 2 = stereo)
|
|
18
|
+
*/
|
|
19
|
+
constructor(e) {
|
|
20
|
+
this.config = e, this.mp3encoder = new g.Mp3Encoder(
|
|
21
|
+
e.channels,
|
|
22
|
+
e.sampleRate,
|
|
23
|
+
e.bitRate
|
|
24
|
+
), this.encodedData = [];
|
|
25
|
+
}
|
|
26
|
+
// Convert AudioData to interleaved Int16 samples
|
|
27
|
+
convertAudioDataToInt16(e) {
|
|
28
|
+
const t = e.numberOfChannels, a = e.numberOfFrames, r = A(e), o = new Int16Array(a * t);
|
|
29
|
+
for (let n = 0; n < a; n++)
|
|
30
|
+
for (let s = 0; s < t; s++) {
|
|
31
|
+
const i = Math.max(-1, Math.min(1, r[s][n]));
|
|
32
|
+
o[n * t + s] = i * 32767;
|
|
33
|
+
}
|
|
34
|
+
return o;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Encode a single AudioData object to MP3.
|
|
38
|
+
*
|
|
39
|
+
* @param audioData - The AudioData object to encode
|
|
40
|
+
* @returns Encoded MP3 data as Uint8Array (add this to encodedData array)
|
|
41
|
+
*/
|
|
42
|
+
processBatch(e) {
|
|
43
|
+
const t = this.convertAudioDataToInt16(e);
|
|
44
|
+
let a;
|
|
45
|
+
if (this.config.channels === 2) {
|
|
46
|
+
const r = new Int16Array(t.length / 2), o = new Int16Array(t.length / 2);
|
|
47
|
+
for (let n = 0; n < t.length / 2; n++)
|
|
48
|
+
r[n] = t[n * 2], o[n] = t[n * 2 + 1];
|
|
49
|
+
a = this.mp3encoder.encodeBuffer(r, o);
|
|
50
|
+
} else
|
|
51
|
+
a = this.mp3encoder.encodeBuffer(t);
|
|
52
|
+
return a;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Finalize encoding and get the complete MP3 file as a Blob.
|
|
56
|
+
*
|
|
57
|
+
* This method flushes any remaining data and combines all encoded chunks
|
|
58
|
+
* into a single MP3 file blob.
|
|
59
|
+
*
|
|
60
|
+
* @returns Complete MP3 file as a Blob
|
|
61
|
+
*/
|
|
62
|
+
finish() {
|
|
63
|
+
const e = this.mp3encoder.flush();
|
|
64
|
+
e.length > 0 && this.encodedData.push(e);
|
|
65
|
+
const t = this.encodedData.reduce((o, n) => o + n.length, 0), a = new Uint8Array(t);
|
|
66
|
+
let r = 0;
|
|
67
|
+
for (const o of this.encodedData)
|
|
68
|
+
a.set(o, r), r += o.length;
|
|
69
|
+
return this.encodedData = [], new Blob([a], { type: "audio/mp3" });
|
|
70
|
+
}
|
|
71
|
+
// Optional: Get current size of encoded data
|
|
72
|
+
getEncodedSize() {
|
|
73
|
+
return this.encodedData.reduce((e, t) => e + t.length, 0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
class I {
|
|
77
|
+
/**
|
|
78
|
+
* Create a new MP3Decoder.
|
|
79
|
+
*
|
|
80
|
+
* The decoder auto-detects sample rate and channel configuration from the MP3 file.
|
|
81
|
+
* Call initialize() before using.
|
|
82
|
+
*/
|
|
83
|
+
constructor() {
|
|
84
|
+
this.isReady = !1;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Initialize the decoder (async)
|
|
88
|
+
*/
|
|
89
|
+
async initialize() {
|
|
90
|
+
if (!this.isReady)
|
|
91
|
+
try {
|
|
92
|
+
this.decoder = new b(), await this.decoder.ready, this.isReady = !0;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
throw console.error("Failed to initialize MP3 decoder:", e), e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Decode MP3 buffer to raw PCM samples
|
|
99
|
+
* @param mp3Buffer - The MP3 data as ArrayBuffer
|
|
100
|
+
* @returns Promise<{channels: Float32Array[], sampleRate: number, numberOfChannels: number}>
|
|
101
|
+
*/
|
|
102
|
+
async toSamples(e) {
|
|
103
|
+
this.isReady || await this.initialize();
|
|
104
|
+
try {
|
|
105
|
+
const t = await this.decoder.decode(new Uint8Array(e)), { channelData: a, sampleRate: r } = t, o = a.length;
|
|
106
|
+
return {
|
|
107
|
+
channels: a.map((s) => {
|
|
108
|
+
const i = new Float32Array(s.length);
|
|
109
|
+
for (let c = 0; c < s.length; c++)
|
|
110
|
+
i[c] = s[c];
|
|
111
|
+
return i;
|
|
112
|
+
}),
|
|
113
|
+
sampleRate: r,
|
|
114
|
+
numberOfChannels: o
|
|
115
|
+
};
|
|
116
|
+
} catch (t) {
|
|
117
|
+
throw console.error("Failed to decode MP3:", t), t;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Decode MP3 to AudioData objects.
|
|
122
|
+
* Internally calls decodeMP3ToSamples and converts the Float32Array channels to AudioData.
|
|
123
|
+
*
|
|
124
|
+
* @param mp3Buffer - The MP3 data as ArrayBuffer
|
|
125
|
+
* @returns Promise<AudioData[]> - Array of AudioData objects
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const decoder = new MP3Decoder();
|
|
130
|
+
* await decoder.initialize();
|
|
131
|
+
* const mp3Buffer = await file.arrayBuffer();
|
|
132
|
+
* const audioDataArray = await decoder.decode(mp3Buffer);
|
|
133
|
+
*
|
|
134
|
+
* // Use with AudioEncoder or other WebCodecs APIs
|
|
135
|
+
* for (const audioData of audioDataArray) {
|
|
136
|
+
* encoder.encode(audioData);
|
|
137
|
+
* audioData.close();
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
async toAudioData(e) {
|
|
142
|
+
const { channels: t, sampleRate: a, numberOfChannels: r } = await this.toSamples(e), o = 1024, n = t[0].length, s = [];
|
|
143
|
+
console.log("Samples", n);
|
|
144
|
+
for (let i = 0; i < n; i += o) {
|
|
145
|
+
const c = n - i, h = Math.min(o, c), u = t.map(
|
|
146
|
+
(l) => l.slice(i, i + h)
|
|
147
|
+
), m = new Float32Array(h * r);
|
|
148
|
+
for (let l = 0; l < h; l++)
|
|
149
|
+
for (let d = 0; d < r; d++)
|
|
150
|
+
m[l * r + d] = u[d][l];
|
|
151
|
+
const p = i / a * 1e6, y = new AudioData({
|
|
152
|
+
format: "f32",
|
|
153
|
+
sampleRate: a,
|
|
154
|
+
numberOfFrames: h,
|
|
155
|
+
numberOfChannels: r,
|
|
156
|
+
timestamp: Math.round(p),
|
|
157
|
+
data: m
|
|
158
|
+
});
|
|
159
|
+
s.push(y);
|
|
160
|
+
}
|
|
161
|
+
return s;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Clean up decoder resources
|
|
165
|
+
*/
|
|
166
|
+
async destroy() {
|
|
167
|
+
this.decoder && (await this.decoder.free(), this.decoder = null, this.isReady = !1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
I as MP3Decoder,
|
|
172
|
+
E as MP3Encoder
|
|
173
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { EncodedVideoPacketSource, EncodedAudioPacketSource, BufferTarget, Output } from 'mediabunny';
|
|
2
|
+
/**
|
|
3
|
+
* Simple muxer for creating MP4 files from encoded video or audio chunks.
|
|
4
|
+
*
|
|
5
|
+
* This class wraps MediaBunny's muxing functionality to provide a simplified
|
|
6
|
+
* API for demos and learning purposes. It handles a single video or audio track.
|
|
7
|
+
*
|
|
8
|
+
* **⚠️ Demo/Learning Only**: This utility is intended for demos and learning.
|
|
9
|
+
* For production use, please use MediaBunny directly: https://mediabunny.dev/
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Create muxer for video
|
|
14
|
+
* const muxer = new ExampleMuxer('video');
|
|
15
|
+
*
|
|
16
|
+
* // In VideoEncoder output callback:
|
|
17
|
+
* encoder.configure({
|
|
18
|
+
* output: (chunk, metadata) => {
|
|
19
|
+
* muxer.addChunk(chunk, metadata);
|
|
20
|
+
* },
|
|
21
|
+
* error: (e) => console.error(e)
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // ... encode frames ...
|
|
25
|
+
* await encoder.flush();
|
|
26
|
+
*
|
|
27
|
+
* // Get final MP4
|
|
28
|
+
* const mp4Buffer = await muxer.finish();
|
|
29
|
+
* const blob = new Blob([mp4Buffer], { type: 'video/mp4' });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* // Create muxer for audio
|
|
35
|
+
* const muxer = new ExampleMuxer('audio');
|
|
36
|
+
*
|
|
37
|
+
* // In AudioEncoder output callback:
|
|
38
|
+
* encoder.configure({
|
|
39
|
+
* output: (chunk) => {
|
|
40
|
+
* muxer.addChunk(chunk);
|
|
41
|
+
* },
|
|
42
|
+
* error: (e) => console.error(e)
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare class ExampleMuxer {
|
|
47
|
+
type: 'audio' | 'video';
|
|
48
|
+
output: Output;
|
|
49
|
+
source: EncodedVideoPacketSource | EncodedAudioPacketSource;
|
|
50
|
+
started: boolean;
|
|
51
|
+
target: BufferTarget;
|
|
52
|
+
/**
|
|
53
|
+
* Create a new ExampleMuxer.
|
|
54
|
+
*
|
|
55
|
+
* @param type - Track type: 'video' (default) or 'audio'
|
|
56
|
+
*/
|
|
57
|
+
constructor(type?: 'audio' | 'video');
|
|
58
|
+
/**
|
|
59
|
+
* Add an encoded chunk to the MP4 file.
|
|
60
|
+
*
|
|
61
|
+
* @param chunk - EncodedVideoChunk or EncodedAudioChunk from encoder output
|
|
62
|
+
* @param meta - Optional metadata from encoder (for video, contains decoderConfig)
|
|
63
|
+
*/
|
|
64
|
+
addChunk(chunk: EncodedAudioChunk | EncodedVideoChunk, meta?: any): void;
|
|
65
|
+
/**
|
|
66
|
+
* Finalize the MP4 file and return the complete buffer.
|
|
67
|
+
*
|
|
68
|
+
* Call this after all chunks have been added.
|
|
69
|
+
*
|
|
70
|
+
* @returns Complete MP4 file as ArrayBuffer
|
|
71
|
+
*/
|
|
72
|
+
finish(): Promise<ArrayBuffer>;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=example-muxer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"example-muxer.d.ts","sourceRoot":"","sources":["../../src/demux/example-muxer.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,wBAAwB,EACxB,wBAAwB,EACxB,YAAY,EAEZ,MAAM,EACP,MAAM,YAAY,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,YAAY;IAErB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,wBAAwB,GAAG,wBAAwB,CAAA;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,YAAY,CAAA;IAEpB;;;;OAIG;gBACS,IAAI,CAAC,EAAE,OAAO,GAAC,OAAO;IAyBlC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,EAAE,IAAI,CAAC,EAAE,GAAG;IASjE;;;;;;OAMG;IACG,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC;CAYvC"}
|