diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java index 2616057b05..2632aaf0d0 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java @@ -811,7 +811,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou if (entry instanceof TextInformationFrame) { TextInformationFrame textFrame = (TextInformationFrame) entry; if ("TXXX".equals(textFrame.id)) { - streamPlayer.triggerUserTextReceived(textFrame.value); + streamPlayer.triggerUserTextReceived(textFrame.values[0]); } } else if (entry instanceof EventMessage) { EventMessage eventMessage = (EventMessage) entry; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java index d842f0dd48..ae658ed9bc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java @@ -107,7 +107,7 @@ public class MetadataRendererTest { assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); TextInformationFrame expectedId3Frame = - new TextInformationFrame("TXXX", "Test description", "Test value"); + new TextInformationFrame("TXXX", "Test description", new String[] { "Test value" }); assertThat(metadata.get(0).get(0)).isEqualTo(expectedId3Frame); } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 245e99c2e4..f0621ab1c2 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -592,7 +592,7 @@ public final class Mp3Extractor implements Extractor { Metadata.Entry entry = metadata.get(i); if (entry instanceof TextInformationFrame && ((TextInformationFrame) entry).id.equals("TLEN")) { - return Util.msToUs(Long.parseLong(((TextInformationFrame) entry).value)); + return Util.msToUs(Long.parseLong(((TextInformationFrame) entry).values[0])); } } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 46bd482178..17d53a5c85 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -455,14 +455,29 @@ public final class Id3Decoder extends SimpleMetadataDecoder { byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int descriptionEndIndex = indexOfEos(data, 0, encoding); + int descriptionEndIndex = indexOfTerminator(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); - int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); - String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); + // In ID3v2.4, text information frames can contain multiple values delimited by a null + // terminator. Thus, we after each "end of stream" marker we actually need to keep looking + // for more data, at least until the index is equal to the data length. + ArrayList values = new ArrayList<>(); - return new TextInformationFrame("TXXX", description, value); + int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); + if (valueStartIndex >= data.length) { + return new TextInformationFrame("TXXX", description, new String[0]); + } + + int valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); + while (valueStartIndex < valueEndIndex) { + String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); + values.add(value); + + valueStartIndex = valueEndIndex + delimiterLength(encoding); + valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); + } + + return new TextInformationFrame("TXXX", description, values.toArray(new String[0])); } @Nullable @@ -479,10 +494,26 @@ public final class Id3Decoder extends SimpleMetadataDecoder { byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int valueEndIndex = indexOfEos(data, 0, encoding); - String value = new String(data, 0, valueEndIndex, charset); + // In ID3v2.4, text information frames can contain multiple values delimited by a null + // terminator. Thus, we after each "end of stream" marker we actually need to keep looking + // for more data, at least until the index is equal to the data length. + ArrayList values = new ArrayList<>(); - return new TextInformationFrame(id, null, value); + int valueStartIndex = 0; + if (valueStartIndex >= data.length) { + return new TextInformationFrame(id, null, new String[0]); + } + + int valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); + while (valueStartIndex < valueEndIndex) { + String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); + values.add(value); + + valueStartIndex = valueEndIndex + delimiterLength(encoding); + valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); + } + + return new TextInformationFrame(id, null, values.toArray(new String[0])); } @Nullable @@ -499,7 +530,7 @@ public final class Id3Decoder extends SimpleMetadataDecoder { byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int descriptionEndIndex = indexOfEos(data, 0, encoding); + int descriptionEndIndex = indexOfTerminator(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); @@ -546,11 +577,11 @@ public final class Id3Decoder extends SimpleMetadataDecoder { String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); int filenameStartIndex = mimeTypeEndIndex + 1; - int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding); + int filenameEndIndex = indexOfTerminator(data, filenameStartIndex, encoding); String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset); int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); - int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + int descriptionEndIndex = indexOfTerminator(data, descriptionStartIndex, encoding); String description = decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset); @@ -588,7 +619,7 @@ public final class Id3Decoder extends SimpleMetadataDecoder { int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; int descriptionStartIndex = mimeTypeEndIndex + 2; - int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + int descriptionEndIndex = indexOfTerminator(data, descriptionStartIndex, encoding); String description = new String( data, descriptionStartIndex, descriptionEndIndex - descriptionStartIndex, charset); @@ -617,11 +648,11 @@ public final class Id3Decoder extends SimpleMetadataDecoder { data = new byte[frameSize - 4]; id3Data.readBytes(data, 0, frameSize - 4); - int descriptionEndIndex = indexOfEos(data, 0, encoding); + int descriptionEndIndex = indexOfTerminator(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); int textStartIndex = descriptionEndIndex + delimiterLength(encoding); - int textEndIndex = indexOfEos(data, textStartIndex, encoding); + int textEndIndex = indexOfTerminator(data, textStartIndex, encoding); String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset); return new CommentFrame(language, description, text); @@ -798,7 +829,7 @@ public final class Id3Decoder extends SimpleMetadataDecoder { : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); } - private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + private static int indexOfTerminator(byte[] data, int fromIndex, int encoding) { int terminationPos = indexOfZeroByte(data, fromIndex); // For single byte encoding charsets, we're done. diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 7bab697331..fc1f26d7d9 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -19,52 +19,77 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** Text information ID3 frame. */ public final class TextInformationFrame extends Id3Frame { + private final static String MULTI_VALUE_DELIMITER = ", "; @Nullable public final String description; + + /** @deprecated Use {@code values} instead. */ + @Deprecated public final String value; - public TextInformationFrame(String id, @Nullable String description, String value) { + @NonNull + public final String[] values; + + public TextInformationFrame(String id, @Nullable String description, @NonNull String[] values) { super(id); this.description = description; - this.value = value; + this.values = values; + + if (values.length > 0) { + this.value = values[0]; + } else { + this.value = null; + } + } + + /** @deprecated Use {@code TextInformationFrame(String id, String description, String[] values} instead */ + @Deprecated + public TextInformationFrame(String id, @Nullable String description, String value) { + this(id, description, new String[] {value } ); } /* package */ TextInformationFrame(Parcel in) { super(castNonNull(in.readString())); description = in.readString(); - value = castNonNull(in.readString()); + values = in.createStringArray(); + this.value = values[0]; } @Override public void populateMediaMetadata(MediaMetadata.Builder builder) { + // Depending on the context this frame is in, we either take the first value of a multi-value + // frame because multiple values make no sense, or we join the values together with a comma + // when multiple values do make sense. switch (id) { case "TT2": case "TIT2": - builder.setTitle(value); + builder.setTitle(values[0]); break; case "TP1": case "TPE1": - builder.setArtist(value); + builder.setArtist(String.join(MULTI_VALUE_DELIMITER, values)); break; case "TP2": case "TPE2": - builder.setAlbumArtist(value); + builder.setAlbumArtist(String.join(MULTI_VALUE_DELIMITER, values)); break; case "TAL": case "TALB": - builder.setAlbumTitle(value); + builder.setAlbumTitle(values[0]); break; case "TRK": case "TRCK": - String[] trackNumbers = Util.split(value, "/"); + String[] trackNumbers = Util.split(values[0], "/"); try { int trackNumber = Integer.parseInt(trackNumbers[0]); @Nullable @@ -78,7 +103,7 @@ public final class TextInformationFrame extends Id3Frame { case "TYE": case "TYER": try { - builder.setRecordingYear(Integer.parseInt(value)); + builder.setRecordingYear(Integer.parseInt(values[0])); } catch (NumberFormatException e) { // Do nothing, invalid input. } @@ -86,15 +111,16 @@ public final class TextInformationFrame extends Id3Frame { case "TDA": case "TDAT": try { - int month = Integer.parseInt(value.substring(2, 4)); - int day = Integer.parseInt(value.substring(0, 2)); + String date = values[0]; + int month = Integer.parseInt(date.substring(2, 4)); + int day = Integer.parseInt(date.substring(0, 2)); builder.setRecordingMonth(month).setRecordingDay(day); } catch (NumberFormatException | StringIndexOutOfBoundsException e) { // Do nothing, invalid input. } break; case "TDRC": - List recordingDate = parseId3v2point4TimestampFrameForDate(value); + List recordingDate = parseId3v2point4TimestampFrameForDate(values[0]); switch (recordingDate.size()) { case 3: builder.setRecordingDay(recordingDate.get(2)); @@ -112,7 +138,7 @@ public final class TextInformationFrame extends Id3Frame { } break; case "TDRL": - List releaseDate = parseId3v2point4TimestampFrameForDate(value); + List releaseDate = parseId3v2point4TimestampFrameForDate(values[0]); switch (releaseDate.size()) { case 3: builder.setReleaseDay(releaseDate.get(2)); @@ -131,15 +157,15 @@ public final class TextInformationFrame extends Id3Frame { break; case "TCM": case "TCOM": - builder.setComposer(value); + builder.setComposer(String.join(MULTI_VALUE_DELIMITER, values)); break; case "TP3": case "TPE3": - builder.setConductor(value); + builder.setConductor(String.join(MULTI_VALUE_DELIMITER, values)); break; case "TXT": case "TEXT": - builder.setWriter(value); + builder.setWriter(String.join(MULTI_VALUE_DELIMITER, values)); break; default: break; @@ -157,7 +183,7 @@ public final class TextInformationFrame extends Id3Frame { TextInformationFrame other = (TextInformationFrame) obj; return Util.areEqual(id, other.id) && Util.areEqual(description, other.description) - && Util.areEqual(value, other.value); + && Arrays.equals(values, other.values); } @Override @@ -165,13 +191,13 @@ public final class TextInformationFrame extends Id3Frame { int result = 17; result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + Arrays.hashCode(values); return result; } @Override public String toString() { - return id + ": description=" + description + ": value=" + value; + return id + ": description=" + description + ": value=" + String.join(MULTI_VALUE_DELIMITER, values); } // Parcelable implementation. @@ -180,7 +206,7 @@ public final class TextInformationFrame extends Id3Frame { public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); dest.writeString(description); - dest.writeString(value); + dest.writeStringArray(values); } public static final Parcelable.Creator CREATOR = diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 833a290cea..ae53ac3c43 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -52,7 +52,7 @@ public final class Id3DecoderTest { TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEmpty(); - assertThat(textInformationFrame.value).isEqualTo("mdialog_VINDICO1527664_start"); + assertThat(textInformationFrame.values[0]).isEqualTo("mdialog_VINDICO1527664_start"); // Test UTF-16. rawId3 = @@ -67,7 +67,7 @@ public final class Id3DecoderTest { textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEqualTo("Hello World"); - assertThat(textInformationFrame.value).isEmpty(); + assertThat(textInformationFrame.values).isEmpty(); // Test empty. rawId3 = buildSingleFrameTag("TXXX", new byte[0]); @@ -81,7 +81,7 @@ public final class Id3DecoderTest { textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEmpty(); - assertThat(textInformationFrame.value).isEmpty(); + assertThat(textInformationFrame.values).isEmpty(); } @Test @@ -95,7 +95,8 @@ public final class Id3DecoderTest { TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.description).isNull(); - assertThat(textInformationFrame.value).isEqualTo("Hello World"); + assertThat(textInformationFrame.values.length).isEqualTo(1); + assertThat(textInformationFrame.values[0]).isEqualTo("Hello World"); // Test empty. rawId3 = buildSingleFrameTag("TIT2", new byte[0]); @@ -109,7 +110,7 @@ public final class Id3DecoderTest { textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.description).isNull(); - assertThat(textInformationFrame.value).isEmpty(); + assertThat(textInformationFrame.values).isEmpty(); } @Test