Make MP3 next frame location more strict for better recovery (fixes #382), fix Date timezone for ID3 tags

This commit is contained in:
Vanilagy
2026-05-22 11:31:26 +02:00
parent 149a2b4571
commit 85a80972a0
9 changed files with 70 additions and 26 deletions
+2 -2
View File
@@ -93,7 +93,7 @@ export const parseId3V1Tag = (slice: FileSlice, tags: MetadataTags) => {
const yearText = readId3V1String(slice, 4);
const year = Number.parseInt(yearText, 10);
if (Number.isInteger(year) && year > 0) {
tags.date ??= new Date(year, 0, 1);
tags.date ??= new Date(String(year)); // String so that it parses as UTC
}
const commentBytes = readBytes(slice, 30);
@@ -339,7 +339,7 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, tags: Metad
const year = Number.parseInt(yearText, 10);
if (Number.isInteger(year)) {
tags.date ??= new Date(year, 0, 1);
tags.date ??= new Date(String(year)); // String so that it parses as UTC
}
}; break;
+2 -2
View File
@@ -22,7 +22,7 @@ import {
} from './matroska/ebml';
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
import { Mp3Demuxer } from './mp3/mp3-demuxer';
import { FRAME_HEADER_SIZE, getXingOffset, INFO, XING } from '../shared/mp3-misc';
import { MP3_FRAME_HEADER_SIZE, getXingOffset, INFO, XING } from '../shared/mp3-misc';
import { ID3_V2_HEADER_SIZE, readId3V2Header } from './id3';
import { readNextMp3FrameHeader } from './mp3/mp3-reader';
import { OggDemuxer } from './ogg/ogg-demuxer';
@@ -330,7 +330,7 @@ export class Mp3InputFormat extends InputFormat {
// Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find
// another header right after it:
const secondResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + FRAME_HEADER_SIZE);
const secondResult = await readNextMp3FrameHeader(input._reader, currentPos, currentPos + MP3_FRAME_HEADER_SIZE);
if (!secondResult) {
return false;
}
+9 -10
View File
@@ -28,6 +28,7 @@ import {
XING,
XingFlags,
computeAverageMp3FrameSize,
getMp3ChannelCount,
} from '../../shared/mp3-misc';
import {
ID3_V1_TAG_SIZE,
@@ -108,7 +109,12 @@ export class Mp3Demuxer extends Demuxer {
}
}
const result = await readNextMp3FrameHeader(this.reader, this.lastLoadedPos, this.reader.fileSize);
const result = await readNextMp3FrameHeader(
this.reader,
this.lastLoadedPos,
this.reader.fileSize,
this.firstFrameHeader,
);
if (!result) {
this.lastSampleLoaded = true;
return;
@@ -157,13 +163,6 @@ export class Mp3Demuxer extends Demuxer {
this.firstFrameHeaderPos = result.startPos;
}
if (header.sampleRate !== this.firstFrameHeader.sampleRate) {
console.warn(
`MP3 changed sample rate mid-file: ${this.firstFrameHeader.sampleRate} Hz to ${header.sampleRate} Hz.`
+ ` Might be a bug, so please report this file.`,
);
}
const sampleDuration = header.audioSamplesInFrame / this.firstFrameHeader.sampleRate;
const sample: Sample = {
timestamp: this.nextTimestampInSamples / this.firstFrameHeader.sampleRate,
@@ -332,7 +331,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
getNumberOfChannels() {
assert(this.demuxer.firstFrameHeader);
return this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2;
return getMp3ChannelCount(this.demuxer.firstFrameHeader.channel);
}
getSampleRate() {
@@ -351,7 +350,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
return {
codec: 'mp3',
numberOfChannels: this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2,
numberOfChannels: getMp3ChannelCount(this.demuxer.firstFrameHeader.channel),
sampleRate: this.demuxer.firstFrameHeader.sampleRate,
};
}
+21 -6
View File
@@ -6,10 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { FRAME_HEADER_SIZE, Mp3FrameHeader, readMp3FrameHeader } from '../../shared/mp3-misc';
import { MP3_FRAME_HEADER_SIZE, getMp3ChannelCount, Mp3FrameHeader, readMp3FrameHeader } from '../../shared/mp3-misc';
import { Reader, readU32Be } from '../reader';
export const readNextMp3FrameHeader = async (reader: Reader, startPos: number, until: number | null): Promise<{
export const readNextMp3FrameHeader = async (
reader: Reader,
startPos: number,
until: number | null,
ref: Mp3FrameHeader | null = null,
): Promise<{
header: Mp3FrameHeader;
startPos: number;
} | null> => {
@@ -21,11 +26,11 @@ export const readNextMp3FrameHeader = async (reader: Reader, startPos: number, u
? Math.min(CHUNK_SIZE, until - currentPos)
: CHUNK_SIZE;
let slice = reader.requestSliceRange(currentPos, FRAME_HEADER_SIZE, maxLength);
let slice = reader.requestSliceRange(currentPos, MP3_FRAME_HEADER_SIZE, maxLength);
if (slice instanceof Promise) slice = await slice;
if (!slice || slice.length < FRAME_HEADER_SIZE) break;
if (!slice || slice.length < MP3_FRAME_HEADER_SIZE) break;
while (slice.remainingLength >= FRAME_HEADER_SIZE) {
while (slice.remainingLength >= MP3_FRAME_HEADER_SIZE) {
const posBeforeRead = slice.filePos;
const word = readU32Be(slice);
const remainingBytes = reader.fileSize !== null
@@ -33,7 +38,17 @@ export const readNextMp3FrameHeader = async (reader: Reader, startPos: number, u
: null;
const result = readMp3FrameHeader(word, remainingBytes);
if (result.header) {
if (
result.header
&& (!ref || (
// This condition helps us recover malformed streams
// https://stackoverflow.com/a/20884944
result.header.sampleRate === ref.sampleRate
&& result.header.mpegVersionId === ref.mpegVersionId
&& result.header.layer === ref.layer
&& getMp3ChannelCount(result.header.channel) === getMp3ChannelCount(ref.channel)
))
) {
return { header: result.header, startPos: currentPos };
}
+6 -2
View File
@@ -63,7 +63,11 @@ import {
TRANSFER_CHARACTERISTICS_MAP_INVERSE,
UNDETERMINED_LANGUAGE,
} from '../misc';
import { FRAME_HEADER_SIZE as MP3_FRAME_HEADER_SIZE, readMp3FrameHeader } from '../../shared/mp3-misc';
import {
MP3_FRAME_HEADER_SIZE,
getMp3ChannelCount,
readMp3FrameHeader,
} from '../../shared/mp3-misc';
import { EncodedPacket, PacketType, PLACEHOLDER_DATA } from '../packet';
import { FileSlice, readBytes, Reader, readU16Be, readU32Be, readU8 } from '../reader';
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
@@ -629,7 +633,7 @@ export class MpegTsDemuxer extends Demuxer {
);
}
elementaryStream.info.numberOfChannels = result.header.channel === 3 ? 1 : 2;
elementaryStream.info.numberOfChannels = getMp3ChannelCount(result.header.channel);
elementaryStream.info.sampleRate = result.header.sampleRate;
} else if (elementaryStream.info.codec === 'ac3') {
const frameInfo = parseAc3SyncFrame(context.suppliedPacket.data);