Skip to content

Commit

Permalink
feat: Parse ID3 metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
Alvaro Velad committed Aug 18, 2022
1 parent 122f223 commit 6bf4b82
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 0 deletions.
1 change: 1 addition & 0 deletions build/types/core
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
+../../lib/util/functional.js
+../../lib/util/i_destroyable.js
+../../lib/util/i_releasable.js
+../../lib/util/id3_utils.js
+../../lib/util/iterables.js
+../../lib/util/language_utils.js
+../../lib/util/lazy.js
Expand Down
36 changes: 36 additions & 0 deletions externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,42 @@ shaka.extern.SupportType;
shaka.extern.ID3Metadata;


/**
* @typedef {{
* type: string,
* size: number,
* data: Uint8Array
* }}
*
* @description metadata raw frame.
* @property {string} type
* @property {number} size
* @property {Uint8Array} data
* @exportDoc
*/
shaka.extern.MetadataRawFrame;


/**
* @typedef {{
* data: (ArrayBuffer|string),
* description: string,
* id: string,
* key: string,
* value: (ArrayBuffer|string)
* }}
*
* @description metadata frame parsed.
* @property {ArrayBuffer|string} data
* @property {string} description
* @property {string} id
* @property {string} key
* @property {ArrayBuffer|string} value
* @exportDoc
*/
shaka.extern.MetadataFrame;


/**
* @typedef {{
* schemeIdUri: string,
Expand Down
9 changes: 9 additions & 0 deletions lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ goog.require('shaka.media.SegmentReference');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.media.Transmuxer');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.Destroyer');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.Id3Utils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.Platform');
Expand Down Expand Up @@ -525,6 +527,8 @@ shaka.media.MediaSourceEngine = class {
* @return {!Promise}
*/
async appendBuffer(contentType, data, reference, hasClosedCaptions, seeked) {
const BufferUtils = shaka.util.BufferUtils;
const Id3Utils = shaka.util.Id3Utils;
const ContentType = shaka.util.ManifestParserUtils.ContentType;

if (contentType == ContentType.TEXT) {
Expand All @@ -539,6 +543,11 @@ shaka.media.MediaSourceEngine = class {
reference ? reference.endTime : null);
return;
}
const id3Frames = Id3Utils.getID3Frames(BufferUtils.toUint8(data));
if (id3Frames.length) {
// TODO...
shaka.log.warning('id3Frames', id3Frames);
}

if (this.transmuxers_[contentType]) {
const transmuxedData =
Expand Down
298 changes: 298 additions & 0 deletions lib/util/id3_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
/*! @license
* Shaka Player
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

goog.provide('shaka.util.Id3Utils');

goog.require('shaka.log');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.StringUtils');


/**
* @summary A set of Id3Utils utility functions.
* @export
*/
shaka.util.Id3Utils = class {
/**
* @param {Uint8Array} data
* @param {number} offset
* @return {boolean}
* @private
*/
static isHeader_(data, offset) {
/*
* http://id3.org/id3v2.3.0
* [0] = 'I'
* [1] = 'D'
* [2] = '3'
* [3,4] = {Version}
* [5] = {Flags}
* [6-9] = {ID3 Size}
*
* An ID3v2 tag can be detected with the following pattern:
* $49 44 33 yy yy xx zz zz zz zz
* Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80
*/
if (offset + 10 <= data.length) {
// look for 'ID3' identifier
if (data[offset] === 0x49 &&
data[offset + 1] === 0x44 &&
data[offset + 2] === 0x33) {
// check version is within range
if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) {
// check size is within range
if (data[offset + 6] < 0x80 &&
data[offset + 7] < 0x80 &&
data[offset + 8] < 0x80 &&
data[offset + 9] < 0x80) {
return true;
}
}
}
}

return false;
}

/**
* @param {Uint8Array} data
* @param {number} offset
* @return {boolean}
* @private
*/
static isFooter_(data, offset) {
/*
* The footer is a copy of the header, but with a different identifier
*/
if (offset + 10 <= data.length) {
// look for '3DI' identifier
if (data[offset] === 0x33 &&
data[offset + 1] === 0x44 &&
data[offset + 2] === 0x49) {
// check version is within range
if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) {
// check size is within range
if (data[offset + 6] < 0x80 &&
data[offset + 7] < 0x80 &&
data[offset + 8] < 0x80 &&
data[offset + 9] < 0x80) {
return true;
}
}
}
}

return false;
}

/**
* @param {Uint8Array} data
* @param {number} offset
* @return {number}
* @private
*/
static readSize_(data, offset) {
let size = 0;
size = (data[offset] & 0x7f) << 21;
size |= (data[offset + 1] & 0x7f) << 14;
size |= (data[offset + 2] & 0x7f) << 7;
size |= data[offset + 3] & 0x7f;
return size;
}

/**
* @param {Uint8Array} data
* @return {shaka.extern.MetadataRawFrame}
* @private
*/
static getFrameData_(data) {
/*
* Frame ID $xx xx xx xx (four characters)
* Size $xx xx xx xx
* Flags $xx xx
*/
const type = String.fromCharCode(data[0], data[1], data[2], data[3]);
const size = shaka.util.Id3Utils.readSize_(data, 4);

// skip frame id, size, and flags
const offset = 10;

return {
type,
size,
data: data.subarray(offset, offset + size),
};
}

/**
* @param {shaka.extern.MetadataRawFrame} frame
* @return {?shaka.extern.MetadataFrame}
* @private
*/
static decodeFrame_(frame) {
const BufferUtils = shaka.util.BufferUtils;
const StringUtils = shaka.util.StringUtils;

const metadataFrame = {
id: frame.type,
key: frame.type,
description: '',
data: '',
value: '',
};

if (frame.type === 'TXXX') {
/*
* Format:
* [0] = {Text Encoding}
* [1-?] = {Description}\0{Value}
*/
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 descriptionEndIndex = frame.data.subarray(1).indexOf(0);

if (descriptionEndIndex === -1) {
return null;
}
const description = StringUtils.fromUTF8(
BufferUtils.toUint8(frame.data, 1, descriptionEndIndex));
const data = StringUtils.fromUTF8(
BufferUtils.toUint8(frame.data, 2 + descriptionEndIndex));

metadataFrame.description = description;
metadataFrame.data = data;
metadataFrame.value = metadataFrame.data;
return metadataFrame;
} else if (frame.type === 'WXXX') {
/*
* Format:
* [0] = {Text Encoding}
* [1-?] = {Description}\0{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 descriptionEndIndex = frame.data.subarray(1).indexOf(0);

if (descriptionEndIndex === -1) {
return null;
}
const description = StringUtils.fromUTF8(
BufferUtils.toUint8(frame.data, 1, descriptionEndIndex));
const data = StringUtils.fromUTF8(
BufferUtils.toUint8(frame.data, description.length + 1));

metadataFrame.description = description;
metadataFrame.data = data;
metadataFrame.value = metadataFrame.data;
return metadataFrame;
} else if (frame.type === 'PRIV') {
/*
* Format: <text string>\0<binary data>
*/
if (frame.size < 2) {
return null;
}
const ownerEndIndex = frame.data.indexOf(0);
if (ownerEndIndex === -1) {
return null;
}
const owner = StringUtils.fromUTF8(
BufferUtils.toUint8(frame.data, 0, ownerEndIndex));
const data = BufferUtils.toArrayBuffer(
frame.data.subarray(owner.length + 1));
metadataFrame.description = owner;
metadataFrame.data = data;
metadataFrame.value = metadataFrame.data;
return metadataFrame;
} else if (frame.type[0] === 'T') {
/*
* Format:
* [0] = {Text Encoding}
* [1-?] = {Value}
*/
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 text = StringUtils.fromUTF8(frame.data.subarray(1));
metadataFrame.data = text;
metadataFrame.value = metadataFrame.data;
return metadataFrame;
} else if (frame.type[0] === 'W') {
/*
* Format:
* [0-?] = {URL}
*/
const url = StringUtils.fromUTF8(frame.data);
metadataFrame.data = url;
metadataFrame.value = metadataFrame.data;
return metadataFrame;
}

shaka.log.warning('Unrecognized/unsupported ID3 frame type:', frame.type);

return null;
}

/**
* Returns an array of ID3 frames found in all the ID3 tags in the id3Data
* @param {Uint8Array} id3Data - The ID3 data containing one or more ID3 tags
* @return {!Array.<shaka.extern.MetadataFrame>}
*/
static getID3Frames(id3Data) {
const Id3Utils = shaka.util.Id3Utils;
let offset = 0;
const frames = [];

while (offset < (id3Data.length - 1000)) {
while (Id3Utils.isHeader_(id3Data, offset)) {
const size = Id3Utils.readSize_(id3Data, offset + 6);
// skip past ID3 header
offset += 10;
const end = offset + size;
// loop through frames in the ID3 tag
while (offset + 8 < end) {
const frameData = Id3Utils.getFrameData_(id3Data.subarray(offset));
const frame = Id3Utils.decodeFrame_(frameData);
if (frame) {
frames.push(frame);
}

// skip frame header and frame data
offset += frameData.size + 10;
}

if (Id3Utils.isFooter_(id3Data, offset)) {
offset += 10;
}
}
offset++;
}
return frames;
}
};

/**
* UTF8 encoding byte
* @const {number}
*/
shaka.util.Id3Utils.UTF8_encoding = 0x03;

0 comments on commit 6bf4b82

Please sign in to comment.