mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Make FLAC encoder bit depth depend on input sample format, add sampleFormat audio transformation option, fix incorrect bitrate validation for FLAC (closes #357)
This commit is contained in:
+14
-8
@@ -3,10 +3,12 @@
|
|||||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||||
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
|
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
|
||||||
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
|
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
|
||||||
|
<script src="../packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.js"></script>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
//MediabunnyMp3Encoder.registerMp3Encoder();
|
//MediabunnyMp3Encoder.registerMp3Encoder();
|
||||||
MediabunnyAc3.registerAc3Decoder();
|
MediabunnyAc3.registerAc3Decoder();
|
||||||
|
MediabunnyFlacEncoder.registerFlacEncoder();
|
||||||
|
|
||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement('input');
|
||||||
fileInput.type = 'file';
|
fileInput.type = 'file';
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
chunked: true,
|
chunked: true,
|
||||||
chunkSize: 2**20
|
chunkSize: 2**20
|
||||||
});
|
});
|
||||||
const outputFormat = new Mediabunny.WavOutputFormat();
|
const outputFormat = new Mediabunny.FlacOutputFormat();
|
||||||
|
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.textContent = 'Capturing...';
|
p.textContent = 'Capturing...';
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
const tracks = [];
|
const tracks = [];
|
||||||
let start = 0;
|
let start = 0;
|
||||||
|
|
||||||
if (true) {
|
if (false) {
|
||||||
input = new Mediabunny.Input({
|
input = new Mediabunny.Input({
|
||||||
source: new Mediabunny.UrlSource('http://localhost:8000/index.m3u8'),
|
source: new Mediabunny.UrlSource('http://localhost:8000/index.m3u8'),
|
||||||
formats: Mediabunny.ALL_FORMATS,
|
formats: Mediabunny.ALL_FORMATS,
|
||||||
@@ -85,15 +87,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryTrack = await input.getPrimaryAudioTrack();
|
//const primaryTrack = await input.getPrimaryAudioTrack();
|
||||||
const startTime = await primaryTrack.getFirstTimestamp();
|
//const startTime = await primaryTrack.getFirstTimestamp();
|
||||||
console.log(startTime)
|
//console.log(startTime)
|
||||||
|
|
||||||
let ctx = null;
|
let ctx = null;
|
||||||
let conversion = await Mediabunny.Conversion.init({
|
let conversion = await Mediabunny.Conversion.init({
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
audio: (track) => ({ discard: track.number !== primaryTrack.number }),
|
audio: {
|
||||||
|
//forceTranscode: true,
|
||||||
|
//sampleFormat: 's16',
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
video: {
|
video: {
|
||||||
discard: true,
|
discard: true,
|
||||||
@@ -139,11 +144,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
trim: {
|
trim: {
|
||||||
start: startTime,
|
//start: startTime,
|
||||||
end: startTime + 2,
|
//end: startTime + 2,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//console.log(conversion);
|
//console.log(conversion);
|
||||||
|
console.log(conversion.discardedTracks);
|
||||||
|
|
||||||
let progress = 0;
|
let progress = 0;
|
||||||
conversion.onProgress = newProgress => progress = newProgress;
|
conversion.onProgress = newProgress => progress = newProgress;
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ type ConversionAudioOptions = {
|
|||||||
bitrate?: number | Quality;
|
bitrate?: number | Quality;
|
||||||
numberOfChannels?: number;
|
numberOfChannels?: number;
|
||||||
sampleRate?: number;
|
sampleRate?: number;
|
||||||
|
sampleFormat?: 'u8' | 's16' | 's32' | 'f32';
|
||||||
forceTranscode?: boolean;
|
forceTranscode?: boolean;
|
||||||
process?: (sample: AudioSample) => MaybePromise<
|
process?: (sample: AudioSample) => MaybePromise<
|
||||||
AudioSample | AudioSample[] | null
|
AudioSample | AudioSample[] | null
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default tseslint.config(
|
|||||||
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
||||||
'@typescript-eslint/no-unsafe-unary-minus': 'off',
|
'@typescript-eslint/no-unsafe-unary-minus': 'off',
|
||||||
'@typescript-eslint/no-deprecated': 'error',
|
'@typescript-eslint/no-deprecated': 'error',
|
||||||
|
'@typescript-eslint/consistent-type-exports': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
BIN
Binary file not shown.
@@ -12,7 +12,6 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define BITS_PER_SAMPLE 16
|
|
||||||
#define COMPRESSION_LEVEL 5
|
#define COMPRESSION_LEVEL 5
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -23,14 +22,10 @@ typedef struct {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
FLAC__StreamEncoder *encoder;
|
FLAC__StreamEncoder *encoder;
|
||||||
|
|
||||||
// Input buffer for interleaved int16 samples from JS
|
// Input buffer for interleaved int32 samples from JS
|
||||||
int16_t *input_buffer;
|
FLAC__int32 *input_buffer;
|
||||||
int input_buffer_size;
|
int input_buffer_size;
|
||||||
|
|
||||||
// Widened to int32 for libFLAC
|
|
||||||
FLAC__int32 *int32_buffer;
|
|
||||||
int int32_buffer_size;
|
|
||||||
|
|
||||||
// Contiguous output buffer for encoded frame data
|
// Contiguous output buffer for encoded frame data
|
||||||
uint8_t *output_buffer;
|
uint8_t *output_buffer;
|
||||||
int output_size;
|
int output_size;
|
||||||
@@ -48,6 +43,7 @@ typedef struct {
|
|||||||
bool header_done;
|
bool header_done;
|
||||||
|
|
||||||
int channels;
|
int channels;
|
||||||
|
int bits_per_sample;
|
||||||
} EncoderContext;
|
} EncoderContext;
|
||||||
|
|
||||||
static void ensure_output_capacity(EncoderContext *ctx, int needed) {
|
static void ensure_output_capacity(EncoderContext *ctx, int needed) {
|
||||||
@@ -120,13 +116,14 @@ static void reset_output(EncoderContext *ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
EMSCRIPTEN_KEEPALIVE
|
||||||
int init_encoder(int channels, int sample_rate) {
|
int init_encoder(int channels, int sample_rate, int bits_per_sample) {
|
||||||
EncoderContext *ctx = calloc(1, sizeof(EncoderContext));
|
EncoderContext *ctx = calloc(1, sizeof(EncoderContext));
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx->channels = channels;
|
ctx->channels = channels;
|
||||||
|
ctx->bits_per_sample = bits_per_sample;
|
||||||
|
|
||||||
ctx->encoder = FLAC__stream_encoder_new();
|
ctx->encoder = FLAC__stream_encoder_new();
|
||||||
if (!ctx->encoder) {
|
if (!ctx->encoder) {
|
||||||
@@ -136,7 +133,7 @@ int init_encoder(int channels, int sample_rate) {
|
|||||||
|
|
||||||
FLAC__stream_encoder_set_channels(ctx->encoder, channels);
|
FLAC__stream_encoder_set_channels(ctx->encoder, channels);
|
||||||
FLAC__stream_encoder_set_sample_rate(ctx->encoder, sample_rate);
|
FLAC__stream_encoder_set_sample_rate(ctx->encoder, sample_rate);
|
||||||
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, BITS_PER_SAMPLE);
|
FLAC__stream_encoder_set_bits_per_sample(ctx->encoder, bits_per_sample);
|
||||||
FLAC__stream_encoder_set_compression_level(ctx->encoder, COMPRESSION_LEVEL);
|
FLAC__stream_encoder_set_compression_level(ctx->encoder, COMPRESSION_LEVEL);
|
||||||
FLAC__stream_encoder_set_verify(ctx->encoder, false);
|
FLAC__stream_encoder_set_verify(ctx->encoder, false);
|
||||||
|
|
||||||
@@ -174,19 +171,15 @@ EMSCRIPTEN_KEEPALIVE
|
|||||||
int send_samples(int ctx_ptr, int num_samples) {
|
int send_samples(int ctx_ptr, int num_samples) {
|
||||||
EncoderContext *ctx = (EncoderContext *)ctx_ptr;
|
EncoderContext *ctx = (EncoderContext *)ctx_ptr;
|
||||||
|
|
||||||
// Widen int16 to int32 for libFLAC
|
|
||||||
int total = num_samples * ctx->channels;
|
int total = num_samples * ctx->channels;
|
||||||
if (total > ctx->int32_buffer_size) {
|
int shift = 32 - ctx->bits_per_sample;
|
||||||
ctx->int32_buffer = realloc(ctx->int32_buffer, total * sizeof(FLAC__int32));
|
|
||||||
ctx->int32_buffer_size = total;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
ctx->int32_buffer[i] = ctx->input_buffer[i];
|
ctx->input_buffer[i] >>= shift;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_output(ctx);
|
reset_output(ctx);
|
||||||
|
|
||||||
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->int32_buffer, num_samples);
|
FLAC__bool ok = FLAC__stream_encoder_process_interleaved(ctx->encoder, ctx->input_buffer, num_samples);
|
||||||
return ok ? 0 : -1;
|
return ok ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type ExtendedEmscriptenModule = EmscriptenModule & {
|
|||||||
let module: ExtendedEmscriptenModule;
|
let module: ExtendedEmscriptenModule;
|
||||||
let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
|
let modulePromise: Promise<ExtendedEmscriptenModule> | null = null;
|
||||||
|
|
||||||
let initEncoderFn: (channels: number, sampleRate: number) => number;
|
let initEncoderFn: (channels: number, sampleRate: number, bitsPerSample: number) => number;
|
||||||
let getEncodeInputPtr: (ctx: number, size: number) => number;
|
let getEncodeInputPtr: (ctx: number, size: number) => number;
|
||||||
let sendSamplesFn: (ctx: number, numSamples: number) => number;
|
let sendSamplesFn: (ctx: number, numSamples: number) => number;
|
||||||
let getOutputData: (ctx: number) => number;
|
let getOutputData: (ctx: number) => number;
|
||||||
@@ -37,7 +37,7 @@ const ensureModule = async () => {
|
|||||||
module = await modulePromise;
|
module = await modulePromise;
|
||||||
modulePromise = null;
|
modulePromise = null;
|
||||||
|
|
||||||
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number']);
|
initEncoderFn = module.cwrap('init_encoder', 'number', ['number', 'number', 'number']);
|
||||||
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
|
getEncodeInputPtr = module.cwrap('get_encode_input_ptr', 'number', ['number', 'number']);
|
||||||
sendSamplesFn = module.cwrap('send_samples', 'number', ['number', 'number']);
|
sendSamplesFn = module.cwrap('send_samples', 'number', ['number', 'number']);
|
||||||
getOutputData = module.cwrap('get_output_data', 'number', ['number']);
|
getOutputData = module.cwrap('get_output_data', 'number', ['number']);
|
||||||
@@ -50,10 +50,10 @@ const ensureModule = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initEncoder = async (numberOfChannels: number, sampleRate: number) => {
|
const initEncoder = async (numberOfChannels: number, sampleRate: number, bitsPerSample: 16 | 24) => {
|
||||||
await ensureModule();
|
await ensureModule();
|
||||||
|
|
||||||
const ctx = initEncoderFn(numberOfChannels, sampleRate);
|
const ctx = initEncoderFn(numberOfChannels, sampleRate, bitsPerSample);
|
||||||
if (ctx === 0) {
|
if (ctx === 0) {
|
||||||
throw new Error('Failed to initialize FLAC encoder.');
|
throw new Error('Failed to initialize FLAC encoder.');
|
||||||
}
|
}
|
||||||
@@ -121,6 +121,7 @@ const onMessage = (data: { id: number; command: WorkerCommand }) => {
|
|||||||
const { ctx, header } = await initEncoder(
|
const { ctx, header } = await initEncoder(
|
||||||
command.data.numberOfChannels,
|
command.data.numberOfChannels,
|
||||||
command.data.sampleRate,
|
command.data.sampleRate,
|
||||||
|
command.data.bitsPerSample,
|
||||||
);
|
);
|
||||||
result = { type: command.type, ctx, header };
|
result = { type: command.type, ctx, header };
|
||||||
transferables.push(header);
|
transferables.push(header);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class FlacEncoder extends CustomAudioEncoder {
|
|||||||
reject: (reason?: unknown) => void;
|
reject: (reason?: unknown) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
private ctx = 0;
|
private ctx: number | null = null;
|
||||||
private chunkMetadata: EncodedAudioChunkMetadata = {};
|
private chunkMetadata: EncodedAudioChunkMetadata = {};
|
||||||
private description: Uint8Array | null = null;
|
private description: Uint8Array | null = null;
|
||||||
private nextTimestampInSamples: number | null = null;
|
private nextTimestampInSamples: number | null = null;
|
||||||
@@ -65,19 +65,6 @@ class FlacEncoder extends CustomAudioEncoder {
|
|||||||
};
|
};
|
||||||
nodeWorker.on('message', onMessage);
|
nodeWorker.on('message', onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.sendCommand({
|
|
||||||
type: 'init',
|
|
||||||
data: {
|
|
||||||
numberOfChannels: this.config.numberOfChannels,
|
|
||||||
sampleRate: this.config.sampleRate,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ctx = result.ctx;
|
|
||||||
|
|
||||||
this.description = new Uint8Array(result.header);
|
|
||||||
this.resetInternalState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetInternalState() {
|
private resetInternalState() {
|
||||||
@@ -94,15 +81,50 @@ class FlacEncoder extends CustomAudioEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async encode(audioSample: AudioSample) {
|
async encode(audioSample: AudioSample) {
|
||||||
|
if (this.ctx === null) {
|
||||||
|
// This is the first sample, let's do some init
|
||||||
|
|
||||||
|
let bitsPerSample: 16 | 24;
|
||||||
|
switch (audioSample.format) {
|
||||||
|
case 'u8':
|
||||||
|
case 'u8-planar':
|
||||||
|
case 's16':
|
||||||
|
case 's16-planar':
|
||||||
|
bitsPerSample = 16;
|
||||||
|
break;
|
||||||
|
case 's32':
|
||||||
|
case 's32-planar':
|
||||||
|
case 'f32':
|
||||||
|
case 'f32-planar':
|
||||||
|
bitsPerSample = 24;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertNever(audioSample.format);
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.sendCommand({
|
||||||
|
type: 'init',
|
||||||
|
data: {
|
||||||
|
numberOfChannels: this.config.numberOfChannels,
|
||||||
|
sampleRate: this.config.sampleRate,
|
||||||
|
bitsPerSample,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx = result.ctx;
|
||||||
|
this.description = new Uint8Array(result.header);
|
||||||
|
this.resetInternalState();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.nextTimestampInSamples === null) {
|
if (this.nextTimestampInSamples === null) {
|
||||||
this.nextTimestampInSamples = Math.round(audioSample.timestamp * this.config.sampleRate);
|
this.nextTimestampInSamples = Math.round(audioSample.timestamp * this.config.sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalBytes = audioSample.allocationSize({ format: 's16', planeIndex: 0 });
|
const totalBytes = audioSample.allocationSize({ format: 's32', planeIndex: 0 });
|
||||||
const audioBytes = new Uint8Array(totalBytes);
|
const audioData = new ArrayBuffer(totalBytes);
|
||||||
audioSample.copyTo(audioBytes, { format: 's16', planeIndex: 0 });
|
audioSample.copyTo(audioData, { format: 's32', planeIndex: 0 });
|
||||||
|
|
||||||
const audioData = audioBytes.buffer;
|
|
||||||
const result = await this.sendCommand({
|
const result = await this.sendCommand({
|
||||||
type: 'encode',
|
type: 'encode',
|
||||||
data: {
|
data: {
|
||||||
@@ -116,6 +138,10 @@ class FlacEncoder extends CustomAudioEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async flush() {
|
async flush() {
|
||||||
|
if (this.ctx === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } });
|
const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } });
|
||||||
this.emitPackets(result.packets);
|
this.emitPackets(result.packets);
|
||||||
|
|
||||||
@@ -198,3 +224,8 @@ function assert(x: unknown): asserts x {
|
|||||||
throw new Error('Assertion failed.');
|
throw new Error('Assertion failed.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const assertNever = (x: never) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unexpected value: ${x}`);
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type WorkerCommand = {
|
|||||||
data: {
|
data: {
|
||||||
numberOfChannels: number;
|
numberOfChannels: number;
|
||||||
sampleRate: number;
|
sampleRate: number;
|
||||||
|
bitsPerSample: 16 | 24;
|
||||||
};
|
};
|
||||||
} | {
|
} | {
|
||||||
type: 'encode';
|
type: 'encode';
|
||||||
|
|||||||
+45
-11
@@ -50,7 +50,15 @@ import {
|
|||||||
} from './misc';
|
} from './misc';
|
||||||
import { Output, OutputTrackGroup, TrackType } from './output';
|
import { Output, OutputTrackGroup, TrackType } from './output';
|
||||||
import { Mp4OutputFormat } from './output-format';
|
import { Mp4OutputFormat } from './output-format';
|
||||||
import { AudioSample, clampCropRectangle, CropRectangle, validateCropRectangle, VideoSample } from './sample';
|
import {
|
||||||
|
AudioSample,
|
||||||
|
audioSampleToInterleavedFormat,
|
||||||
|
clampCropRectangle,
|
||||||
|
CropRectangle,
|
||||||
|
toInterleavedAudioFormat,
|
||||||
|
validateCropRectangle,
|
||||||
|
VideoSample,
|
||||||
|
} from './sample';
|
||||||
import { MetadataTags, validateMetadataTags } from './metadata';
|
import { MetadataTags, validateMetadataTags } from './metadata';
|
||||||
import { NullTarget } from './target';
|
import { NullTarget } from './target';
|
||||||
import { AudioResampler } from './resample';
|
import { AudioResampler } from './resample';
|
||||||
@@ -261,6 +269,13 @@ export type ConversionAudioOptions = {
|
|||||||
numberOfChannels?: number;
|
numberOfChannels?: number;
|
||||||
/** The desired sample rate of the output audio, in hertz. */
|
/** The desired sample rate of the output audio, in hertz. */
|
||||||
sampleRate?: number;
|
sampleRate?: number;
|
||||||
|
/**
|
||||||
|
* The desired sample format (and therefore bit depth) of the audio samples before they are passed to the encoder.
|
||||||
|
* Can be used to control bit depth with certain output codecs such as FLAC.
|
||||||
|
*
|
||||||
|
* Setting this field forces audio transcoding.
|
||||||
|
*/
|
||||||
|
sampleFormat?: 'u8' | 's16' | 's32' | 'f32';
|
||||||
/** The desired output audio codec. */
|
/** The desired output audio codec. */
|
||||||
codec?: AudioCodec;
|
codec?: AudioCodec;
|
||||||
/** The desired bitrate of the output audio. */
|
/** The desired bitrate of the output audio. */
|
||||||
@@ -442,6 +457,12 @@ const validateAudioOptions = (audioOptions: ConversionAudioOptions) => {
|
|||||||
) {
|
) {
|
||||||
throw new TypeError('options.audio.sampleRate, when provided, must be a positive integer.');
|
throw new TypeError('options.audio.sampleRate, when provided, must be a positive integer.');
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
audioOptions?.sampleFormat !== undefined
|
||||||
|
&& !['u8', 's16', 's32', 'f32'].includes(audioOptions.sampleFormat)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.audio.sampleFormat, when provided, must be one of: u8, s16, s32, f32.');
|
||||||
|
}
|
||||||
if (audioOptions?.process !== undefined && typeof audioOptions.process !== 'function') {
|
if (audioOptions?.process !== undefined && typeof audioOptions.process !== 'function') {
|
||||||
throw new TypeError('options.audio.process, when provided, must be a function.');
|
throw new TypeError('options.audio.process, when provided, must be a function.');
|
||||||
}
|
}
|
||||||
@@ -1347,7 +1368,7 @@ export class Conversion {
|
|||||||
timestamp: lastCanvasTimestamp! + i / frameRate,
|
timestamp: lastCanvasTimestamp! + i / frameRate,
|
||||||
duration: 1 / frameRate,
|
duration: 1 / frameRate,
|
||||||
});
|
});
|
||||||
await this._registerVideoSample(track, trackOptions, outputTrackId, source, sample);
|
await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
|
||||||
sample.close();
|
sample.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1384,7 +1405,7 @@ export class Conversion {
|
|||||||
timestamp: adjustedSampleTimestamp,
|
timestamp: adjustedSampleTimestamp,
|
||||||
duration: frameRate !== undefined ? 1 / frameRate : duration,
|
duration: frameRate !== undefined ? 1 / frameRate : duration,
|
||||||
});
|
});
|
||||||
await this._registerVideoSample(track, trackOptions, outputTrackId, source, sample);
|
await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
|
||||||
sample.close();
|
sample.close();
|
||||||
|
|
||||||
if (frameRate !== undefined) {
|
if (frameRate !== undefined) {
|
||||||
@@ -1425,7 +1446,7 @@ export class Conversion {
|
|||||||
for (let i = 1; i < frameDifference; i++) {
|
for (let i = 1; i < frameDifference; i++) {
|
||||||
lastSample.setTimestamp(lastSampleTimestamp! + i / frameRate);
|
lastSample.setTimestamp(lastSampleTimestamp! + i / frameRate);
|
||||||
lastSample.setDuration(1 / frameRate);
|
lastSample.setDuration(1 / frameRate);
|
||||||
await this._registerVideoSample(track, trackOptions, outputTrackId, source, lastSample);
|
await this._registerVideoSample(trackOptions, outputTrackId, source, lastSample);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSample.close();
|
lastSample.close();
|
||||||
@@ -1464,7 +1485,7 @@ export class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sample.setTimestamp(adjustedSampleTimestamp);
|
sample.setTimestamp(adjustedSampleTimestamp);
|
||||||
await this._registerVideoSample(track, trackOptions, outputTrackId, source, sample);
|
await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
|
||||||
|
|
||||||
if (frameRate !== undefined) {
|
if (frameRate !== undefined) {
|
||||||
lastSample = sample;
|
lastSample = sample;
|
||||||
@@ -1513,7 +1534,6 @@ export class Conversion {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async _registerVideoSample(
|
async _registerVideoSample(
|
||||||
track: InputVideoTrack,
|
|
||||||
trackOptions: ConversionVideoOptions,
|
trackOptions: ConversionVideoOptions,
|
||||||
outputTrackId: number,
|
outputTrackId: number,
|
||||||
source: VideoSampleSource,
|
source: VideoSampleSource,
|
||||||
@@ -1609,6 +1629,7 @@ export class Conversion {
|
|||||||
&& audioCodecs.includes(sourceCodec)
|
&& audioCodecs.includes(sourceCodec)
|
||||||
&& (!trackOptions.codec || trackOptions.codec === sourceCodec)
|
&& (!trackOptions.codec || trackOptions.codec === sourceCodec)
|
||||||
&& !trackOptions.process
|
&& !trackOptions.process
|
||||||
|
&& trackOptions.sampleFormat === undefined
|
||||||
) {
|
) {
|
||||||
// Fast path, we can simply copy over the encoded packets
|
// Fast path, we can simply copy over the encoded packets
|
||||||
|
|
||||||
@@ -1745,7 +1766,7 @@ export class Conversion {
|
|||||||
// Offset the timestamp as needed
|
// Offset the timestamp as needed
|
||||||
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
||||||
|
|
||||||
await this._registerAudioSample(track, trackOptions, outputTrackId, source, sample);
|
await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
|
||||||
sample.close();
|
sample.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1778,16 +1799,25 @@ export class Conversion {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async _registerAudioSample(
|
async _registerAudioSample(
|
||||||
track: InputAudioTrack,
|
|
||||||
trackOptions: ConversionAudioOptions,
|
trackOptions: ConversionAudioOptions,
|
||||||
outputTrackId: number,
|
outputTrackId: number,
|
||||||
source: AudioSampleSource,
|
source: AudioSampleSource,
|
||||||
sample: AudioSample,
|
inputSample: AudioSample,
|
||||||
) {
|
) {
|
||||||
if (this._canceled) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sample = inputSample;
|
||||||
|
|
||||||
|
if (
|
||||||
|
trackOptions.sampleFormat !== undefined
|
||||||
|
&& toInterleavedAudioFormat(sample.format) !== trackOptions.sampleFormat
|
||||||
|
) {
|
||||||
|
// Do a sample format conversion
|
||||||
|
sample = audioSampleToInterleavedFormat(sample, trackOptions.sampleFormat);
|
||||||
|
}
|
||||||
|
|
||||||
this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
|
this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
|
||||||
|
|
||||||
let finalSamples: AudioSample[];
|
let finalSamples: AudioSample[];
|
||||||
@@ -1823,8 +1853,12 @@ export class Conversion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (sample !== inputSample) {
|
||||||
|
sample.close();
|
||||||
|
}
|
||||||
|
|
||||||
for (const finalSample of finalSamples) {
|
for (const finalSample of finalSamples) {
|
||||||
if (finalSample !== sample) {
|
if (finalSample !== inputSample) {
|
||||||
finalSample.close();
|
finalSample.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1857,7 +1891,7 @@ export class Conversion {
|
|||||||
onSample: async (sample) => {
|
onSample: async (sample) => {
|
||||||
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
||||||
|
|
||||||
await this._registerAudioSample(track, trackOptions, outputTrackId, source, sample);
|
await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
|
||||||
sample.close();
|
sample.close();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-1
@@ -386,6 +386,11 @@ export type AudioTransformOptions = {
|
|||||||
numberOfChannels?: number;
|
numberOfChannels?: number;
|
||||||
/** The desired output sample rate in hertz to resample to. */
|
/** The desired output sample rate in hertz to resample to. */
|
||||||
sampleRate?: number;
|
sampleRate?: number;
|
||||||
|
/**
|
||||||
|
* The desired sample format (and therefore bit depth) of the audio samples before they are passed to the encoder.
|
||||||
|
* Can be used to control bit depth with certain output codecs such as FLAC.
|
||||||
|
*/
|
||||||
|
sampleFormat?: 'u8' | 's16' | 's32' | 'f32';
|
||||||
/**
|
/**
|
||||||
* Allows for custom user-defined processing of audio samples, e.g. for applying audio effects or timestamp
|
* Allows for custom user-defined processing of audio samples, e.g. for applying audio effects or timestamp
|
||||||
* modifications. Called for each audio sample after resampling and remixing.
|
* modifications. Called for each audio sample after resampling and remixing.
|
||||||
@@ -406,7 +411,7 @@ export const validateAudioEncodingConfig = (config: AudioEncodingConfig) => {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
config.bitrate === undefined
|
config.bitrate === undefined
|
||||||
&& (!(PCM_AUDIO_CODECS as readonly string[]).includes(config.codec) || config.codec === 'flac')
|
&& !((PCM_AUDIO_CODECS as readonly string[]).includes(config.codec) || config.codec === 'flac')
|
||||||
) {
|
) {
|
||||||
throw new TypeError('config.bitrate must be provided for compressed audio codecs.');
|
throw new TypeError('config.bitrate must be provided for compressed audio codecs.');
|
||||||
}
|
}
|
||||||
@@ -433,6 +438,12 @@ export const validateAudioEncodingConfig = (config: AudioEncodingConfig) => {
|
|||||||
) {
|
) {
|
||||||
throw new TypeError('config.transform.sampleRate, when provided, must be a positive integer.');
|
throw new TypeError('config.transform.sampleRate, when provided, must be a positive integer.');
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
config.transform.sampleFormat !== undefined
|
||||||
|
&& !['u8', 's16', 's32', 'f32'].includes(config.transform.sampleFormat)
|
||||||
|
) {
|
||||||
|
throw new TypeError('config.transform.sampleFormat, when provided, must be one of: u8, s16, s32, f32.');
|
||||||
|
}
|
||||||
if (config.transform.process !== undefined && typeof config.transform.process !== 'function') {
|
if (config.transform.process !== undefined && typeof config.transform.process !== 'function') {
|
||||||
throw new TypeError('config.transform.process, when provided, must be a function.');
|
throw new TypeError('config.transform.process, when provided, must be a function.');
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-82
@@ -22,48 +22,48 @@ if ((globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL]) {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Output,
|
Output,
|
||||||
OutputOptions,
|
type OutputOptions,
|
||||||
OutputTrack,
|
OutputTrack,
|
||||||
OutputVideoTrack,
|
OutputVideoTrack,
|
||||||
OutputAudioTrack,
|
OutputAudioTrack,
|
||||||
OutputSubtitleTrack,
|
OutputSubtitleTrack,
|
||||||
OutputTrackGroup,
|
OutputTrackGroup,
|
||||||
BaseTrackMetadata,
|
type BaseTrackMetadata,
|
||||||
VideoTrackMetadata,
|
type VideoTrackMetadata,
|
||||||
AudioTrackMetadata,
|
type AudioTrackMetadata,
|
||||||
SubtitleTrackMetadata,
|
type SubtitleTrackMetadata,
|
||||||
OutputEvents,
|
type OutputEvents,
|
||||||
} from './output';
|
} from './output';
|
||||||
export {
|
export {
|
||||||
OutputFormat,
|
OutputFormat,
|
||||||
AdtsOutputFormat,
|
AdtsOutputFormat,
|
||||||
AdtsOutputFormatOptions,
|
type AdtsOutputFormatOptions,
|
||||||
CmafOutputFormat,
|
CmafOutputFormat,
|
||||||
CmafOutputFormatOptions,
|
type CmafOutputFormatOptions,
|
||||||
FlacOutputFormat,
|
FlacOutputFormat,
|
||||||
FlacOutputFormatOptions,
|
type FlacOutputFormatOptions,
|
||||||
HlsOutputFormat,
|
HlsOutputFormat,
|
||||||
HlsOutputFormatOptions,
|
type HlsOutputFormatOptions,
|
||||||
HlsOutputPlaylistInfo,
|
type HlsOutputPlaylistInfo,
|
||||||
HlsOutputSegmentInfo,
|
type HlsOutputSegmentInfo,
|
||||||
IsobmffOutputFormat,
|
IsobmffOutputFormat,
|
||||||
IsobmffOutputFormatOptions,
|
type IsobmffOutputFormatOptions,
|
||||||
MkvOutputFormat,
|
MkvOutputFormat,
|
||||||
MkvOutputFormatOptions,
|
type MkvOutputFormatOptions,
|
||||||
MovOutputFormat,
|
MovOutputFormat,
|
||||||
Mp3OutputFormat,
|
Mp3OutputFormat,
|
||||||
Mp3OutputFormatOptions,
|
type Mp3OutputFormatOptions,
|
||||||
Mp4OutputFormat,
|
Mp4OutputFormat,
|
||||||
MpegTsOutputFormat,
|
MpegTsOutputFormat,
|
||||||
MpegTsOutputFormatOptions,
|
type MpegTsOutputFormatOptions,
|
||||||
OggOutputFormat,
|
OggOutputFormat,
|
||||||
OggOutputFormatOptions,
|
type OggOutputFormatOptions,
|
||||||
WavOutputFormat,
|
WavOutputFormat,
|
||||||
WavOutputFormatOptions,
|
type WavOutputFormatOptions,
|
||||||
WebMOutputFormat,
|
WebMOutputFormat,
|
||||||
WebMOutputFormatOptions,
|
type WebMOutputFormatOptions,
|
||||||
InclusiveIntegerRange,
|
type InclusiveIntegerRange,
|
||||||
TrackCountLimits,
|
type TrackCountLimits,
|
||||||
} from './output-format';
|
} from './output-format';
|
||||||
export {
|
export {
|
||||||
MediaSource,
|
MediaSource,
|
||||||
@@ -76,17 +76,17 @@ export {
|
|||||||
EncodedAudioPacketSource,
|
EncodedAudioPacketSource,
|
||||||
EncodedVideoPacketSource,
|
EncodedVideoPacketSource,
|
||||||
MediaStreamAudioTrackSource,
|
MediaStreamAudioTrackSource,
|
||||||
MediaStreamAudioTrackSourceOptions,
|
type MediaStreamAudioTrackSourceOptions,
|
||||||
MediaStreamVideoTrackSource,
|
MediaStreamVideoTrackSource,
|
||||||
MediaStreamVideoTrackSourceOptions,
|
type MediaStreamVideoTrackSourceOptions,
|
||||||
TextSubtitleSource,
|
TextSubtitleSource,
|
||||||
VideoSampleSource,
|
VideoSampleSource,
|
||||||
} from './media-source';
|
} from './media-source';
|
||||||
export {
|
export {
|
||||||
MediaCodec,
|
type MediaCodec,
|
||||||
VideoCodec,
|
type VideoCodec,
|
||||||
AudioCodec,
|
type AudioCodec,
|
||||||
SubtitleCodec,
|
type SubtitleCodec,
|
||||||
VIDEO_CODECS,
|
VIDEO_CODECS,
|
||||||
AUDIO_CODECS,
|
AUDIO_CODECS,
|
||||||
PCM_AUDIO_CODECS,
|
PCM_AUDIO_CODECS,
|
||||||
@@ -102,12 +102,12 @@ export {
|
|||||||
getDecodableAudioCodecs,
|
getDecodableAudioCodecs,
|
||||||
} from './decode';
|
} from './decode';
|
||||||
export {
|
export {
|
||||||
VideoEncodingConfig,
|
type VideoEncodingConfig,
|
||||||
VideoEncodingAdditionalOptions,
|
type VideoEncodingAdditionalOptions,
|
||||||
VideoTransformOptions,
|
type VideoTransformOptions,
|
||||||
AudioEncodingConfig,
|
type AudioEncodingConfig,
|
||||||
AudioEncodingAdditionalOptions,
|
type AudioEncodingAdditionalOptions,
|
||||||
AudioTransformOptions,
|
type AudioTransformOptions,
|
||||||
canEncode,
|
canEncode,
|
||||||
canEncodeVideo,
|
canEncodeVideo,
|
||||||
canEncodeAudio,
|
canEncodeAudio,
|
||||||
@@ -128,69 +128,69 @@ export {
|
|||||||
} from './encode';
|
} from './encode';
|
||||||
export {
|
export {
|
||||||
Target,
|
Target,
|
||||||
TargetEvents,
|
type TargetEvents,
|
||||||
TargetRequest,
|
type TargetRequest,
|
||||||
AppendOnlyStreamTarget,
|
AppendOnlyStreamTarget,
|
||||||
BufferTarget,
|
BufferTarget,
|
||||||
BufferTargetOptions,
|
type BufferTargetOptions,
|
||||||
FilePathTarget,
|
FilePathTarget,
|
||||||
FilePathTargetOptions,
|
type FilePathTargetOptions,
|
||||||
NullTarget,
|
NullTarget,
|
||||||
PathedTarget,
|
PathedTarget,
|
||||||
RangedTarget,
|
RangedTarget,
|
||||||
StreamTarget,
|
StreamTarget,
|
||||||
StreamTargetOptions,
|
type StreamTargetOptions,
|
||||||
StreamTargetChunk,
|
type StreamTargetChunk,
|
||||||
} from './target';
|
} from './target';
|
||||||
export {
|
export {
|
||||||
AnyIterable,
|
type AnyIterable,
|
||||||
ConcurrentRunner,
|
ConcurrentRunner,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
EventListenerOptions,
|
type EventListenerOptions,
|
||||||
FilePath,
|
type FilePath,
|
||||||
MaybePromise,
|
type MaybePromise,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
export {
|
export {
|
||||||
PsshBox,
|
type PsshBox,
|
||||||
} from './isobmff/isobmff-misc';
|
} from './isobmff/isobmff-misc';
|
||||||
export {
|
export {
|
||||||
Rational,
|
type Rational,
|
||||||
Rectangle,
|
type Rectangle,
|
||||||
Rotation,
|
type Rotation,
|
||||||
SetOptional,
|
type SetOptional,
|
||||||
SetRequired,
|
type SetRequired,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
export {
|
export {
|
||||||
TrackType,
|
type TrackType,
|
||||||
ALL_TRACK_TYPES,
|
ALL_TRACK_TYPES,
|
||||||
} from './output';
|
} from './output';
|
||||||
export {
|
export {
|
||||||
Source,
|
Source,
|
||||||
SourceEvents,
|
type SourceEvents,
|
||||||
SourceRef,
|
SourceRef,
|
||||||
SourceRequest,
|
type SourceRequest,
|
||||||
BlobSource,
|
BlobSource,
|
||||||
BlobSourceOptions,
|
type BlobSourceOptions,
|
||||||
BufferSource,
|
BufferSource,
|
||||||
CustomPathedSource,
|
CustomPathedSource,
|
||||||
FilePathSource,
|
FilePathSource,
|
||||||
FilePathSourceOptions,
|
type FilePathSourceOptions,
|
||||||
PathedSource,
|
PathedSource,
|
||||||
StreamSource,
|
StreamSource,
|
||||||
StreamSourceOptions,
|
type StreamSourceOptions,
|
||||||
RangedSource,
|
RangedSource,
|
||||||
ReadableStreamSource,
|
ReadableStreamSource,
|
||||||
ReadableStreamSourceOptions,
|
type ReadableStreamSourceOptions,
|
||||||
UrlSource,
|
UrlSource,
|
||||||
UrlSourceOptions,
|
type UrlSourceOptions,
|
||||||
} from './source';
|
} from './source';
|
||||||
export {
|
export {
|
||||||
InputFormat,
|
InputFormat,
|
||||||
InputFormatOptions,
|
type InputFormatOptions,
|
||||||
AdtsInputFormat,
|
AdtsInputFormat,
|
||||||
FlacInputFormat,
|
FlacInputFormat,
|
||||||
IsobmffInputFormat,
|
IsobmffInputFormat,
|
||||||
IsobmffInputFormatOptions,
|
type IsobmffInputFormatOptions,
|
||||||
HlsInputFormat,
|
HlsInputFormat,
|
||||||
MatroskaInputFormat,
|
MatroskaInputFormat,
|
||||||
Mp3InputFormat,
|
Mp3InputFormat,
|
||||||
@@ -216,38 +216,38 @@ export {
|
|||||||
} from './input-format';
|
} from './input-format';
|
||||||
export {
|
export {
|
||||||
Input,
|
Input,
|
||||||
InputOptions,
|
type InputOptions,
|
||||||
InputEvents,
|
type InputEvents,
|
||||||
InputDisposedError,
|
InputDisposedError,
|
||||||
UnsupportedInputFormatError,
|
UnsupportedInputFormatError,
|
||||||
} from './input';
|
} from './input';
|
||||||
export {
|
export {
|
||||||
DurationMetadataRequestOptions,
|
type DurationMetadataRequestOptions,
|
||||||
} from './demuxer';
|
} from './demuxer';
|
||||||
export {
|
export {
|
||||||
InputTrack,
|
InputTrack,
|
||||||
InputVideoTrack,
|
InputVideoTrack,
|
||||||
InputAudioTrack,
|
InputAudioTrack,
|
||||||
InputTrackQuery,
|
type InputTrackQuery,
|
||||||
PacketStats,
|
type PacketStats,
|
||||||
asc,
|
asc,
|
||||||
desc,
|
desc,
|
||||||
prefer,
|
prefer,
|
||||||
} from './input-track';
|
} from './input-track';
|
||||||
export {
|
export {
|
||||||
EncodedPacket,
|
EncodedPacket,
|
||||||
EncodedPacketSideData,
|
type EncodedPacketSideData,
|
||||||
PacketType,
|
type PacketType,
|
||||||
} from './packet';
|
} from './packet';
|
||||||
export {
|
export {
|
||||||
AudioSample,
|
AudioSample,
|
||||||
AudioSampleInit,
|
type AudioSampleInit,
|
||||||
AudioSampleCopyToOptions,
|
type AudioSampleCopyToOptions,
|
||||||
VideoSample,
|
VideoSample,
|
||||||
VideoSampleInit,
|
type VideoSampleInit,
|
||||||
VideoSamplePixelFormat,
|
type VideoSamplePixelFormat,
|
||||||
VideoSampleColorSpace,
|
VideoSampleColorSpace,
|
||||||
CropRectangle,
|
type CropRectangle,
|
||||||
VIDEO_SAMPLE_PIXEL_FORMATS,
|
VIDEO_SAMPLE_PIXEL_FORMATS,
|
||||||
} from './sample';
|
} from './sample';
|
||||||
export {
|
export {
|
||||||
@@ -255,20 +255,20 @@ export {
|
|||||||
AudioSampleSink,
|
AudioSampleSink,
|
||||||
BaseMediaSampleSink,
|
BaseMediaSampleSink,
|
||||||
CanvasSink,
|
CanvasSink,
|
||||||
CanvasSinkOptions,
|
type CanvasSinkOptions,
|
||||||
EncodedPacketSink,
|
EncodedPacketSink,
|
||||||
PacketRetrievalOptions,
|
type PacketRetrievalOptions,
|
||||||
VideoSampleSink,
|
VideoSampleSink,
|
||||||
WrappedAudioBuffer,
|
type WrappedAudioBuffer,
|
||||||
WrappedCanvas,
|
type WrappedCanvas,
|
||||||
} from './media-sink';
|
} from './media-sink';
|
||||||
export {
|
export {
|
||||||
Conversion,
|
Conversion,
|
||||||
ConversionOptions,
|
type ConversionOptions,
|
||||||
ConversionVideoOptions,
|
type ConversionVideoOptions,
|
||||||
ConversionAudioOptions,
|
type ConversionAudioOptions,
|
||||||
ConversionCanceledError,
|
ConversionCanceledError,
|
||||||
DiscardedTrack,
|
type DiscardedTrack,
|
||||||
} from './conversion';
|
} from './conversion';
|
||||||
export {
|
export {
|
||||||
CustomVideoDecoder,
|
CustomVideoDecoder,
|
||||||
@@ -279,11 +279,11 @@ export {
|
|||||||
registerEncoder,
|
registerEncoder,
|
||||||
} from './custom-coder';
|
} from './custom-coder';
|
||||||
export {
|
export {
|
||||||
MetadataTags,
|
type MetadataTags,
|
||||||
AttachedImage,
|
type AttachedImage,
|
||||||
RichImageData,
|
RichImageData,
|
||||||
AttachedFile,
|
AttachedFile,
|
||||||
TrackDisposition,
|
type TrackDisposition,
|
||||||
} from './metadata';
|
} from './metadata';
|
||||||
|
|
||||||
// 🐡🦔
|
// 🐡🦔
|
||||||
|
|||||||
+26
-1
@@ -50,7 +50,13 @@ import {
|
|||||||
customAudioEncoders,
|
customAudioEncoders,
|
||||||
} from './custom-coder';
|
} from './custom-coder';
|
||||||
import { EncodedPacket, EncodedPacketSideData } from './packet';
|
import { EncodedPacket, EncodedPacketSideData } from './packet';
|
||||||
import { AudioSample, clampCropRectangle, VideoSample } from './sample';
|
import {
|
||||||
|
AudioSample,
|
||||||
|
audioSampleToInterleavedFormat,
|
||||||
|
clampCropRectangle,
|
||||||
|
toInterleavedAudioFormat,
|
||||||
|
VideoSample,
|
||||||
|
} from './sample';
|
||||||
import {
|
import {
|
||||||
AudioEncodingConfig,
|
AudioEncodingConfig,
|
||||||
buildAudioEncoderConfig,
|
buildAudioEncoderConfig,
|
||||||
@@ -1888,6 +1894,21 @@ class AudioEncoderWrapper {
|
|||||||
private async processAndEncode(audioSample: AudioSample, shouldClose: boolean) {
|
private async processAndEncode(audioSample: AudioSample, shouldClose: boolean) {
|
||||||
const config = this.encodingConfig;
|
const config = this.encodingConfig;
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.transform?.sampleFormat !== undefined
|
||||||
|
&& toInterleavedAudioFormat(audioSample.format) !== config.transform.sampleFormat
|
||||||
|
) {
|
||||||
|
// Do a sample format conversion
|
||||||
|
const newSample = audioSampleToInterleavedFormat(audioSample, config.transform.sampleFormat);
|
||||||
|
|
||||||
|
if (shouldClose) {
|
||||||
|
audioSample.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
audioSample = newSample;
|
||||||
|
shouldClose = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (config.transform?.process) {
|
if (config.transform?.process) {
|
||||||
let processed = config.transform.process(audioSample);
|
let processed = config.transform.process(audioSample);
|
||||||
if (processed instanceof Promise) {
|
if (processed instanceof Promise) {
|
||||||
@@ -1910,6 +1931,10 @@ class AudioEncoderWrapper {
|
|||||||
}
|
}
|
||||||
await this.encodeSample(sample, true);
|
await this.encodeSample(sample, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldClose) {
|
||||||
|
audioSample.close();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.encodeSample(audioSample, shouldClose);
|
await this.encodeSample(audioSample, shouldClose);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1953,6 +1953,21 @@ const isAudioData = (x: unknown): x is AudioData => {
|
|||||||
return typeof AudioData !== 'undefined' && x instanceof AudioData;
|
return typeof AudioData !== 'undefined' && x instanceof AudioData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toInterleavedAudioFormat = (format: AudioSampleFormat): 'u8' | 's16' | 's32' | 'f32' => {
|
||||||
|
switch (format) {
|
||||||
|
case 'u8-planar':
|
||||||
|
return 'u8';
|
||||||
|
case 's16-planar':
|
||||||
|
return 's16';
|
||||||
|
case 's32-planar':
|
||||||
|
return 's32';
|
||||||
|
case 'f32-planar':
|
||||||
|
return 'f32';
|
||||||
|
default:
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format
|
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format
|
||||||
* crashes the tab when there are more than 2 channels. This function works around that by always
|
* crashes the tab when there are more than 2 channels. This function works around that by always
|
||||||
@@ -2061,3 +2076,18 @@ const doAudioDataCopyToWebKitWorkaround = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const audioSampleToInterleavedFormat = (sample: AudioSample, format: 'u8' | 's16' | 's32' | 'f32') => {
|
||||||
|
const size = sample.allocationSize({ format, planeIndex: 0 });
|
||||||
|
const buffer = new ArrayBuffer(size);
|
||||||
|
sample.copyTo(buffer, { format, planeIndex: 0 });
|
||||||
|
|
||||||
|
return new AudioSample({
|
||||||
|
data: buffer,
|
||||||
|
format,
|
||||||
|
numberOfChannels: sample.numberOfChannels,
|
||||||
|
sampleRate: sample.sampleRate,
|
||||||
|
timestamp: sample.timestamp,
|
||||||
|
duration: sample.duration,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { expect, test } from 'vitest';
|
||||||
|
import { Input } from '../../src/input.js';
|
||||||
|
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||||
|
import { AudioSampleSource } from '../../src/media-source.js';
|
||||||
|
import { AudioSampleSink } from '../../src/media-sink.js';
|
||||||
|
import { assert } from '../../src/misc.js';
|
||||||
|
import { Output } from '../../src/output.js';
|
||||||
|
import { FlacOutputFormat } from '../../src/output-format.js';
|
||||||
|
import { AudioSample } from '../../src/sample.js';
|
||||||
|
import { BufferSource } from '../../src/source.js';
|
||||||
|
import { BufferTarget } from '../../src/target.js';
|
||||||
|
import { registerFlacEncoder } from '@mediabunny/flac-encoder';
|
||||||
|
|
||||||
|
test('FLAC encoder, 24-bit', async () => {
|
||||||
|
registerFlacEncoder();
|
||||||
|
|
||||||
|
const sampleRate = 48000;
|
||||||
|
const channels = 2;
|
||||||
|
const durationSeconds = 2;
|
||||||
|
const data = createF32SineWave(sampleRate, channels, durationSeconds);
|
||||||
|
|
||||||
|
using sample = await encodeAndDecodeFirstSample(new AudioSample({
|
||||||
|
data,
|
||||||
|
format: 'f32',
|
||||||
|
numberOfChannels: channels,
|
||||||
|
sampleRate,
|
||||||
|
timestamp: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(sample.format).toBe('s32');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FLAC encoder, 16-bit', async () => {
|
||||||
|
registerFlacEncoder();
|
||||||
|
|
||||||
|
const sampleRate = 48000;
|
||||||
|
const channels = 2;
|
||||||
|
const durationSeconds = 2;
|
||||||
|
const data = createS16SineWave(sampleRate, channels, durationSeconds);
|
||||||
|
|
||||||
|
using sample = await encodeAndDecodeFirstSample(new AudioSample({
|
||||||
|
data,
|
||||||
|
format: 's16',
|
||||||
|
numberOfChannels: channels,
|
||||||
|
sampleRate,
|
||||||
|
timestamp: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(sample.format).toBe('s16');
|
||||||
|
});
|
||||||
|
|
||||||
|
const createF32SineWave = (sampleRate: number, channels: number, durationSeconds: number) => {
|
||||||
|
const totalFrames = sampleRate * durationSeconds;
|
||||||
|
const data = new Float32Array(totalFrames * channels);
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const value = Math.sin(2 * Math.PI * 440 * i / sampleRate);
|
||||||
|
for (let ch = 0; ch < channels; ch++) {
|
||||||
|
data[i * channels + ch] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createS16SineWave = (sampleRate: number, channels: number, durationSeconds: number) => {
|
||||||
|
const totalFrames = sampleRate * durationSeconds;
|
||||||
|
const data = new Int16Array(totalFrames * channels);
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const value = Math.round(Math.sin(2 * Math.PI * 440 * i / sampleRate) * 32767);
|
||||||
|
for (let ch = 0; ch < channels; ch++) {
|
||||||
|
data[i * channels + ch] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodeAndDecodeFirstSample = async (audioSample: AudioSample) => {
|
||||||
|
const output = new Output({
|
||||||
|
format: new FlacOutputFormat(),
|
||||||
|
target: new BufferTarget(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const audioSource = new AudioSampleSource({ codec: 'flac' });
|
||||||
|
output.addAudioTrack(audioSource);
|
||||||
|
|
||||||
|
await output.start();
|
||||||
|
await audioSource.add(audioSample);
|
||||||
|
audioSource.close();
|
||||||
|
await output.finalize();
|
||||||
|
|
||||||
|
using input = new Input({
|
||||||
|
source: new BufferSource(output.target.buffer!),
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const track = await input.getPrimaryAudioTrack();
|
||||||
|
assert(track);
|
||||||
|
|
||||||
|
const sink = new AudioSampleSink(track);
|
||||||
|
const sample = await sink.getSample(0);
|
||||||
|
assert(sample);
|
||||||
|
|
||||||
|
return sample;
|
||||||
|
};
|
||||||
@@ -47,7 +47,7 @@ test('FLAC encoding', async () => {
|
|||||||
target: new BufferTarget(),
|
target: new BufferTarget(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioSource = new AudioSampleSource({ codec: 'flac', bitrate: 1 });
|
const audioSource = new AudioSampleSource({ codec: 'flac' });
|
||||||
output.addAudioTrack(audioSource);
|
output.addAudioTrack(audioSource);
|
||||||
|
|
||||||
await output.start();
|
await output.start();
|
||||||
@@ -97,7 +97,7 @@ test('FLAC with huge timestamps', async () => {
|
|||||||
target: new BufferTarget(),
|
target: new BufferTarget(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioSource = new AudioSampleSource({ codec: 'flac', bitrate: 1 });
|
const audioSource = new AudioSampleSource({ codec: 'flac' });
|
||||||
output.addAudioTrack(audioSource);
|
output.addAudioTrack(audioSource);
|
||||||
|
|
||||||
await output.start();
|
await output.start();
|
||||||
|
|||||||
Reference in New Issue
Block a user