Skip to content

Commit

Permalink
feat(ID3): decode APIC frames (#5857)
Browse files Browse the repository at this point in the history
Since the official ID3 site (https://id3.org) is not available, I used a
mirror
(https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-frames.html#apic)
to implement the decoding of APIC frames.

---------

Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
  • Loading branch information
pszemus and avelad authored Nov 6, 2023
1 parent 2eedb12 commit 6a862d2
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 2 deletions.
6 changes: 5 additions & 1 deletion externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,13 +498,17 @@ shaka.extern.MetadataRawFrame;
* @typedef {{
* key: string,
* data: (ArrayBuffer|string|number),
* description: string
* description: string,
* mimeType: ?string,
* pictureType: ?number
* }}
*
* @description metadata frame parsed.
* @property {string} key
* @property {ArrayBuffer|string|number} data
* @property {string} description
* @property {?string} mimeType
* @property {?number} pictureType
* @exportDoc
*/
shaka.extern.MetadataFrame;
Expand Down
51 changes: 50 additions & 1 deletion lib/util/id3_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,58 @@ shaka.util.Id3Utils = class {
key: frame.type,
description: '',
data: '',
mimeType: null,
pictureType: null,
};

if (frame.type === 'TXXX') {
if (frame.type === 'APIC') {
/*
* Format:
* [0] = {Text Encoding}
* [1 - X] = {MIME Type}\0
* [X+1] = {Picture Type}
* [X+2 - Y] = {Description}\0
* [Y - ?] = {Picture Data or Picture URL}
*/
if (frame.size < 2) {
return null;
}
if (frame.data[0] !== shaka.util.Id3Utils.UTF8_encoding) {
shaka.log.warning('Ignore frame with unrecognized character ' +
'encoding');
return null;
}

const mimeTypeEndIndex = frame.data.subarray(1).indexOf(0);
if (mimeTypeEndIndex === -1) {
return null;
}
const mimeType = StringUtils.fromUTF8(
BufferUtils.toUint8(frame.data, 1, mimeTypeEndIndex));
const pictureType = frame.data[2 + mimeTypeEndIndex];
const descriptionEndIndex = frame.data.subarray(3 + mimeTypeEndIndex)
.indexOf(0);
if (descriptionEndIndex === -1) {
return null;
}
const description = StringUtils.fromUTF8(
BufferUtils.toUint8(frame.data, 3 + mimeTypeEndIndex,
descriptionEndIndex));
let data;
if (mimeType === '-->') {
data = StringUtils.fromUTF8(
BufferUtils.toUint8(
frame.data, 4 + mimeTypeEndIndex + descriptionEndIndex));
} else {
data = BufferUtils.toArrayBuffer(
frame.data.subarray(4 + mimeTypeEndIndex + descriptionEndIndex));
}
metadataFrame.mimeType = mimeType;
metadataFrame.pictureType = pictureType;
metadataFrame.description = description;
metadataFrame.data = data;
return metadataFrame;
} else if (frame.type === 'TXXX') {
/*
* Format:
* [0] = {Text Encoding}
Expand Down
52 changes: 52 additions & 0 deletions test/util/id3_utils_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,44 @@ describe('Id3Utils', () => {
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);
Expand All @@ -22,6 +60,8 @@ describe('Id3Utils', () => {
key: 'TXXX',
description: 'A',
data: 'Shaka',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(txxxID3)).toEqual(expectedID3);
Expand All @@ -36,6 +76,8 @@ describe('Id3Utils', () => {
key: 'TXXX',
description: 'A',
data: 'Shaka',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(txxxID3)).toEqual(expectedID3);
Expand All @@ -51,6 +93,8 @@ describe('Id3Utils', () => {
key: 'TCOP',
description: '',
data: 'Shaka 2016',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(tcopID3)).toEqual(expectedID3);
Expand All @@ -66,6 +110,8 @@ describe('Id3Utils', () => {
key: 'WXXX',
description: 'A',
data: 'google.com',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(wxxxID3)).toEqual(expectedID3);
Expand All @@ -81,6 +127,8 @@ describe('Id3Utils', () => {
key: 'WCOP',
description: '',
data: 'google.com',
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(wcopID3)).toEqual(expectedID3);
Expand All @@ -95,6 +143,8 @@ describe('Id3Utils', () => {
key: 'PRIV',
description: 'A',
data: BufferUtils.toArrayBuffer(new Uint8Array([83, 104, 97, 107])),
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(privID3)).toEqual(expectedID3);
Expand All @@ -109,6 +159,8 @@ describe('Id3Utils', () => {
key: 'XXXX',
description: '',
data: BufferUtils.toArrayBuffer(new Uint8Array([83, 104, 97, 107])),
mimeType: null,
pictureType: null,
},
];
expect(Id3Utils.getID3Frames(unknownID3)).toEqual(expectedID3);
Expand Down

0 comments on commit 6a862d2

Please sign in to comment.