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

benrr101 / node-taglib-sharp / 46462135

pending completion
46462135

push

appveyor

Benjamin Russell
Merge branch 'release/v5.1.0'

3096 of 3788 branches covered (81.73%)

Branch coverage included in aggregate %.

2171 of 2171 new or added lines in 47 files covered. (100.0%)

25320 of 26463 relevant lines covered (95.68%)

437.0 hits per line

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

19.89
/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
192
        this._audioHeader = MpegAudioHeader.find(this, position + dataOffset, length - 9);
×
193
        this._audioFound = !!this._audioHeader;
×
194

195
        return position + length;
×
196
    }
197

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

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

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

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

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

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

253
        return position + packetSize;
×
254
    }
255

256
    private readTimestamp(position: number): number {
257
        let high: number;
258
        let low: number;
259

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

284
        return (high * 0x10000 * 0x10000 + low) / 90000;
×
285
    }
286

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

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

307
        return position + length;
×
308
    }
309

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

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

321
        return position;
×
322
    }
323

324
    // #endregion
325
}
326

327
// /////////////////////////////////////////////////////////////////////////
328
// Register the file type
329
[
1✔
330
    "taglib/mpg",
331
    "taglib/mpeg",
332
    "taglib/mpe",
333
    "taglib/mpv2",
334
    "taglib/m2v",
335
    "video/x-mpg",
336
    "video/mpeg"
337
].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