• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

benrr101 / node-taglib-sharp / 51046570

24 Nov 2024 06:12AM UTC coverage: 92.543% (-0.01%) from 92.553%
51046570

push

appveyor

benrr101
Fix MPEG audio stream detection
ALSO UNCOVER A HUGE BUG IN APE FILE INTEGRATION TESTS??? WTF

3246 of 4127 branches covered (78.65%)

Branch coverage included in aggregate %.

35 of 42 new or added lines in 6 files covered. (83.33%)

1 existing line in 1 file now uncovered.

26750 of 28286 relevant lines covered (94.57%)

471.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.68
/src/mpeg/mpegContainerFile.ts
1
import MpegContainerFileSettings from "./mpegContainerFileSettings";
1✔
2
import MpegAudioHeader from "./mpegAudioHeader";
1✔
3
import MpegVideoHeader from "./mpegVideoHeader";
1✔
4
import SandwichFile from "../sandwich/sandwichFile";
1✔
5
import {ByteVector} from "../byteVector";
1✔
6
import {CorruptFileError, UnsupportedFormatError} from "../errors";
1✔
7
import {File, ReadStyle} from "../file";
1✔
8
import {IFileAbstraction} from "../fileAbstraction";
9
import {MpegVersion} from "./mpegEnums";
1✔
10
import {Properties} from "../properties";
1✔
11
import {TagTypes} from "../tag";
1✔
12
import {NumberUtils} from "../utils";
1✔
13

14
/**
15
 * Indicates the type of marker found in an MPEG file.
16
 */
17
enum MpegFileMarker {
1✔
18
    /**
19
     * An invalid marker.
20
     */
21
    Corrupt = -1,
1✔
22

23
    /**
24
     * A zero value marker.
25
     */
26
    Zero = 0,
1✔
27

28
    /**
29
     * A marker indicating a system sync packet.
30
     */
31
    SystemSyncPacket = 0xBA,
1✔
32

33
    /**
34
     * A marker indicating a video sync packet.
35
     */
36
    VideoSyncPacket = 0xB3,
1✔
37

38
    /**
39
     * A marker indicating a system packet.
40
     */
41
    SystemPacket = 0xBB,
1✔
42

43
    /**
44
     * A marker indicating a padding packet.
45
     */
46
    PaddingPacket = 0xBE,
1✔
47

48
    /**
49
     * A marker indicating an audio packet.
50
     */
51
    AudioPacket = 0xC0,
1✔
52

53
    /**
54
     * A marker indicating a video packet.
55
     */
56
    VideoPacket = 0xE0,
1✔
57

58
    /**
59
     * A marker indicating the end of a stream.
60
     */
61
    EndOfStream = 0xB9
1✔
62
}
63

64
/**
65
 * This class extends {@link SandwichFile} to provide tagging and properties support for
66
 * MPEG-1, MPEG-2, and MPEG-2.5 containerized video files.
67
 */
68
export default class MpegContainerFile extends SandwichFile {
1✔
69
    private static readonly DEFAULT_TAG_LOCATION_MAPPING = new Map<TagTypes, () => boolean>([
1✔
70
        [TagTypes.Ape, () => true],
1✔
71
        [TagTypes.Id3v1, () => true],
1✔
72
        [TagTypes.Id3v2, () => true]
1✔
73
    ]);
74
    private static readonly MARKER_START = ByteVector.fromByteArray([0, 0, 1]);
1✔
75

76
    private _audioFound = false;
2✔
77
    private _audioHeader: MpegAudioHeader;
78
    private _endTime: number;
79
    private _startTime: number | undefined;
80
    private _version: MpegVersion;
81
    private _videoFound = false;
2✔
82
    private _videoHeader: MpegVideoHeader;
83

84
    public constructor(file: IFileAbstraction|string, propertiesStyle: ReadStyle) {
85
        super(
2✔
86
            file,
87
            propertiesStyle,
88
            MpegContainerFile.DEFAULT_TAG_LOCATION_MAPPING,
89
            MpegContainerFileSettings.defaultTagTypes
90
        );
91
    }
92

93
    /** @inheritDoc */
94
    protected readProperties(readStyle: ReadStyle): Properties {
95
        // Skip processing if we aren't supposed to read the properties
96
        if (!NumberUtils.hasFlag(readStyle, ReadStyle.Average)) {
2!
97
            return;
2✔
98
        }
99

100
        // Read the audio and video properties
101
        const firstSyncPosition = this.findNextMarkerPosition(this.mediaStartPosition, MpegFileMarker.SystemSyncPacket);
×
102
        this.readSystemFile(firstSyncPosition);
×
103

104
        const codecs = [];
×
105
        if (this._videoHeader) { codecs.push(this._videoHeader); }
×
106
        if (this._audioHeader) { codecs.push(this._audioHeader); }
×
107

108
        // @TODO: Can we omit calculating duration via start/end timestamps if we have audio with
109
        //     a valid duration?
110
        // Calculate duration of the file
111
        const lastSyncPosition = this.rFindMarkerPosition(
×
112
            this.length - this.mediaEndPosition,
113
            MpegFileMarker.SystemSyncPacket
114
        );
115
        this._endTime = this.readTimestamp(lastSyncPosition + 4);
×
116
        const durationMilliseconds = this._startTime === undefined
×
117
            ? (this._audioHeader ? this._audioHeader.durationMilliseconds : 0)
×
118
            : (this._endTime - this._startTime) * 1000;
119

120
        return new Properties(durationMilliseconds, codecs);
×
121
    }
122

123
    // #region Private Methods
124

125
    private findFirstMarker(position: number): {marker: MpegFileMarker, position: number} {
126
        position = this.find(MpegContainerFile.MARKER_START, position);
×
127
        if (position < 0) {
×
128
            throw new CorruptFileError("Marker not found");
×
129
        }
130

131
        return {
×
132
            marker: this.getMarker(position),
133
            position: position
134
        };
135
    }
136

137
    private findNextMarkerPosition(position: number, marker: MpegFileMarker): number {
138
        const packet = ByteVector.concatenate(
×
139
            MpegContainerFile.MARKER_START,
140
            marker
141
        );
142
        position = this.find(packet, position);
×
143

144
        if (position < 0) {
×
145
            throw new CorruptFileError("Marker not found");
×
146
        }
147

148
        return position;
×
149
    }
150

151
    private getMarker(position: number): MpegFileMarker {
152
        this.seek(position);
×
153
        const identifier = this.readBlock(4);
×
154

155
        if (identifier.length === 4 && identifier.startsWith(MpegContainerFile.MARKER_START)) {
×
156
            return identifier.get(3);
×
157
        }
158

159
        throw new CorruptFileError(`Invalid marker at position ${position}`);
×
160
    }
161

162
    private readAudioPacket(position: number): number {
163
        this.seek(position + 4);
×
164
        const headerBytes = this.readBlock(21);
×
165
        const length = headerBytes.subarray(0, 2).toUshort();
×
166
        const returnValue = position + length;
×
167

168
        if (this._audioFound) {
×
169
            return returnValue;
×
170
        }
171

172
        // There is a maximum of 16 stuffing bytes, read to the PTS/DTS flags
173
        const packetHeaderBytes = headerBytes.subarray(2, 19);
×
174
        let i = 0;
×
175
        while (i < packetHeaderBytes.length && packetHeaderBytes.get(i) === 0xFF) {
×
176
            // Byte is a stuffing byte
177
            i++;
×
178
        }
179

180
        if (NumberUtils.hasFlag(packetHeaderBytes.get(i), 0x40 )) {
×
181
            // STD buffer size is unexpected for audio packets, but whatever
182
            i++;
×
183
        }
184

185
        // Decode the PTS/DTS flags
186
        const timestampFlags = packetHeaderBytes.get(i);
×
187
        const dataOffset = 4 + 2 + i                 // Packet marker + packet length + stuffing bytes/STD buffer size
×
188
            + (NumberUtils.hasFlag(timestampFlags, 0x20) ? 4 : 0)  // Presentation timestamp
×
189
            + (NumberUtils.hasFlag(timestampFlags, 0x10) ? 4 : 0); // Decode timestamp
×
190

191
        // Decode the MPEG audio header
NEW
192
        const searchStart = position + dataOffset;
×
NEW
193
        const searchEnd = searchStart + length - 9;
×
NEW
194
        this._audioHeader = MpegAudioHeader.fromFile(this, searchStart, searchEnd);
×
UNCOV
195
        this._audioFound = !!this._audioHeader;
×
196

197
        return position + length;
×
198
    }
199

200
    private readSystemFile(position: number): void {
201
        const sanityLimit = 100;
×
202

203
        for (
×
204
            let i = 0;
×
205
            i < sanityLimit && (this._startTime === undefined || !this._audioFound || !this._videoFound);
×
206
            i++
207
        ) {
208
            const markerResult = this.findFirstMarker(position);
×
209
            position = markerResult.position;
×
210

211
            switch (markerResult.marker) {
×
212
                case MpegFileMarker.SystemSyncPacket:
×
213
                    position = this.readSystemSyncPacket(position);
×
214
                    break;
×
215
                case MpegFileMarker.SystemPacket:
216
                case MpegFileMarker.PaddingPacket:
217
                    this.seek(position + 4);
×
218
                    position += this.readBlock(2).toUshort() + 6;
×
219
                    break;
×
220
                case MpegFileMarker.VideoPacket:
221
                    position = this.readVideoPacket(position);
×
222
                    break;
×
223
                case MpegFileMarker.AudioPacket:
224
                    position = this.readAudioPacket(position);
×
225
                    break;
×
226
                case MpegFileMarker.EndOfStream:
227
                    return;
×
228
                default:
229
                    position += 4;
×
230
                    break;
×
231
            }
232
        }
233
    }
234

235
    private readSystemSyncPacket(position: number): number {
236
        let packetSize = 0;
×
237
        this.seek(position + 4);
×
238

239
        const versionInfo = this.readBlock(1).get(0);
×
240
        if (NumberUtils.uintAnd(versionInfo, 0xF0) === 0x20) {
×
241
            this._version = MpegVersion.Version1;
×
242
            packetSize = 12;
×
243
        } else if (NumberUtils.uintAnd(versionInfo, 0xC0) === 0x40) {
×
244
            this._version = MpegVersion.Version2;
×
245
            this.seek(position + 13);
×
246
            packetSize = 14 + NumberUtils.uintAnd(this.readBlock(1).get(0), 0x07);
×
247
        } else {
248
            throw new UnsupportedFormatError("Unknown MPEG version");
×
249
        }
250

251
        if (this._startTime === undefined) {
×
252
            this._startTime = this.readTimestamp(position + 4);
×
253
        }
254

255
        return position + packetSize;
×
256
    }
257

258
    private readTimestamp(position: number): number {
259
        let high: number;
260
        let low: number;
261

262
        this.seek(position);
×
263
        if (this._version === MpegVersion.Version1) {
×
264
            const data = this.readBlock(5);
×
265
            high = NumberUtils.uintAnd(NumberUtils.uintRShift(data.get(0), 3), 0x01);
×
266
            low = NumberUtils.uintOr(
×
267
                NumberUtils.uintLShift(NumberUtils.uintAnd(NumberUtils.uintRShift(data.get(0), 1), 0x03), 30),
268
                NumberUtils.uintLShift(data.get(1), 22),
269
                NumberUtils.uintLShift(NumberUtils.uintRShift(data.get(2), 1), 15),
270
                NumberUtils.uintLShift(data.get(3), 7),
271
                NumberUtils.uintRShift(data.get(4), 1)
272
            );
273
        } else {
274
            const data = this.readBlock(6);
×
275
            high = NumberUtils.uintRShift(NumberUtils.uintAnd(data.get(0), 0x20), 5);
×
276
            low = NumberUtils.uintOr(
×
277
                NumberUtils.uintLShift(NumberUtils.uintAnd(data.get(0), 0x03), 28),
278
                NumberUtils.uintLShift(data.get(1), 20),
279
                NumberUtils.uintLShift(NumberUtils.uintAnd(data.get(2), 0xF8), 12),
280
                NumberUtils.uintLShift(NumberUtils.uintAnd(data.get(2), 0x03), 13),
281
                NumberUtils.uintLShift(data.get(3), 5),
282
                NumberUtils.uintRShift(data.get(4), 3)
283
            );
284
        }
285

286
        return (high * 0x10000 * 0x10000 + low) / 90000;
×
287
    }
288

289
    private readVideoPacket(position: number): number {
290
        this.seek(position + 4);
×
291
        const length = this.readBlock(2).toUshort();
×
292
        let offset = position + 6;
×
293

294
        while (!this._videoFound && offset < position + length) {
×
295
            const markerResult = this.findFirstMarker(offset);
×
296
            offset = markerResult.position;
×
297
            if (markerResult.marker === MpegFileMarker.VideoSyncPacket) {
×
298
                this._videoHeader = new MpegVideoHeader(this, offset + 4);
×
299
                this._videoFound = true;
×
300
            } else {
301
                // Advance the offset by 6 bytes, so the next iteration of the loop won't find the
302
                // same marker and get stuck. 6 bytes because findFirstMarker is a generic find
303
                // that found get both PES packets and stream packets, the smallest possible PES
304
                // packet with a size of 0 would be 6 bytes.
305
                offset += 6;
×
306
            }
307
        }
308

309
        return position + length;
×
310
    }
311

312
    private rFindMarkerPosition(position: number, marker: MpegFileMarker): number {
313
        const packet = ByteVector.concatenate(
×
314
            MpegContainerFile.MARKER_START,
315
            marker
316
        );
317
        position = this.rFind(packet, position);
×
318

319
        if (position < 0) {
×
320
            throw new CorruptFileError("Marker not found");
×
321
        }
322

323
        return position;
×
324
    }
325

326
    // #endregion
327
}
328

329
// /////////////////////////////////////////////////////////////////////////
330
// Register the file type
331
[
1✔
332
    "taglib/mpg",
333
    "taglib/mpeg",
334
    "taglib/mpe",
335
    "taglib/mpv2",
336
    "taglib/m2v",
337
    "video/x-mpg",
338
    "video/mpeg"
339
].forEach((mt) => File.addFileType(mt, MpegContainerFile));
7✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc