Files
shaka-player/test/util/id3_utils_unit.js
T
Álvaro Velad Galván 67b0a177b4 feat: Add support for ID3v1 and IDv2.3 (#10108)
v2.3 support is required for some HLS streams.
v1 support is not yet needed in this pull request, but it may be in a
future one.
2026-05-20 10:35:29 +02:00

483 lines
12 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('Id3Utils', () => {
const Id3Utils = shaka.util.Id3Utils;
const Id3Generator = shaka.test.Id3Generator;
const BufferUtils = shaka.util.BufferUtils;
function utf16le(str) {
const out = [];
for (const c of str) {
const code = c.charCodeAt(0);
out.push(code & 0xff);
out.push(code >> 8);
}
return out;
}
it('no valid data produces empty output', () => {
expect(Id3Utils.getID3Frames(new Uint8Array([]))).toEqual([]);
});
it('parse an APIC frame with image data', () => {
const apicValue = new Uint8Array([
3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 3, 83, 104, 97,
107, 97, 0, 1, 2, 3,
]);
const apicFrame = Id3Generator.generateId3Frame('APIC', apicValue);
const apicID3 = Id3Generator.generateId3(apicFrame);
const expectedID3 = [
{
key: 'APIC',
mimeType: 'image/jpeg',
pictureType: 3,
description: 'Shaka',
data: BufferUtils.toArrayBuffer(new Uint8Array([1, 2, 3])),
},
];
expect(Id3Utils.getID3Frames(apicID3)).toEqual(expectedID3);
});
it('parse an APIC frame with image URL', () => {
const apicValue = new Uint8Array([
3, 45, 45, 62, 0, 3, 83, 104, 97, 107, 97, 0, 103, 111, 111, 103, 108,
101, 46, 99, 111, 109,
]);
const apicFrame = Id3Generator.generateId3Frame('APIC', apicValue);
const apicID3 = Id3Generator.generateId3(apicFrame);
const expectedID3 = [
{
key: 'APIC',
mimeType: '-->',
pictureType: 3,
description: 'Shaka',
data: 'google.com',
},
];
expect(Id3Utils.getID3Frames(apicID3)).toEqual(expectedID3);
});
it('parse a TXXX frame', () => {
const txxxValue = new Uint8Array([3, 65, 0, 83, 104, 97, 107, 97]);
const txxxFrame = Id3Generator.generateId3Frame('TXXX', txxxValue);
const txxxID3 = Id3Generator.generateId3(txxxFrame);
const expectedID3 = [
{
key: 'TXXX',
description: 'A',
data: 'Shaka',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(txxxID3)).toEqual(expectedID3);
});
it('parse a TXXX frame with extended header', () => {
const txxxValue = new Uint8Array([3, 65, 0, 83, 104, 97, 107, 97]);
const txxxFrame = Id3Generator.generateId3Frame('TXXX', txxxValue);
const txxxID3 = Id3Generator.generateId3(txxxFrame, true);
const expectedID3 = [
{
key: 'TXXX',
description: 'A',
data: 'Shaka',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(txxxID3)).toEqual(expectedID3);
});
it('parse a TCOP frame', () => {
const tcopValue = new Uint8Array(
[3, 83, 104, 97, 107, 97, 32, 50, 48, 49, 54]);
const tcopFrame = Id3Generator.generateId3Frame('TCOP', tcopValue);
const tcopID3 = Id3Generator.generateId3(tcopFrame);
const expectedID3 = [
{
key: 'TCOP',
description: '',
data: 'Shaka 2016',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(tcopID3)).toEqual(expectedID3);
});
it('parse a WXXX frame', () => {
const wxxxValue = new Uint8Array(
[3, 65, 0, 103, 111, 111, 103, 108, 101, 46, 99, 111, 109]);
const wxxxFrame = Id3Generator.generateId3Frame('WXXX', wxxxValue);
const wxxxID3 = Id3Generator.generateId3(wxxxFrame);
const expectedID3 = [
{
key: 'WXXX',
description: 'A',
data: 'google.com',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(wxxxID3)).toEqual(expectedID3);
});
it('parse a WCOP frame', () => {
const wcopValue = new Uint8Array(
[103, 111, 111, 103, 108, 101, 46, 99, 111, 109]);
const wcopFrame = Id3Generator.generateId3Frame('WCOP', wcopValue);
const wcopID3 = Id3Generator.generateId3(wcopFrame);
const expectedID3 = [
{
key: 'WCOP',
description: '',
data: 'google.com',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(wcopID3)).toEqual(expectedID3);
});
it('parse a PRIV frame', () => {
const privValue = new Uint8Array([65, 0, 83, 104, 97, 107]);
const privFrame = Id3Generator.generateId3Frame('PRIV', privValue);
const privID3 = Id3Generator.generateId3(privFrame);
const expectedID3 = [
{
key: 'PRIV',
description: 'A',
data: BufferUtils.toArrayBuffer(new Uint8Array([83, 104, 97, 107])),
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(privID3)).toEqual(expectedID3);
});
it('parse an unknown frame', () => {
const unknownValue = new Uint8Array([83, 104, 97, 107]);
const unknownFrame = Id3Generator.generateId3Frame('XXXX', unknownValue);
const unknownID3 = Id3Generator.generateId3(unknownFrame);
const expectedID3 = [
{
key: 'XXXX',
description: '',
data: BufferUtils.toArrayBuffer(new Uint8Array([83, 104, 97, 107])),
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(unknownID3)).toEqual(expectedID3);
});
it('parses ID3v2.3 frame sizes correctly', () => {
const value = new Uint8Array([
3,
83, 104, 97, 107, 97,
]);
const frame = Id3Generator.generateId3Frame('TIT2', value);
// Force version 2.3
frame[3] = 3;
const id3 = Id3Generator.generateId3(frame);
id3[3] = 3;
const frames = Id3Utils.getID3Frames(id3);
expect(frames[0].data).toBe('Shaka');
});
it('parses UTF16 TXXX frames', () => {
const payload = new Uint8Array([
1,
0xff, 0xfe,
...utf16le('A'),
0, 0,
0xff, 0xfe,
...utf16le('Shaka'),
]);
const frame = Id3Generator.generateId3Frame('TXXX', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames).toEqual([{
key: 'TXXX',
description: 'A',
data: 'Shaka',
mimeType: null,
pictureType: null,
}]);
});
it('parses UTF16BE text frames', () => {
const payload = new Uint8Array([
2,
0x00, 0x53,
0x00, 0x68,
0x00, 0x61,
0x00, 0x6b,
0x00, 0x61,
]);
const frame = Id3Generator.generateId3Frame('TIT2', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames[0].data).toBe('Shaka');
});
it('parses multi-value v2.4 text frames', () => {
const payload = new Uint8Array([
3,
65,
0,
66,
0,
67,
]);
const frame = Id3Generator.generateId3Frame('TPE1', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames[0].data).toBe('A / B / C');
});
it('parses COMM frames', () => {
const payload = new Uint8Array([
3,
101, 110, 103,
100, 101, 115, 99,
0,
104, 101, 108, 108, 111,
]);
const frame = Id3Generator.generateId3Frame('COMM', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames[0]).toEqual({
key: 'COMM',
description: 'desc',
data: 'hello',
mimeType: null,
pictureType: null,
});
});
it('falls back to language in COMM frame', () => {
const payload = new Uint8Array([
3,
101, 110, 103,
0,
104, 101, 108, 108, 111,
]);
const frame = Id3Generator.generateId3Frame('COMM', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames[0].description).toBe('eng');
});
it('parses UFID frames', () => {
const payload = new Uint8Array([
111, 119, 110, 101, 114,
0,
1, 2, 3,
]);
const frame = Id3Generator.generateId3Frame('UFID', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames[0]).toEqual({
key: 'UFID',
description: 'owner',
data: BufferUtils.toArrayBuffer(new Uint8Array([1, 2, 3])),
mimeType: null,
pictureType: null,
});
});
it('stops parsing at padding', () => {
const frame = Id3Generator.generateId3Frame(
'TIT2',
new Uint8Array([3, 65]),
);
const id3 = Id3Generator.generateId3(frame);
const padded = new Uint8Array(id3.length + 20);
padded.set(id3);
const frames = Id3Utils.getID3Frames(padded);
expect(frames.length).toBe(1);
});
it('handles footer correctly', () => {
const frame = Id3Generator.generateId3Frame(
'TIT2',
new Uint8Array([3, 65]),
);
const id3 = Id3Generator.generateId3(frame);
const footer = new Uint8Array([
0x33, 0x44, 0x49,
4, 0,
0,
0, 0, 0, 10,
]);
const combined = new Uint8Array(id3.length + footer.length);
combined.set(id3);
combined.set(footer, id3.length);
const frames = Id3Utils.getID3Frames(combined);
expect(frames.length).toBe(1);
});
it('extracts adjacent ID3 blocks', () => {
const frame = Id3Generator.generateId3Frame(
'TIT2',
new Uint8Array([3, 65]),
);
const tag1 = Id3Generator.generateId3(frame);
const tag2 = Id3Generator.generateId3(frame);
const combined = new Uint8Array(tag1.length + tag2.length);
combined.set(tag1);
combined.set(tag2, tag1.length);
const data = Id3Utils.getID3Data(combined);
expect(data.length).toBe(combined.length);
});
it('returns empty ID3Data when absent', () => {
const result = Id3Utils.getID3Data(new Uint8Array([1, 2, 3]));
expect(result.length).toBe(0);
});
it('parses ID3v1 tags', () => {
const data = new Uint8Array(128);
data[0] = 0x54;
data[1] = 0x41;
data[2] = 0x47;
const write = (offset, text) => {
for (let i = 0; i < text.length; i++) {
data[offset + i] = text.charCodeAt(i);
}
};
write(3, 'Title');
write(33, 'Artist');
write(63, 'Album');
write(93, '2024');
data[125] = 0;
data[126] = 7;
data[127] = 13;
const frames = Id3Utils.getID3v1Frames(data);
expect(frames).toEqual(jasmine.arrayContaining([
jasmine.objectContaining({
key: 'TIT2',
data: 'Title',
}),
jasmine.objectContaining({
key: 'TPE1',
data: 'Artist',
}),
jasmine.objectContaining({
key: 'TRCK',
data: '7',
}),
jasmine.objectContaining({
key: 'TCON',
data: '13',
}),
]));
});
it('returns empty array when no ID3v1 tag exists', () => {
const frames = Id3Utils.getID3v1Frames(new Uint8Array(128));
expect(frames).toEqual([]);
});
it('parses Apple transportStreamTimestamp PRIV frame', () => {
const owner =
'com.apple.streaming.transportStreamTimestamp';
const ownerBytes = Array.from(owner).map((c) => c.charCodeAt(0));
const payload = new Uint8Array([
...ownerBytes,
0,
0, 0, 0, 0,
0, 0, 0, 90,
]);
const frame = Id3Generator.generateId3Frame('PRIV', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(typeof frames[0].data).toBe('number');
});
it('returns null for malformed TXXX frame', () => {
const payload = new Uint8Array([
3,
65,
66,
67,
]);
const frame = Id3Generator.generateId3Frame('TXXX', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames).toEqual([]);
});
it('strips trailing nulls from W frames', () => {
const payload = new Uint8Array([
103, 111, 111, 103, 108, 101,
0, 0,
]);
const frame = Id3Generator.generateId3Frame('WCOP', payload);
const id3 = Id3Generator.generateId3(frame);
const frames = Id3Utils.getID3Frames(id3);
expect(frames[0].data).toBe('google');
});
});