From ebbd022a524677d3623ab37281c3d7476f66f64d Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Thu, 26 Feb 2015 16:49:05 +0100 Subject: [PATCH 1/6] Added new ID3 frames - Added support for ID3 frames of types GEOB and PRIV. - GEOB type is commonly used by dynamic ads provider to include in the stream information about the ads to be played. - PRIV type is commonly used for time synchronization (example: synchronizing playback of a live stream and its webvtt captions) and also by analytics companies to include tracking information in the stream. - Added a sample stream from Apple that contains ID3 metadata. --- .../android/exoplayer/demo/Samples.java | 3 ++ .../exoplayer/metadata/GeobMetadata.java | 38 +++++++++++++++++++ .../android/exoplayer/metadata/Id3Parser.java | 33 ++++++++++++++++ .../exoplayer/metadata/PrivMetadata.java | 34 +++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/GeobMetadata.java create mode 100644 library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index a22e825960..8b62775694 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -123,6 +123,9 @@ import java.util.Locale; new Sample("Apple AAC media playlist", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/" + "prog_index.m3u8", DemoUtil.TYPE_HLS), + new Sample("Apple ID3 metadata", + "http://devimages.apple.com/samplecode/adDemo/" + + "ad.m3u8", DemoUtil.TYPE_HLS), }; public static final Sample[] MISC = new Sample[] { diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/GeobMetadata.java b/library/src/main/java/com/google/android/exoplayer/metadata/GeobMetadata.java new file mode 100644 index 0000000000..1d4fcb4dee --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/GeobMetadata.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.metadata; + +/** + * A metadata that contains parsed ID3 GEOB (General Encapsulated Object) frame data associated + * with time indices. + */ +public class GeobMetadata { + + public static final String TYPE = "GEOB"; + + public final String mimeType; + public final String filename; + public final String description; + public final byte[] data; + + public GeobMetadata(String mimeType, String filename, String description, byte[] data) { + this.mimeType = mimeType; + this.filename = filename; + this.description = description; + this.data = data; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 1ec0e363dd..339390974d 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -67,6 +67,39 @@ public class Id3Parser implements MetadataParser> { String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, charset); metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + // Check frame ID == PRIV + byte[] frame = new byte[frameSize]; + id3Data.readBytes(frame, 0, frameSize); + + int firstZeroIndex = indexOf(frame, 0, (byte) 0); + String owner = new String(frame, 0, firstZeroIndex); + byte[] privateData = new byte[frameSize - firstZeroIndex - 1]; + System.arraycopy(frame, firstZeroIndex + 1, privateData, 0, frameSize - firstZeroIndex - 1); + metadata.put(PrivMetadata.TYPE, new PrivMetadata(owner, privateData)); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + // Check frame ID == GEOB + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + byte[] frame = new byte[frameSize - 1]; + id3Data.readBytes(frame, 0, frameSize - 1); + + int firstZeroIndex = indexOf(frame, 0, (byte) 0); + String mimeType = new String(frame, 0, firstZeroIndex); + int filenameStartIndex = firstZeroIndex + 1; + int filenameEndIndex = indexOf(frame, filenameStartIndex, (byte) 0); + String filename = new String(frame, filenameStartIndex, + filenameEndIndex - filenameStartIndex, charset); + int descriptionStartIndex = filenameEndIndex + 1; + int descriptionEndIndex = indexOf(frame, descriptionStartIndex, (byte) 0); + String description = new String(frame, descriptionStartIndex, + descriptionEndIndex - descriptionStartIndex, charset); + + byte[] objectData = new byte[frameSize - descriptionEndIndex - 2]; + System.arraycopy(frame, descriptionEndIndex + 1, objectData, 0, + frameSize - descriptionEndIndex - 2); + metadata.put(GeobMetadata.TYPE, new GeobMetadata(mimeType, filename, + description, objectData)); } else { String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); byte[] frame = new byte[frameSize]; diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java b/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java new file mode 100644 index 0000000000..ba3918a775 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.metadata; + +/** + * A metadata that contains parsed ID3 PRIV (Private) frame data associated + * with time indices. + */ +public class PrivMetadata { + + public static final String TYPE = "PRIV"; + + public final String owner; + public final byte[] privateData; + + public PrivMetadata(String owner, byte [] privateData) { + this.owner = owner; + this.privateData = privateData; + } + +} From c135bb7a57414ef77e5effee98a400cbb7eba738 Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Thu, 26 Feb 2015 21:54:29 +0100 Subject: [PATCH 2/6] Fixes for correctly supporting UTF-16 and UTF-16BE charsets For fields encoded using UTF-16 or UTF-16BE charsets when looking for termination character we have to look for two zero consecutive bytes. Otherwise, as many characters encoded with UTF-16 or UTF-16BE has one of their 2 bytes set with the value zero, we will be truncating text fields. --- .../android/exoplayer/metadata/Id3Parser.java | 46 ++++++++++++++----- .../exoplayer/metadata/PrivMetadata.java | 2 +- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 339390974d..111e735b7d 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -29,6 +29,11 @@ import java.util.Map; */ public class Id3Parser implements MetadataParser> { + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; + private static final int ID3_TEXT_ENCODING_UTF_16 = 1; + private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; + private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + @Override public boolean canParse(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_ID3); @@ -60,10 +65,10 @@ public class Id3Parser implements MetadataParser> { byte[] frame = new byte[frameSize - 1]; id3Data.readBytes(frame, 0, frameSize - 1); - int firstZeroIndex = indexOf(frame, 0, (byte) 0); + int firstZeroIndex = indexOfEOS(frame, 0, encoding); String description = new String(frame, 0, firstZeroIndex, charset); - int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0); - int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0); + int valueStartIndex = firstZeroIndex + 1; + int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding); String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, charset); metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); @@ -73,7 +78,7 @@ public class Id3Parser implements MetadataParser> { id3Data.readBytes(frame, 0, frameSize); int firstZeroIndex = indexOf(frame, 0, (byte) 0); - String owner = new String(frame, 0, firstZeroIndex); + String owner = new String(frame, 0, firstZeroIndex, "ISO-8859-1"); byte[] privateData = new byte[frameSize - firstZeroIndex - 1]; System.arraycopy(frame, firstZeroIndex + 1, privateData, 0, frameSize - firstZeroIndex - 1); metadata.put(PrivMetadata.TYPE, new PrivMetadata(owner, privateData)); @@ -85,13 +90,13 @@ public class Id3Parser implements MetadataParser> { id3Data.readBytes(frame, 0, frameSize - 1); int firstZeroIndex = indexOf(frame, 0, (byte) 0); - String mimeType = new String(frame, 0, firstZeroIndex); + String mimeType = new String(frame, 0, firstZeroIndex, "ISO-8859-1"); int filenameStartIndex = firstZeroIndex + 1; - int filenameEndIndex = indexOf(frame, filenameStartIndex, (byte) 0); + int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding); String filename = new String(frame, filenameStartIndex, filenameEndIndex - filenameStartIndex, charset); int descriptionStartIndex = filenameEndIndex + 1; - int descriptionEndIndex = indexOf(frame, descriptionStartIndex, (byte) 0); + int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding); String description = new String(frame, descriptionStartIndex, descriptionEndIndex - descriptionStartIndex, charset); @@ -131,6 +136,25 @@ public class Id3Parser implements MetadataParser> { return data.length; } + private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) { + int terminationPos = indexOf(data, fromIndex, (byte) 0); + + // For single byte encoding charsets, we are done + if(encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise, look for a two zero bytes + while(terminationPos < data.length - 1) { + if(data[terminationPos + 1] == (byte) 0) { + return terminationPos + 1; + } + terminationPos = indexOf(data, terminationPos + 1, (byte) 0); + } + + return data.length; + } + /** * Parses an ID3 header. * @@ -175,13 +199,13 @@ public class Id3Parser implements MetadataParser> { */ private static String getCharsetName(int encodingByte) { switch (encodingByte) { - case 0: + case ID3_TEXT_ENCODING_ISO_8859_1: return "ISO-8859-1"; - case 1: + case ID3_TEXT_ENCODING_UTF_16: return "UTF-16"; - case 2: + case ID3_TEXT_ENCODING_UTF_16BE: return "UTF-16BE"; - case 3: + case ID3_TEXT_ENCODING_UTF_8: return "UTF-8"; default: return "ISO-8859-1"; diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java b/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java index ba3918a775..8573b25906 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java @@ -26,7 +26,7 @@ public class PrivMetadata { public final String owner; public final byte[] privateData; - public PrivMetadata(String owner, byte [] privateData) { + public PrivMetadata(String owner, byte[] privateData) { this.owner = owner; this.privateData = privateData; } From f33cdd97e6d9ce92073ca46ac77d1481bce89b47 Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Fri, 27 Feb 2015 10:57:43 +0100 Subject: [PATCH 3/6] Fixed an issue when looking for termination string in UTF-16 and UTF-16BE Modified parse method to take into account different lengths for the termination character (1 for UTF-8 and ISO-8859-1; 2 for UTF-16 and UTF-16BE). --- .../android/exoplayer/metadata/Id3Parser.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 111e735b7d..0b05a59631 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -67,7 +67,7 @@ public class Id3Parser implements MetadataParser> { int firstZeroIndex = indexOfEOS(frame, 0, encoding); String description = new String(frame, 0, firstZeroIndex, charset); - int valueStartIndex = firstZeroIndex + 1; + int valueStartIndex = firstZeroIndex + delimiterLength(encoding); int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding); String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, charset); @@ -91,17 +91,17 @@ public class Id3Parser implements MetadataParser> { int firstZeroIndex = indexOf(frame, 0, (byte) 0); String mimeType = new String(frame, 0, firstZeroIndex, "ISO-8859-1"); - int filenameStartIndex = firstZeroIndex + 1; + int filenameStartIndex = firstZeroIndex + delimiterLength(encoding); int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding); String filename = new String(frame, filenameStartIndex, filenameEndIndex - filenameStartIndex, charset); - int descriptionStartIndex = filenameEndIndex + 1; + int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding); String description = new String(frame, descriptionStartIndex, descriptionEndIndex - descriptionStartIndex, charset); byte[] objectData = new byte[frameSize - descriptionEndIndex - 2]; - System.arraycopy(frame, descriptionEndIndex + 1, objectData, 0, + System.arraycopy(frame, descriptionEndIndex + delimiterLength(encoding), objectData, 0, frameSize - descriptionEndIndex - 2); metadata.put(GeobMetadata.TYPE, new GeobMetadata(mimeType, filename, description, objectData)); @@ -147,7 +147,7 @@ public class Id3Parser implements MetadataParser> { // Otherwise, look for a two zero bytes while(terminationPos < data.length - 1) { if(data[terminationPos + 1] == (byte) 0) { - return terminationPos + 1; + return terminationPos; } terminationPos = indexOf(data, terminationPos + 1, (byte) 0); } @@ -155,6 +155,11 @@ public class Id3Parser implements MetadataParser> { return data.length; } + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || + encodingByte == ID3_TEXT_ENCODING_UTF_8) ? 1 : 2; + } + /** * Parses an ID3 header. * From 1739af685a2b2cca76c253dc9c4733db9b3a1e43 Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Fri, 27 Feb 2015 11:02:37 +0100 Subject: [PATCH 4/6] Removed indexOfNot method Removed the unused method indexOfNot --- .../android/exoplayer/metadata/Id3Parser.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 0b05a59631..47fca79fe1 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -70,7 +70,7 @@ public class Id3Parser implements MetadataParser> { int valueStartIndex = firstZeroIndex + delimiterLength(encoding); int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding); String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, - charset); + charset); metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { // Check frame ID == PRIV @@ -98,7 +98,7 @@ public class Id3Parser implements MetadataParser> { int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding); String description = new String(frame, descriptionStartIndex, - descriptionEndIndex - descriptionStartIndex, charset); + descriptionEndIndex - descriptionStartIndex, charset); byte[] objectData = new byte[frameSize - descriptionEndIndex - 2]; System.arraycopy(frame, descriptionEndIndex + delimiterLength(encoding), objectData, 0, @@ -127,15 +127,6 @@ public class Id3Parser implements MetadataParser> { return data.length; } - private static int indexOfNot(byte[] data, int fromIndex, byte key) { - for (int i = fromIndex; i < data.length; i++) { - if (data[i] != key) { - return i; - } - } - return data.length; - } - private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) { int terminationPos = indexOf(data, fromIndex, (byte) 0); From 88475e4feedc68107779e44f135746086044538e Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Fri, 27 Feb 2015 22:09:05 +0100 Subject: [PATCH 5/6] Fixed issue in the calculation of size of objectData - Fixed issue in the calculation of size of objectData - Indentation fixes --- .../android/exoplayer/metadata/Id3Parser.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 47fca79fe1..6519c5a31b 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -70,7 +70,7 @@ public class Id3Parser implements MetadataParser> { int valueStartIndex = firstZeroIndex + delimiterLength(encoding); int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding); String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, - charset); + charset); metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { // Check frame ID == PRIV @@ -94,17 +94,19 @@ public class Id3Parser implements MetadataParser> { int filenameStartIndex = firstZeroIndex + delimiterLength(encoding); int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding); String filename = new String(frame, filenameStartIndex, - filenameEndIndex - filenameStartIndex, charset); + filenameEndIndex - filenameStartIndex, charset); int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding); String description = new String(frame, descriptionStartIndex, - descriptionEndIndex - descriptionStartIndex, charset); + descriptionEndIndex - descriptionStartIndex, charset); - byte[] objectData = new byte[frameSize - descriptionEndIndex - 2]; + int objectDataSize = frameSize - 1 /* encoding byte */ - descriptionEndIndex - + delimiterLength(encoding); + byte[] objectData = new byte[objectDataSize]; System.arraycopy(frame, descriptionEndIndex + delimiterLength(encoding), objectData, 0, - frameSize - descriptionEndIndex - 2); + objectDataSize); metadata.put(GeobMetadata.TYPE, new GeobMetadata(mimeType, filename, - description, objectData)); + description, objectData)); } else { String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); byte[] frame = new byte[frameSize]; From b03c8a713b4d94a135b2c487223e043afd40724d Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Sat, 28 Feb 2015 00:47:42 +0100 Subject: [PATCH 6/6] Mime type string always finish with a single 0 byte Mime type string always finish with a single 0 byte --- .../java/com/google/android/exoplayer/metadata/Id3Parser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 6519c5a31b..9fe098ae10 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -91,7 +91,7 @@ public class Id3Parser implements MetadataParser> { int firstZeroIndex = indexOf(frame, 0, (byte) 0); String mimeType = new String(frame, 0, firstZeroIndex, "ISO-8859-1"); - int filenameStartIndex = firstZeroIndex + delimiterLength(encoding); + int filenameStartIndex = firstZeroIndex + 1; int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding); String filename = new String(frame, filenameStartIndex, filenameEndIndex - filenameStartIndex, charset);