Skip to content

Commit 1d0f7bf

Browse files
authored
fix: MP4 parser is more robust (#55)
* add a file cursor so we can track the position of the buffer in the file * add a cursor to track the position in the file * change the doc of the ID3v2 * use buffer and fix the data boxes * a bit of doc to explain the change * remove print
1 parent 5eedca0 commit 1d0f7bf

File tree

5 files changed

+84
-57
lines changed

5 files changed

+84
-57
lines changed

lib/src/parsers/id3v2.dart

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -290,20 +290,18 @@ class ID3v2Parser extends TagParser {
290290
return metadata;
291291
}
292292

293-
/**
294-
* Search and return the first MP3 frame header.
295-
* Returns null if none has been found.
296-
*
297-
* The MP3 frame has a magic word : 0xFFF or 0xFFE
298-
*
299-
* Sometimes the MP3 files contains blocks of 0x00 or 0xFF and relying on the magic word
300-
* is not reliable anymore.
301-
*
302-
* To prevent false positives, we need to verify that the bytes after the potential
303-
* valid word are correct. The MP3 specs specify several flags that must be set or not.
304-
*
305-
* Credit to [exiftool](https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/MPEG.pm#L464)
306-
*/
293+
/// Search and return the first MP3 frame header.
294+
/// Returns null if none has been found.
295+
///
296+
/// The MP3 frame has a magic word : 0xFFF or 0xFFE
297+
///
298+
/// Sometimes the MP3 files contains blocks of 0x00 or 0xFF and relying on the magic word
299+
/// is not reliable anymore.
300+
///
301+
/// To prevent false positives, we need to verify that the bytes after the potential
302+
/// valid word are correct. The MP3 specs specify several flags that must be set or not.
303+
///
304+
/// Credit to [exiftool](https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/MPEG.pm#L464)
307305
Uint8List? _findFirstMp3Frame(Buffer buffer) {
308306
Uint8List frameHeader = buffer.readAtMost(4);
309307

lib/src/parsers/mp4.dart

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:typed_data';
44

55
import 'package:audio_metadata_reader/src/metadata/mp4_metadata.dart';
66
import 'package:audio_metadata_reader/src/parsers/tag_parser.dart';
7+
import 'package:audio_metadata_reader/src/utils/buffer.dart';
78
import 'package:mime/mime.dart';
89

910
import '../../audio_metadata_reader.dart';
@@ -68,24 +69,26 @@ final supportedBox = [
6869
///
6970
class MP4Parser extends TagParser {
7071
Mp4Metadata tags = Mp4Metadata();
72+
late final Buffer buffer;
7173

7274
MP4Parser({fetchImage = false}) : super(fetchImage: fetchImage);
7375

7476
@override
7577
ParserTag parse(RandomAccessFile reader) {
7678
reader.setPositionSync(0);
79+
buffer = Buffer(randomAccessFile: reader);
7780

7881
final lengthFile = reader.lengthSync();
7982

80-
while (reader.positionSync() < lengthFile) {
81-
final box = _readBox(reader);
83+
while (buffer.fileCursor < lengthFile) {
84+
final box = _readBox(buffer);
8285

8386
if (supportedBox.contains(box.type)) {
84-
processBox(reader, box);
87+
processBox(buffer, box);
8588
} else {
8689
// We substract 8 to the box size because we already read the data for
8790
// the box header
88-
reader.setPositionSync(reader.positionSync() + box.size - 8);
91+
buffer.skip(box.size - 8);
8992
}
9093
}
9194

@@ -100,8 +103,8 @@ class MP4Parser extends TagParser {
100103
/// [0...3] -> box size (header + body)
101104
/// [4...7] -> box name (ASCII)
102105
///
103-
BoxHeader _readBox(RandomAccessFile reader) {
104-
final headerBytes = reader.readSync(8);
106+
BoxHeader _readBox(Buffer buffer) {
107+
final headerBytes = buffer.read(8);
105108
final parser = ByteData.sublistView(headerBytes);
106109

107110
final boxSize = parser.getUint32(0);
@@ -119,43 +122,48 @@ class MP4Parser extends TagParser {
119122
return BoxHeader(boxSize, String.fromCharCodes(boxNameBytes));
120123
}
121124

122-
///
123125
/// Parse a box
124126
///
125127
/// The metadata are inside special boxes. We only read data when we need it
126128
/// otherwise we skip them
127-
///
128-
void processBox(RandomAccessFile reader, BoxHeader box) {
129+
void processBox(Buffer buffer, BoxHeader box) {
129130
if (box.type == "moov") {
130-
parseRecurvise(reader, box);
131+
parseRecurvise(buffer, box);
131132
} else if (box.type == "mvhd") {
132-
final bytes = reader.readSync(100);
133+
final bytes = buffer.read(100);
133134

134135
final timeScale = getUint32(bytes.sublist(12, 16));
135136
final timeUnit = getUint32(bytes.sublist(16, 20));
136137

137138
double microseconds = (timeUnit / timeScale) * 1000000;
138139
tags.duration = Duration(microseconds: microseconds.toInt());
139140
} else if (box.type == "udta") {
140-
parseRecurvise(reader, box);
141+
parseRecurvise(buffer, box);
141142
} else if (box.type == "ilst") {
142-
parseRecurvise(reader, box);
143+
parseRecurvise(buffer, box);
143144
} else if (["trak", "mdia", "minf", "stbl", "stsd"].contains(box.type)) {
144-
parseRecurvise(reader, box);
145+
parseRecurvise(buffer, box);
145146
} else if (box.type == "meta") {
146-
reader.readSync(4);
147+
buffer.read(4);
147148

148-
parseRecurvise(reader, box);
149+
parseRecurvise(buffer, box);
149150
} else if (box.type[0] == "©" ||
150151
["gnre", "trkn", "disk", "tmpo", "cpil", "too", "covr", "pgap", "gen"]
151152
.contains(box.type)) {
152-
final bytes = reader.readSync(box.size - 8);
153+
final metadataValue = buffer.read(box.size - 8);
154+
155+
// sometimes the data is stored inside another box called `data`
156+
// we try to find out if the data contains the box type "data" (0:4 is the box size)
157+
// otherwise we just skip the Apple's tag of 4 chars
158+
final data = (String.fromCharCodes(metadataValue.sublist(4, 8)) == "data")
159+
? metadataValue.sublist(16)
160+
: metadataValue.sublist(4);
153161

154162
String value;
155163
try {
156-
value = const Utf8Decoder().convert(bytes.sublist(16));
164+
value = utf8.decode(data);
157165
} catch (e) {
158-
value = String.fromCharCodes(bytes.sublist(16));
166+
value = latin1.decode(data);
159167
}
160168

161169
final boxName = (box.type[0] == "©") ? box.type.substring(1) : box.type;
@@ -190,37 +198,37 @@ class MP4Parser extends TagParser {
190198
case "too":
191199
break;
192200
case "disk":
193-
tags.discNumber = getUint16(bytes.sublist(18, 20));
194-
tags.totalDiscs = getUint16(bytes.sublist(20, 22));
201+
tags.discNumber = getUint16(data.sublist(2, 4));
202+
tags.totalDiscs = getUint16(data.sublist(4, 6));
195203
break;
196204

197205
case "covr":
198206
if (fetchImage) {
199-
final imageData = bytes.sublist(16);
207+
final imageData = data;
200208
tags.picture = Picture(
201209
imageData,
202210
lookupMimeType("no path", headerBytes: imageData) ?? "",
203211
PictureType.coverFront);
204212
}
205213
case "trkn":
206-
final a = getUint16(bytes.sublist(18, 20));
207-
final totalTracks = getUint16(bytes.sublist(20, 22));
214+
final a = getUint16(data.sublist(2, 4));
215+
final totalTracks = getUint16(data.sublist(4, 6));
208216
tags.totalTracks = totalTracks;
209217
if (a > 0) {
210218
tags.trackNumber = a;
211219
}
212220
break;
213221
}
214222
} else if (box.type == "----") {
215-
final mean = _readBox(reader);
216-
String.fromCharCodes(reader.readSync(mean.size - 8)); // mean value
223+
final mean = _readBox(buffer);
224+
String.fromCharCodes(buffer.read(mean.size - 8)); // mean value
217225

218-
final name = _readBox(reader);
226+
final name = _readBox(buffer);
219227

220228
final nameValue =
221-
String.fromCharCodes(reader.readSync(name.size - 8).sublist(4));
222-
final dataBox = _readBox(reader);
223-
final data = reader.readSync(dataBox.size - 8);
229+
String.fromCharCodes(buffer.read(name.size - 8).sublist(4));
230+
final dataBox = _readBox(buffer);
231+
final data = buffer.read(dataBox.size - 8);
224232
final finalValue = String.fromCharCodes(data.sublist(8));
225233

226234
switch (nameValue) {
@@ -233,19 +241,17 @@ class MP4Parser extends TagParser {
233241
default:
234242
}
235243
} else if (box.type == "mp4a") {
236-
final bytes = reader.readSync(box.size - 8);
244+
final bytes = buffer.read(box.size - 8);
237245

238246
// tags.bitrate = timeScale;
239247
tags.sampleRate = getUint32(bytes.sublist(22, 26));
240248
} else {
241-
reader.setPositionSync(reader.positionSync() + box.size - 8);
249+
buffer.setPositionSync(buffer.fileCursor + box.size - 8);
242250
}
243251
}
244252

245-
///
246253
/// Parse a box with multiple sub boxes.
247-
///
248-
void parseRecurvise(RandomAccessFile reader, BoxHeader box) {
254+
void parseRecurvise(Buffer buffer, BoxHeader box) {
249255
final limit = box.size - 8;
250256
int offset = 0;
251257

@@ -254,26 +260,24 @@ class MP4Parser extends TagParser {
254260
offset += 4;
255261
} else if (box.type == "stsd") {
256262
offset += 8;
257-
reader.readSync(8);
263+
buffer.read(8);
258264
}
259265

260266
while (offset < limit) {
261-
final newBox = _readBox(reader);
267+
final newBox = _readBox(buffer);
262268

263269
if (supportedBox.contains(newBox.type)) {
264-
processBox(reader, newBox);
270+
processBox(buffer, newBox);
265271
} else {
266-
reader.setPositionSync(reader.positionSync() + newBox.size - 8);
272+
buffer.skip(newBox.size - 8);
267273
}
268274

269275
offset += newBox.size;
270276
}
271277
}
272278

273-
///
274279
/// To detect if this parser can be used to parse this file, we need to detect
275280
/// the first box. It should be a `ftyp` box
276-
///
277281
static bool canUserParser(RandomAccessFile reader) {
278282
reader.setPositionSync(4);
279283

lib/src/utils/buffer.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,18 @@ import '../../audio_metadata_reader.dart';
77
class Buffer {
88
final RandomAccessFile randomAccessFile;
99
final Uint8List _buffer;
10+
11+
/// It's like positionSync() but adapted with the buffer
12+
int fileCursor = 0;
13+
14+
/// Position of the cursor in the buffer of size [_bufferSize]
1015
int _cursor = 0;
11-
int _bufferedBytes = 0; // Track how many bytes are actually in the buffer
16+
17+
/// Track how many bytes are actually in the buffer
18+
int _bufferedBytes = 0;
19+
20+
/// The buffer size is always a power of 2.
21+
/// To reach good performance, we need at least 4096
1222
static final int _bufferSize = 16384;
1323

1424
/// The number of bytes remaining to be read from the file.
@@ -39,6 +49,8 @@ class Buffer {
3949
}
4050

4151
Uint8List read(int size) {
52+
fileCursor += size;
53+
4254
// if we read something big (~100kb), we can read it directly from file
4355
// it makes the read faster
4456
// no need to use the buffer
@@ -101,6 +113,7 @@ class Buffer {
101113
}
102114

103115
void setPositionSync(int position) {
116+
fileCursor = position;
104117
randomAccessFile.setPositionSync(position);
105118
_fill();
106119
}
@@ -110,11 +123,14 @@ class Buffer {
110123
final remainingInBuffer = _bufferedBytes - _cursor;
111124

112125
if (length <= remainingInBuffer) {
126+
fileCursor += length;
127+
113128
// If we can skip within the current buffer, just move the cursor
114129
_cursor += length;
115130
} else {
116131
// Calculate the actual file position we need to skip to
117132
int currentPosition = randomAccessFile.positionSync() - remainingInBuffer;
133+
fileCursor += currentPosition;
118134
// Skip to the new position
119135
randomAccessFile.setPositionSync(currentPosition + length);
120136
// Refill the buffer at the new position

test/mp4/mp4_test.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,17 @@ void main() {
4444
expect(result.title, "Sexy Ladies (Remix) [feat. 50 Cent]");
4545
expect(result.album, "FutureSex/LoveSounds (Deluxe Edition)");
4646
expect(result.artist, "Justin Timberlake");
47-
expect(result.year, (DateTime.utc(2006, 9, 12)));
47+
expect(result.year, DateTime.utc(2006, 9, 12));
4848
expect(result.trackNumber, 15);
4949
expect(result.trackTotal, 15);
5050
});
51+
52+
test("Should work with .mov file", () {
53+
final track = File('./test/mp4/track.mov');
54+
final result = readMetadata(track, getImage: false);
55+
56+
expect(result.title, "Blue Test Pattern");
57+
expect(result.artist, "FFmpeg Generator");
58+
expect(result.year, (DateTime(2023, 10, 27)));
59+
});
5160
}

test/mp4/track.mov

9.25 KB
Binary file not shown.

0 commit comments

Comments
 (0)