init commit

This commit is contained in:
Carlos
2026-02-21 10:33:18 +01:00
parent c863a943ed
commit 9d955bf338
9512 changed files with 2015317 additions and 1305 deletions

View File

@@ -0,0 +1,7 @@
import type { ParserState } from '../../state/parser-state';
import type { XingData } from './parse-xing';
export declare const getDurationFromMp3Xing: ({ xingData, samplesPerFrame, }: {
xingData: XingData;
samplesPerFrame: number;
}) => number;
export declare const getDurationFromMp3: (state: ParserState) => number | null;

View File

@@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDurationFromMp3 = exports.getDurationFromMp3Xing = void 0;
const get_frame_length_1 = require("./get-frame-length");
const samples_per_mpeg_file_1 = require("./samples-per-mpeg-file");
const getDurationFromMp3Xing = ({ xingData, samplesPerFrame, }) => {
const xingFrames = xingData.numberOfFrames;
if (!xingFrames) {
throw new Error('Cannot get duration of VBR MP3 file - no frames');
}
const { sampleRate } = xingData;
if (!sampleRate) {
throw new Error('Cannot get duration of VBR MP3 file - no sample rate');
}
const xingSamples = xingFrames * samplesPerFrame;
return xingSamples / sampleRate;
};
exports.getDurationFromMp3Xing = getDurationFromMp3Xing;
const getDurationFromMp3 = (state) => {
const mp3Info = state.mp3.getMp3Info();
const mp3BitrateInfo = state.mp3.getMp3BitrateInfo();
if (!mp3Info || !mp3BitrateInfo) {
return null;
}
const samplesPerFrame = (0, samples_per_mpeg_file_1.getSamplesPerMpegFrame)({
layer: mp3Info.layer,
mpegVersion: mp3Info.mpegVersion,
});
if (mp3BitrateInfo.type === 'variable') {
return (0, exports.getDurationFromMp3Xing)({
xingData: mp3BitrateInfo.xingData,
samplesPerFrame,
});
}
/**
* sonnet: The variation between 1044 and 1045 bytes in MP3 frames occurs due to the bit reservoir mechanism in MP3 encoding. Here's the typical distribution:
* • 1044 bytes (99% of frames)
* • 1045 bytes (1% of frames)
*/
// we ignore that fact for now
const frameLengthInBytes = (0, get_frame_length_1.getMpegFrameLength)({
bitrateKbit: mp3BitrateInfo.bitrateInKbit,
padding: false,
samplesPerFrame,
samplingFrequency: mp3Info.sampleRate,
layer: mp3Info.layer,
});
const frames = Math.floor((state.contentLength -
state.mediaSection.getMediaSectionAssertOnlyOne().start) /
frameLengthInBytes);
const samples = frames * samplesPerFrame;
const durationInSeconds = samples / mp3Info.sampleRate;
return durationInSeconds;
};
exports.getDurationFromMp3 = getDurationFromMp3;

View File

@@ -0,0 +1,13 @@
export declare const getAverageMpegFrameLength: ({ samplesPerFrame, bitrateKbit, samplingFrequency, layer, }: {
samplesPerFrame: number;
bitrateKbit: number;
samplingFrequency: number;
layer: number;
}) => number;
export declare const getMpegFrameLength: ({ samplesPerFrame, bitrateKbit, samplingFrequency, padding, layer, }: {
samplesPerFrame: number;
bitrateKbit: number;
samplingFrequency: number;
padding: boolean;
layer: number;
}) => number;

View File

@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMpegFrameLength = exports.getAverageMpegFrameLength = void 0;
const getUnroundedMpegFrameLength = ({ samplesPerFrame, bitrateKbit, samplingFrequency, padding, layer, }) => {
if (layer === 1) {
throw new Error('MPEG Layer I is not supported');
}
return ((((samplesPerFrame / 8) * bitrateKbit) / samplingFrequency) * 1000 +
(padding ? (layer === 1 ? 4 : 1) : 0));
};
const getAverageMpegFrameLength = ({ samplesPerFrame, bitrateKbit, samplingFrequency, layer, }) => {
const withoutPadding = getUnroundedMpegFrameLength({
bitrateKbit,
layer,
padding: false,
samplesPerFrame,
samplingFrequency,
});
const rounded = Math.floor(withoutPadding);
const rest = withoutPadding % 1;
return rest * (rounded + 1) + (1 - rest) * rounded;
};
exports.getAverageMpegFrameLength = getAverageMpegFrameLength;
const getMpegFrameLength = ({ samplesPerFrame, bitrateKbit, samplingFrequency, padding, layer, }) => {
return Math.floor(getUnroundedMpegFrameLength({
bitrateKbit,
layer,
padding,
samplesPerFrame,
samplingFrequency,
}));
};
exports.getMpegFrameLength = getMpegFrameLength;

View File

@@ -0,0 +1,3 @@
import type { MediaParserMetadataEntry } from '../../metadata/get-metadata';
import type { Mp3Structure } from '../../parse-result';
export declare const getMetadataFromMp3: (mp3Structure: Mp3Structure) => MediaParserMetadataEntry[] | null;

View File

@@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMetadataFromMp3 = void 0;
const getMetadataFromMp3 = (mp3Structure) => {
const findHeader = mp3Structure.boxes.find((b) => b.type === 'id3-header');
return findHeader ? findHeader.metatags : null;
};
exports.getMetadataFromMp3 = getMetadataFromMp3;

View File

@@ -0,0 +1,6 @@
import type { SeekResolution } from '../../work-on-seek-request';
import type { Mp3SeekingHints } from './seeking-hints';
export declare const getSeekingByteForMp3: ({ time, info, }: {
time: number;
info: Mp3SeekingHints;
}) => SeekResolution;

View File

@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSeekingByteForMp3 = void 0;
const get_approximate_byte_from_bitrate_1 = require("./seek/get-approximate-byte-from-bitrate");
const get_byte_from_observed_samples_1 = require("./seek/get-byte-from-observed-samples");
const get_seek_point_from_xing_1 = require("./seek/get-seek-point-from-xing");
const getSeekingByteForMp3 = ({ time, info, }) => {
var _a;
if (info.mp3BitrateInfo === null ||
info.mp3Info === null ||
info.mediaSection === null) {
return {
type: 'valid-but-must-wait',
};
}
const approximateByte = (0, get_approximate_byte_from_bitrate_1.getApproximateByteFromBitrate)({
mp3BitrateInfo: info.mp3BitrateInfo,
timeInSeconds: time,
mp3Info: info.mp3Info,
mediaSection: info.mediaSection,
contentLength: info.contentLength,
});
const bestAudioSample = (0, get_byte_from_observed_samples_1.getByteFromObservedSamples)({
info,
timeInSeconds: time,
});
const xingSeekPoint = info.mp3BitrateInfo.type === 'variable'
? (0, get_seek_point_from_xing_1.getSeekPointFromXing)({
mp3Info: info.mp3Info,
timeInSeconds: time,
xingData: info.mp3BitrateInfo.xingData,
})
: null;
const candidates = [
approximateByte,
(_a = bestAudioSample === null || bestAudioSample === void 0 ? void 0 : bestAudioSample.offset) !== null && _a !== void 0 ? _a : null,
xingSeekPoint,
].filter((b) => b !== null);
if (candidates.length === 0) {
return {
type: 'valid-but-must-wait',
};
}
const byte = Math.max(...candidates);
const timeInSeconds = byte === (bestAudioSample === null || bestAudioSample === void 0 ? void 0 : bestAudioSample.offset) ? bestAudioSample.timeInSeconds : time;
return {
type: 'do-seek',
byte,
timeInSeconds,
};
};
exports.getSeekingByteForMp3 = getSeekingByteForMp3;

View File

@@ -0,0 +1,2 @@
import type { BufferIterator } from '../../iterator/buffer-iterator';
export declare const parseID3V1: (iterator: BufferIterator) => void;

View File

@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseID3V1 = void 0;
const parseID3V1 = (iterator) => {
if (iterator.bytesRemaining() < 128) {
return;
}
// we drop ID3v1 because usually there is also ID3v2 and ID3v3 which are superior.
// Better than have duplicated data.
iterator.discard(128);
};
exports.parseID3V1 = parseID3V1;

View File

@@ -0,0 +1,4 @@
import type { ParserState } from '../../state/parser-state';
export declare const parseId3: ({ state }: {
state: ParserState;
}) => void;

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseId3 = void 0;
function combine28Bits(a, b, c, d) {
// Mask each number to ignore first bit (& 0x7F)
const val1 = a & 0x7f; // 7 bits from first byte
const val2 = b & 0x7f; // 7 bits from second byte
const val3 = c & 0x7f; // 7 bits from third byte
const val4 = d & 0x7f; // 7 bits from fourth byte
// Combine all values using bitwise operations
return (val1 << 21) | (val2 << 14) | (val3 << 7) | val4;
}
const parseId3 = ({ state }) => {
const { iterator } = state;
if (iterator.bytesRemaining() < 9) {
return;
}
const { returnToCheckpoint } = iterator.startCheckpoint();
iterator.discard(3);
const versionMajor = iterator.getUint8();
const versionMinor = iterator.getUint8();
const flags = iterator.getUint8();
const sizeArr = iterator.getSlice(4);
const size = combine28Bits(sizeArr[0], sizeArr[1], sizeArr[2], sizeArr[3]);
if (iterator.bytesRemaining() < size) {
returnToCheckpoint();
return;
}
const entries = [];
const initial = iterator.counter.getOffset();
while (iterator.counter.getOffset() < size + initial) {
const name = versionMajor === 3 || versionMajor === 4
? iterator.getByteString(4, true)
: iterator.getByteString(3, true);
if (name === '') {
iterator.discard(size + initial - iterator.counter.getOffset());
break;
}
const s = versionMajor === 4
? iterator.getSyncSafeInt32()
: versionMajor === 3
? iterator.getUint32()
: iterator.getUint24();
if (versionMajor === 3 || versionMajor === 4) {
iterator.getUint16(); // flags
}
let subtract = 0;
if (!name.startsWith('W')) {
iterator.getUint8(); // encoding
subtract += 1;
}
if (name === 'APIC') {
const { discardRest } = iterator.planBytes(s - subtract);
const mimeType = iterator.readUntilNullTerminator();
iterator.getUint16(); // picture type
const description = iterator.readUntilNullTerminator();
iterator.discard(1);
const data = discardRest();
state.images.addImage({
data,
description,
mimeType,
});
}
else {
const information = iterator.getByteString(s - subtract, true);
entries.push({
key: name,
value: information,
trackId: null,
});
}
}
state.structure.getMp3Structure().boxes.push({
type: 'id3-header',
flags,
size,
versionMajor,
versionMinor,
metatags: entries,
});
};
exports.parseId3 = parseId3;

View File

@@ -0,0 +1,3 @@
import type { ParseResult } from '../../parse-result';
import type { ParserState } from '../../state/parser-state';
export declare const parseMp3: (state: ParserState) => Promise<ParseResult>;

View File

@@ -0,0 +1,42 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseMp3 = void 0;
const id3_1 = require("./id3");
const id3_v1_1 = require("./id3-v1");
const parse_mpeg_header_1 = require("./parse-mpeg-header");
const wait_until_syncword_1 = require("./seek/wait-until-syncword");
const parseMp3 = async (state) => {
const { iterator } = state;
if (iterator.bytesRemaining() < 3) {
return null;
}
// When coming from a seek, we need to discard until the syncword
if (state.mediaSection.isCurrentByteInMediaSection(iterator) === 'in-section') {
(0, wait_until_syncword_1.discardUntilSyncword)({ iterator });
await (0, parse_mpeg_header_1.parseMpegHeader)({
state,
});
return null;
}
const { returnToCheckpoint } = iterator.startCheckpoint();
const bytes = iterator.getSlice(3);
returnToCheckpoint();
// ID3 v1
if (bytes[0] === 0x54 && bytes[1] === 0x41 && bytes[2] === 0x47) {
(0, id3_v1_1.parseID3V1)(iterator);
return null;
}
// ID3 v2 or v3
if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) {
(0, id3_1.parseId3)({ state });
return null;
}
if (bytes[0] === 0xff) {
await (0, parse_mpeg_header_1.parseMpegHeader)({
state,
});
return null;
}
throw new Error('Unknown MP3 header ' + JSON.stringify(bytes));
};
exports.parseMp3 = parseMp3;

View File

@@ -0,0 +1,4 @@
import type { ParserState } from '../../state/parser-state';
export declare const parseMpegHeader: ({ state, }: {
state: ParserState;
}) => Promise<void>;

View File

@@ -0,0 +1,117 @@
"use strict";
// spec: http://www.mp3-tech.org/programmer/frame_header.html
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseMpegHeader = void 0;
const log_1 = require("../../log");
const register_track_1 = require("../../register-track");
const webcodecs_timescale_1 = require("../../webcodecs-timescale");
const parse_packet_header_1 = require("./parse-packet-header");
const parse_xing_1 = require("./parse-xing");
const audio_sample_from_cbr_1 = require("./seek/audio-sample-from-cbr");
const audio_sample_from_vbr_1 = require("./seek/audio-sample-from-vbr");
const parseMpegHeader = async ({ state, }) => {
const { iterator } = state;
const initialOffset = iterator.counter.getOffset();
if (iterator.bytesRemaining() < 32) {
return;
}
// parse header
const { frameLength, bitrateInKbit, layer, mpegVersion, numberOfChannels, sampleRate, samplesPerFrame, } = (0, parse_packet_header_1.parseMp3PacketHeader)(iterator);
const cbrMp3Info = state.mp3.getMp3BitrateInfo();
if (cbrMp3Info && cbrMp3Info.type === 'constant') {
if (bitrateInKbit !== cbrMp3Info.bitrateInKbit) {
throw new Error(`Bitrate mismatch at offset ${initialOffset}: ${bitrateInKbit} !== ${cbrMp3Info.bitrateInKbit}`);
}
}
const offsetNow = iterator.counter.getOffset();
iterator.counter.decrement(offsetNow - initialOffset);
const data = iterator.getSlice(frameLength);
if (state.callbacks.tracks.getTracks().length === 0) {
const info = {
layer,
mpegVersion,
sampleRate,
};
const asText = new TextDecoder().decode(data);
if (asText.includes('VBRI')) {
throw new Error('MP3 files with VBRI are currently unsupported because we have no sample file. Submit this file at remotion.dev/report if you would like us to support this file.');
}
if (asText.includes('Info')) {
return;
}
const isVbr = asText.includes('Xing');
if (isVbr) {
const xingData = (0, parse_xing_1.parseXing)(data);
log_1.Log.verbose(state.logLevel, 'MP3 has variable bit rate. Requiring whole file to be read');
state.mp3.setMp3BitrateInfo({
type: 'variable',
xingData,
});
return;
}
if (!state.mp3.getMp3BitrateInfo()) {
state.mp3.setMp3BitrateInfo({
bitrateInKbit,
type: 'constant',
});
}
state.mp3.setMp3Info(info);
await (0, register_track_1.registerAudioTrack)({
container: 'mp3',
track: {
type: 'audio',
codec: 'mp3',
codecData: null,
codecEnum: 'mp3',
description: undefined,
numberOfChannels,
sampleRate,
originalTimescale: 1000000,
trackId: 0,
startInSeconds: 0,
timescale: webcodecs_timescale_1.WEBCODECS_TIMESCALE,
trackMediaTimeOffsetInTrackTimescale: 0,
},
registerAudioSampleCallback: state.callbacks.registerAudioSampleCallback,
tracks: state.callbacks.tracks,
logLevel: state.logLevel,
onAudioTrack: state.onAudioTrack,
});
state.callbacks.tracks.setIsDone(state.logLevel);
state.mediaSection.addMediaSection({
start: initialOffset,
size: state.contentLength - initialOffset,
});
}
const bitrateInfo = state.mp3.getMp3BitrateInfo();
if (!bitrateInfo) {
throw new Error('No bitrate info');
}
const sample = bitrateInfo.type === 'constant'
? (0, audio_sample_from_cbr_1.getAudioSampleFromCbr)({
bitrateInKbit,
data,
initialOffset,
layer,
sampleRate,
samplesPerFrame,
state,
})
: (0, audio_sample_from_vbr_1.getAudioSampleFromVbr)({
data,
info: bitrateInfo,
mp3Info: state.mp3.getMp3Info(),
position: initialOffset,
});
const { audioSample, timeInSeconds, durationInSeconds } = sample;
state.mp3.audioSamples.addSample({
timeInSeconds,
offset: initialOffset,
durationInSeconds,
});
await state.callbacks.onAudioSample({
audioSample,
trackId: 0,
});
};
exports.parseMpegHeader = parseMpegHeader;

View File

@@ -0,0 +1,30 @@
import type { BufferIterator } from '../../iterator/buffer-iterator';
type MpegVersion = 1 | 2;
export declare const parseMp3PacketHeader: (iterator: BufferIterator) => {
frameLength: number;
bitrateInKbit: number;
layer: number;
mpegVersion: MpegVersion;
numberOfChannels: number;
sampleRate: number;
samplesPerFrame: number;
};
export declare const isMp3PacketHeaderHere: (iterator: BufferIterator) => false | {
frameLength: number;
bitrateInKbit: number;
layer: number;
mpegVersion: MpegVersion;
numberOfChannels: number;
sampleRate: number;
samplesPerFrame: number;
};
export declare const isMp3PacketHeaderHereAndInNext: (iterator: BufferIterator) => boolean | {
frameLength: number;
bitrateInKbit: number;
layer: number;
mpegVersion: MpegVersion;
numberOfChannels: number;
sampleRate: number;
samplesPerFrame: number;
};
export {};

View File

@@ -0,0 +1,258 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isMp3PacketHeaderHereAndInNext = exports.isMp3PacketHeaderHere = exports.parseMp3PacketHeader = void 0;
const get_frame_length_1 = require("./get-frame-length");
const samples_per_mpeg_file_1 = require("./samples-per-mpeg-file");
function getSamplingFrequency({ bits, mpegVersion, }) {
const samplingTable = {
0b00: { MPEG1: 44100, MPEG2: 22050 },
0b01: { MPEG1: 48000, MPEG2: 24000 },
0b10: { MPEG1: 32000, MPEG2: 16000 },
0b11: { MPEG1: 'reserved', MPEG2: 'reserved' },
};
const key = `MPEG${mpegVersion}`;
const value = samplingTable[bits][key];
if (value === 'reserved') {
throw new Error('Reserved sampling frequency');
}
if (!value) {
throw new Error('Invalid sampling frequency for MPEG version: ' +
JSON.stringify({ bits, version: mpegVersion }));
}
return value;
}
function getBitrateKB({ bits, mpegVersion, level, }) {
const bitrateTable = {
0b0000: {
'V1,L1': 'free',
'V1,L2': 'free',
'V1,L3': 'free',
'V2,L1': 'free',
'V2,L2&L3': 'free',
},
0b0001: { 'V1,L1': 32, 'V1,L2': 32, 'V1,L3': 32, 'V2,L1': 32, 'V2,L2&L3': 8 },
0b0010: {
'V1,L1': 64,
'V1,L2': 48,
'V1,L3': 40,
'V2,L1': 48,
'V2,L2&L3': 16,
},
0b0011: {
'V1,L1': 96,
'V1,L2': 56,
'V1,L3': 48,
'V2,L1': 56,
'V2,L2&L3': 24,
},
0b0100: {
'V1,L1': 128,
'V1,L2': 64,
'V1,L3': 56,
'V2,L1': 64,
'V2,L2&L3': 32,
},
0b0101: {
'V1,L1': 160,
'V1,L2': 80,
'V1,L3': 64,
'V2,L1': 80,
'V2,L2&L3': 40,
},
0b0110: {
'V1,L1': 192,
'V1,L2': 96,
'V1,L3': 80,
'V2,L1': 96,
'V2,L2&L3': 48,
},
0b0111: {
'V1,L1': 224,
'V1,L2': 112,
'V1,L3': 96,
'V2,L1': 112,
'V2,L2&L3': 56,
},
0b1000: {
'V1,L1': 256,
'V1,L2': 128,
'V1,L3': 112,
'V2,L1': 128,
'V2,L2&L3': 64,
},
0b1001: {
'V1,L1': 288,
'V1,L2': 160,
'V1,L3': 128,
'V2,L1': 144,
'V2,L2&L3': 80,
},
0b1010: {
'V1,L1': 320,
'V1,L2': 192,
'V1,L3': 160,
'V2,L1': 160,
'V2,L2&L3': 96,
},
0b1011: {
'V1,L1': 352,
'V1,L2': 224,
'V1,L3': 192,
'V2,L1': 176,
'V2,L2&L3': 112,
},
0b1100: {
'V1,L1': 384,
'V1,L2': 256,
'V1,L3': 224,
'V2,L1': 192,
'V2,L2&L3': 128,
},
0b1101: {
'V1,L1': 416,
'V1,L2': 320,
'V1,L3': 256,
'V2,L1': 224,
'V2,L2&L3': 144,
},
0b1110: {
'V1,L1': 448,
'V1,L2': 384,
'V1,L3': 320,
'V2,L1': 256,
'V2,L2&L3': 160,
},
0b1111: {
'V1,L1': 'bad',
'V1,L2': 'bad',
'V1,L3': 'bad',
'V2,L1': 'bad',
'V2,L2&L3': 'bad',
},
};
// Determine the correct key based on version and level
let key;
if (mpegVersion === 2 && (level === 2 || level === 3)) {
key = 'V2,L2&L3';
}
else {
key = `V${mpegVersion},L${level}`;
}
// Return the corresponding bitrate
return bitrateTable[bits][key];
}
const innerParseMp3PacketHeader = (iterator) => {
for (let i = 0; i < 11; i++) {
const expectToBe1 = iterator.getBits(1);
if (expectToBe1 !== 1) {
throw new Error('Expected 1');
}
}
const audioVersionId = iterator.getBits(2);
/**
* 00 - MPEG Version 2.5 (later extension of MPEG 2)
01 - reserved
10 - MPEG Version 2 (ISO/IEC 13818-3)
11 - MPEG Version 1 (ISO/IEC 11172-3)
*/
if (audioVersionId !== 0b11 &&
audioVersionId !== 0b10 &&
audioVersionId !== 0b00) {
throw new Error('Expected MPEG Version 1 or 2');
}
const mpegVersion = audioVersionId === 0b11 ? 1 : 2;
const layerBits = iterator.getBits(2);
/**
* 00 - reserved
01 - Layer III
10 - Layer II
11 - Layer I
*/
if (layerBits === 0b00) {
throw new Error('Expected Layer I, II or III');
}
const layer = layerBits === 0b11 ? 1 : layerBits === 0b10 ? 2 : 3;
iterator.getBits(1); // 0b1 means that there is no CRC, 0b0 means there is. Not validating checksum though
const bitrateIndex = iterator.getBits(4);
const bitrateInKbit = getBitrateKB({
bits: bitrateIndex,
mpegVersion,
level: layer,
});
if (bitrateInKbit === 'bad') {
throw new Error('Invalid bitrate');
}
if (bitrateInKbit === 'free') {
throw new Error('Free bitrate not supported');
}
const samplingFrequencyIndex = iterator.getBits(2);
const baseSampleRate = getSamplingFrequency({
bits: samplingFrequencyIndex,
mpegVersion,
});
const sampleRate = audioVersionId === 0b00 ? baseSampleRate / 2 : baseSampleRate;
const padding = Boolean(iterator.getBits(1));
iterator.getBits(1); // private bit
const channelMode = iterator.getBits(2); // channel mode
iterator.getBits(2); // mode extension
iterator.getBits(1); // copyright
iterator.getBits(1); // original
iterator.getBits(2); // emphasis
const numberOfChannels = channelMode === 0b11 ? 1 : 2;
const samplesPerFrame = (0, samples_per_mpeg_file_1.getSamplesPerMpegFrame)({ mpegVersion, layer });
const frameLength = (0, get_frame_length_1.getMpegFrameLength)({
bitrateKbit: bitrateInKbit,
padding,
samplesPerFrame,
samplingFrequency: sampleRate,
layer,
});
return {
frameLength,
bitrateInKbit,
layer,
mpegVersion,
numberOfChannels,
sampleRate,
samplesPerFrame,
};
};
const parseMp3PacketHeader = (iterator) => {
iterator.startReadingBits();
const d = innerParseMp3PacketHeader(iterator);
iterator.stopReadingBits();
return d;
};
exports.parseMp3PacketHeader = parseMp3PacketHeader;
const isMp3PacketHeaderHere = (iterator) => {
const offset = iterator.counter.getOffset();
iterator.startReadingBits();
try {
const res = innerParseMp3PacketHeader(iterator);
iterator.stopReadingBits();
iterator.counter.decrement(iterator.counter.getOffset() - offset);
return res;
}
catch (_a) {
iterator.stopReadingBits();
iterator.counter.decrement(iterator.counter.getOffset() - offset);
return false;
}
};
exports.isMp3PacketHeaderHere = isMp3PacketHeaderHere;
const isMp3PacketHeaderHereAndInNext = (iterator) => {
const offset = iterator.counter.getOffset();
const res = (0, exports.isMp3PacketHeaderHere)(iterator);
if (!res) {
return false;
}
// cannot check here because we don't have enough data, let's hope for the best
if (iterator.bytesRemaining() <= res.frameLength) {
return true;
}
iterator.counter.increment(res.frameLength);
const isHere = (0, exports.isMp3PacketHeaderHere)(iterator);
iterator.counter.decrement(iterator.counter.getOffset() - offset);
return isHere;
};
exports.isMp3PacketHeaderHereAndInNext = isMp3PacketHeaderHereAndInNext;

View File

@@ -0,0 +1,19 @@
export type XingData = {
sampleRate: number;
numberOfFrames: number | null;
fileSize: number | null;
tableOfContents: number[] | null;
vbrScale: number | null;
};
export declare const parseXing: (data: Uint8Array) => XingData;
export declare const getSeekPointInBytes: ({ fileSize, percentBetween0And100, tableOfContents, }: {
tableOfContents: number[];
fileSize: number;
percentBetween0And100: number;
}) => number;
export declare const getTimeFromPosition: ({ position, fileSize, tableOfContents, durationInSeconds, }: {
position: number;
fileSize: number;
tableOfContents: number[];
durationInSeconds: number;
}) => number;

View File

@@ -0,0 +1,121 @@
"use strict";
// implementation of http://www.mp3-tech.org/programmer/sources/vbrheadersdk.zip
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTimeFromPosition = exports.getSeekPointInBytes = exports.parseXing = void 0;
const SAMPLE_RATES = [44100, 48000, 32000, 99999];
const FRAMES_FLAG = 0x0001;
const BYTES_FLAG = 0x0002;
const TOC_FLAG = 0x0004;
const VBR_SCALE_FLAG = 0x0008;
const extractI4 = (data, offset) => {
let x = 0;
x = data[offset];
x <<= 8;
x |= data[offset + 1];
x <<= 8;
x |= data[offset + 2];
x <<= 8;
x |= data[offset + 3];
return x;
};
const parseXing = (data) => {
const h_id = (data[1] >> 3) & 1;
const h_sr_index = (data[2] >> 2) & 3;
const h_mode = (data[3] >> 6) & 3;
let xingOffset = 0;
if (h_id) {
// mpeg1
if (h_mode !== 3) {
xingOffset += 32 + 4;
}
else {
xingOffset += 17 + 4;
}
}
else if (h_mode !== 3) {
xingOffset += 17 + 4;
}
else {
xingOffset += 9 + 4;
}
const expectXing = new TextDecoder('utf8').decode(data.slice(xingOffset, xingOffset + 4));
if (expectXing !== 'Xing') {
throw new Error('Invalid Xing header');
}
let sampleRate = SAMPLE_RATES[h_sr_index];
if (h_id === 0) {
sampleRate >>= 1;
}
let offset = xingOffset + 4;
const flags = extractI4(data, offset);
offset += 4;
let numberOfFrames;
let fileSize;
let tableOfContents;
let vbrScale;
if (flags & FRAMES_FLAG) {
numberOfFrames = extractI4(data, offset);
offset += 4;
}
if (flags & BYTES_FLAG) {
fileSize = extractI4(data, offset);
offset += 4;
}
if (flags & TOC_FLAG) {
tableOfContents = data.slice(offset, offset + 100);
offset += 100;
}
if (flags & VBR_SCALE_FLAG) {
vbrScale = extractI4(data, offset);
offset += 4;
}
// Allow extra data after the standard Xing fields, as some encoders add additional information
if (offset > data.length) {
throw new Error('xing header was parsed wrong: read beyond available data');
}
return {
sampleRate,
numberOfFrames: numberOfFrames !== null && numberOfFrames !== void 0 ? numberOfFrames : null,
fileSize: fileSize !== null && fileSize !== void 0 ? fileSize : null,
tableOfContents: tableOfContents
? Array.from(tableOfContents.slice(0, 100))
: null,
vbrScale: vbrScale !== null && vbrScale !== void 0 ? vbrScale : null,
};
};
exports.parseXing = parseXing;
const getSeekPointInBytes = ({ fileSize, percentBetween0And100, tableOfContents, }) => {
let index = Math.floor(percentBetween0And100);
if (index > 99) {
index = 99;
}
const fa = tableOfContents[index];
let fb;
if (index < 99) {
fb = tableOfContents[index + 1];
}
else {
fb = 256;
}
const fx = fa + (fb - fa) * (percentBetween0And100 - index);
const seekPoint = (1 / 256) * fx * fileSize;
return Math.floor(seekPoint);
};
exports.getSeekPointInBytes = getSeekPointInBytes;
const getTimeFromPosition = ({ position, fileSize, tableOfContents, durationInSeconds, }) => {
// Convert position to a value between 0-256
const positionNormalized = (position / fileSize) * 256;
// Find the closest indices in the table of contents
let index = 0;
while (index < 99 && tableOfContents[index + 1] <= positionNormalized) {
index++;
}
const fa = tableOfContents[index];
const fb = index < 99 ? tableOfContents[index + 1] : 256;
// Interpolate between the two points
const percentWithinSegment = (positionNormalized - fa) / (fb - fa);
const percentBetween0And100 = index + percentWithinSegment;
// Convert percentage to time
return (percentBetween0And100 / 100) * durationInSeconds;
};
exports.getTimeFromPosition = getTimeFromPosition;

View File

@@ -0,0 +1,4 @@
export declare const getSamplesPerMpegFrame: ({ mpegVersion, layer, }: {
mpegVersion: 1 | 2;
layer: number;
}) => 384 | 1152 | 576;

View File

@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSamplesPerMpegFrame = void 0;
const getSamplesPerMpegFrame = ({ mpegVersion, layer, }) => {
if (mpegVersion === 1) {
if (layer === 1) {
return 384;
}
if (layer === 2 || layer === 3) {
return 1152;
}
}
if (mpegVersion === 2) {
if (layer === 1) {
return 384;
}
if (layer === 2) {
return 1152;
}
if (layer === 3) {
return 576;
}
}
throw new Error('Invalid MPEG layer');
};
exports.getSamplesPerMpegFrame = getSamplesPerMpegFrame;

View File

@@ -0,0 +1,16 @@
import type { ParserState } from '../../../state/parser-state';
import type { MediaParserAudioSample } from '../../../webcodec-sample-types';
export declare const getAudioSampleFromCbr: ({ bitrateInKbit, initialOffset, layer, sampleRate, samplesPerFrame, data, state, }: {
bitrateInKbit: number;
layer: number;
samplesPerFrame: number;
sampleRate: number;
initialOffset: number;
data: Uint8Array;
state: ParserState;
}) => {
audioSample: MediaParserAudioSample;
timeInSeconds: number;
durationInSeconds: number;
};
export type AudioSampleFromCbr = ReturnType<typeof getAudioSampleFromCbr>;

View File

@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAudioSampleFromCbr = void 0;
const webcodecs_timescale_1 = require("../../../webcodecs-timescale");
const get_frame_length_1 = require("../get-frame-length");
const getAudioSampleFromCbr = ({ bitrateInKbit, initialOffset, layer, sampleRate, samplesPerFrame, data, state, }) => {
const avgLength = (0, get_frame_length_1.getAverageMpegFrameLength)({
bitrateKbit: bitrateInKbit,
layer,
samplesPerFrame,
samplingFrequency: sampleRate,
});
const mp3Info = state.mp3.getMp3Info();
if (!mp3Info) {
throw new Error('No MP3 info');
}
const nthFrame = Math.round((initialOffset - state.mediaSection.getMediaSectionAssertOnlyOne().start) /
avgLength);
const durationInSeconds = samplesPerFrame / sampleRate;
const timeInSeconds = (nthFrame * samplesPerFrame) / sampleRate;
// Important that we round down, otherwise WebCodecs might stall, e.g.
// Last input = 30570667 Last output = 30570666 -> stuck
const timestamp = Math.floor(timeInSeconds * webcodecs_timescale_1.WEBCODECS_TIMESCALE);
const duration = Math.floor(durationInSeconds * webcodecs_timescale_1.WEBCODECS_TIMESCALE);
const audioSample = {
data,
decodingTimestamp: timestamp,
duration,
offset: initialOffset,
timestamp,
type: 'key',
};
return { audioSample, timeInSeconds, durationInSeconds };
};
exports.getAudioSampleFromCbr = getAudioSampleFromCbr;

View File

@@ -0,0 +1,8 @@
import type { Mp3Info, VariableMp3BitrateInfo } from '../../../state/mp3';
import type { AudioSampleFromCbr } from './audio-sample-from-cbr';
export declare const getAudioSampleFromVbr: ({ info, position, mp3Info, data, }: {
position: number;
info: VariableMp3BitrateInfo;
mp3Info: Mp3Info | null;
data: Uint8Array;
}) => AudioSampleFromCbr;

View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAudioSampleFromVbr = void 0;
const webcodecs_timescale_1 = require("../../../webcodecs-timescale");
const get_duration_1 = require("../get-duration");
const parse_xing_1 = require("../parse-xing");
const samples_per_mpeg_file_1 = require("../samples-per-mpeg-file");
const getAudioSampleFromVbr = ({ info, position, mp3Info, data, }) => {
if (!mp3Info) {
throw new Error('No MP3 info');
}
const samplesPerFrame = (0, samples_per_mpeg_file_1.getSamplesPerMpegFrame)({
layer: mp3Info.layer,
mpegVersion: mp3Info.mpegVersion,
});
const wholeFileDuration = (0, get_duration_1.getDurationFromMp3Xing)({
samplesPerFrame,
xingData: info.xingData,
});
if (!info.xingData.fileSize) {
throw new Error('file size');
}
if (!info.xingData.tableOfContents) {
throw new Error('table of contents');
}
const timeInSeconds = (0, parse_xing_1.getTimeFromPosition)({
durationInSeconds: wholeFileDuration,
fileSize: info.xingData.fileSize,
position,
tableOfContents: info.xingData.tableOfContents,
});
const durationInSeconds = samplesPerFrame / info.xingData.sampleRate;
const timestamp = Math.floor(timeInSeconds * webcodecs_timescale_1.WEBCODECS_TIMESCALE);
const duration = Math.floor(durationInSeconds * webcodecs_timescale_1.WEBCODECS_TIMESCALE);
const audioSample = {
data,
decodingTimestamp: timestamp,
duration,
offset: position,
timestamp,
type: 'key',
};
return { timeInSeconds, audioSample, durationInSeconds };
};
exports.getAudioSampleFromVbr = getAudioSampleFromVbr;

View File

@@ -0,0 +1,9 @@
import type { Mp3BitrateInfo, Mp3Info } from '../../../state/mp3';
import type { MediaSection } from '../../../state/video-section';
export declare const getApproximateByteFromBitrate: ({ mp3BitrateInfo, timeInSeconds, mp3Info, mediaSection, contentLength, }: {
mp3BitrateInfo: Mp3BitrateInfo;
mp3Info: Mp3Info;
timeInSeconds: number;
mediaSection: MediaSection;
contentLength: number;
}) => number | null;

View File

@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getApproximateByteFromBitrate = void 0;
const get_frame_length_1 = require("../get-frame-length");
const samples_per_mpeg_file_1 = require("../samples-per-mpeg-file");
const getApproximateByteFromBitrate = ({ mp3BitrateInfo, timeInSeconds, mp3Info, mediaSection, contentLength, }) => {
if (mp3BitrateInfo.type === 'variable') {
return null;
}
const samplesPerFrame = (0, samples_per_mpeg_file_1.getSamplesPerMpegFrame)({
layer: mp3Info.layer,
mpegVersion: mp3Info.mpegVersion,
});
const frameLengthInBytes = (0, get_frame_length_1.getMpegFrameLength)({
bitrateKbit: mp3BitrateInfo.bitrateInKbit,
padding: false,
samplesPerFrame,
samplingFrequency: mp3Info.sampleRate,
layer: mp3Info.layer,
});
const frameIndexUnclamped = Math.floor((timeInSeconds * mp3Info.sampleRate) / samplesPerFrame);
const frames = Math.floor((contentLength - mediaSection.start) / frameLengthInBytes);
const frameIndex = Math.min(frames - 1, frameIndexUnclamped);
const byteRelativeToMediaSection = frameIndex * frameLengthInBytes;
const byteBeforeFrame = byteRelativeToMediaSection + mediaSection.start;
return byteBeforeFrame;
};
exports.getApproximateByteFromBitrate = getApproximateByteFromBitrate;

View File

@@ -0,0 +1,6 @@
import type { AudioSampleOffset } from '../../../state/audio-sample-map';
import type { Mp3SeekingHints } from '../seeking-hints';
export declare const getByteFromObservedSamples: ({ info, timeInSeconds, }: {
info: Mp3SeekingHints;
timeInSeconds: number;
}) => AudioSampleOffset | undefined;

View File

@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getByteFromObservedSamples = void 0;
const getByteFromObservedSamples = ({ info, timeInSeconds, }) => {
let bestAudioSample;
for (const hint of info.audioSampleMap) {
if (hint.timeInSeconds > timeInSeconds) {
continue;
}
// Everything is a keyframe in mp3, so if this sample does not cover the time, it's not a good candidate.
// Let's go to the next one. Exception: If we already saw the last sample, we use it so we find can at least
// find the closest one.
if (hint.timeInSeconds + hint.durationInSeconds < timeInSeconds &&
!info.lastSampleObserved) {
continue;
}
if (!bestAudioSample) {
bestAudioSample = hint;
continue;
}
if (bestAudioSample.timeInSeconds < hint.timeInSeconds) {
bestAudioSample = hint;
}
}
return bestAudioSample;
};
exports.getByteFromObservedSamples = getByteFromObservedSamples;

View File

@@ -0,0 +1,7 @@
import type { Mp3Info } from '../../../state/mp3';
import { type XingData } from '../parse-xing';
export declare const getSeekPointFromXing: ({ timeInSeconds, xingData, mp3Info, }: {
timeInSeconds: number;
xingData: XingData;
mp3Info: Mp3Info;
}) => number;

View File

@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSeekPointFromXing = void 0;
const get_duration_1 = require("../get-duration");
const parse_xing_1 = require("../parse-xing");
const samples_per_mpeg_file_1 = require("../samples-per-mpeg-file");
const getSeekPointFromXing = ({ timeInSeconds, xingData, mp3Info, }) => {
const samplesPerFrame = (0, samples_per_mpeg_file_1.getSamplesPerMpegFrame)({
layer: mp3Info.layer,
mpegVersion: mp3Info.mpegVersion,
});
const duration = (0, get_duration_1.getDurationFromMp3Xing)({
xingData,
samplesPerFrame,
});
const totalSamples = timeInSeconds * xingData.sampleRate;
// -1 frame so we are sure to be before the target
const oneFrameSubtracted = totalSamples - samplesPerFrame;
const timeToTarget = Math.max(0, oneFrameSubtracted / xingData.sampleRate);
if (!xingData.fileSize || !xingData.tableOfContents) {
throw new Error('Cannot seek of VBR MP3 file');
}
return (0, parse_xing_1.getSeekPointInBytes)({
fileSize: xingData.fileSize,
percentBetween0And100: (timeToTarget / duration) * 100,
tableOfContents: xingData.tableOfContents,
});
};
exports.getSeekPointFromXing = getSeekPointFromXing;

View File

@@ -0,0 +1,4 @@
import type { BufferIterator } from '../../../iterator/buffer-iterator';
export declare const discardUntilSyncword: ({ iterator, }: {
iterator: BufferIterator;
}) => void;

View File

@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.discardUntilSyncword = void 0;
const parse_packet_header_1 = require("../parse-packet-header");
const discardUntilSyncword = ({ iterator, }) => {
while (true) {
const next2Bytes = iterator.getUint8();
if (next2Bytes !== 0xff) {
continue;
}
if (iterator.bytesRemaining() === 0) {
break;
}
const nextByte = iterator.getUint8();
const mask = 0xe0; // 1110 0000
if ((nextByte & mask) !== mask) {
continue;
}
iterator.counter.decrement(2);
if ((0, parse_packet_header_1.isMp3PacketHeaderHereAndInNext)(iterator)) {
break;
}
else {
iterator.counter.increment(2);
}
}
};
exports.discardUntilSyncword = discardUntilSyncword;

View File

@@ -0,0 +1,24 @@
import type { AudioSampleOffset } from '../../state/audio-sample-map';
import type { Mp3BitrateInfo, Mp3Info, Mp3State } from '../../state/mp3';
import type { ParserState } from '../../state/parser-state';
import type { SamplesObservedState } from '../../state/samples-observed/slow-duration-fps';
import type { MediaSection, MediaSectionState } from '../../state/video-section';
export type Mp3SeekingHints = {
type: 'mp3-seeking-hints';
audioSampleMap: AudioSampleOffset[];
lastSampleObserved: boolean;
mp3BitrateInfo: Mp3BitrateInfo | null;
mp3Info: Mp3Info | null;
mediaSection: MediaSection | null;
contentLength: number;
};
export declare const getSeekingHintsForMp3: ({ mp3State, samplesObserved, mediaSectionState, contentLength, }: {
mp3State: Mp3State;
mediaSectionState: MediaSectionState;
samplesObserved: SamplesObservedState;
contentLength: number;
}) => Mp3SeekingHints;
export declare const setSeekingHintsForMp3: ({ hints, state, }: {
hints: Mp3SeekingHints;
state: ParserState;
}) => void;

View File

@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setSeekingHintsForMp3 = exports.getSeekingHintsForMp3 = void 0;
const getSeekingHintsForMp3 = ({ mp3State, samplesObserved, mediaSectionState, contentLength, }) => {
var _a;
return {
type: 'mp3-seeking-hints',
audioSampleMap: mp3State.audioSamples.getSamples(),
lastSampleObserved: samplesObserved.getLastSampleObserved(),
mp3BitrateInfo: mp3State.getMp3BitrateInfo(),
mp3Info: mp3State.getMp3Info(),
mediaSection: (_a = mediaSectionState.getMediaSections()[0]) !== null && _a !== void 0 ? _a : null,
contentLength,
};
};
exports.getSeekingHintsForMp3 = getSeekingHintsForMp3;
// TODO: could set xing data in the hints
const setSeekingHintsForMp3 = ({ hints, state, }) => {
state.mp3.audioSamples.setFromSeekingHints(hints.audioSampleMap);
};
exports.setSeekingHintsForMp3 = setSeekingHintsForMp3;