Improve multi-value text frame parsing

Rework the parsing of multi-value text frames to reduce duplication
and increase efficiency.
parent 157f1f07
...@@ -10395,7 +10395,7 @@ public final class ExoPlayerTest { ...@@ -10395,7 +10395,7 @@ public final class ExoPlayerTest {
new Metadata( new Metadata(
new BinaryFrame(/* id= */ "", /* data= */ new byte[0]), new BinaryFrame(/* id= */ "", /* data= */ new byte[0]),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TT2", /* description= */ null, /* value= */ "title"))) /* id= */ "TT2", /* description= */ null, /* value= */ Collections.singletonList("title"))))
.build(); .build();
// Set multiple values together. // Set multiple values together.
......
...@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; ...@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.metadata.mp4.MdtaMetadataEntry; import com.google.android.exoplayer2.metadata.mp4.MdtaMetadataEntry;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.Collections;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Utilities for handling metadata in MP4. */ /** Utilities for handling metadata in MP4. */
...@@ -452,7 +453,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -452,7 +453,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (atomType == Atom.TYPE_data) { if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4) data.skipBytes(8); // version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(atomSize - 16); String value = data.readNullTerminatedString(atomSize - 16);
return new TextInformationFrame(id, /* description= */ null, value); return new TextInformationFrame(id, /* description= */ null, Collections.singletonList(value));
} }
Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
return null; return null;
...@@ -484,7 +485,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -484,7 +485,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
if (value >= 0) { if (value >= 0) {
return isTextInformationFrame return isTextInformationFrame
? new TextInformationFrame(id, /* description= */ null, Integer.toString(value)) ? new TextInformationFrame(id, /* description= */ null, Collections.singletonList(Integer.toString(value)))
: new CommentFrame(C.LANGUAGE_UNDETERMINED, id, Integer.toString(value)); : new CommentFrame(C.LANGUAGE_UNDETERMINED, id, Integer.toString(value));
} }
Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
...@@ -505,7 +506,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -505,7 +506,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (count > 0) { if (count > 0) {
value += "/" + count; value += "/" + count;
} }
return new TextInformationFrame(attributeName, /* description= */ null, value); return new TextInformationFrame(attributeName, /* description= */ null, Collections.singletonList(value));
} }
} }
Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
...@@ -521,7 +522,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -521,7 +522,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
? STANDARD_GENRES[genreCode - 1] ? STANDARD_GENRES[genreCode - 1]
: null; : null;
if (genreString != null) { if (genreString != null) {
return new TextInformationFrame("TCON", /* description= */ null, genreString); return new TextInformationFrame("TCON", /* description= */ null, Collections.singletonList(genreString));
} }
Log.w(TAG, "Failed to parse standard genre code"); Log.w(TAG, "Failed to parse standard genre code");
return null; return null;
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
...@@ -25,10 +26,12 @@ import com.google.android.exoplayer2.util.ParsableBitArray; ...@@ -25,10 +26,12 @@ import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii; import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
...@@ -458,26 +461,9 @@ public final class Id3Decoder extends SimpleMetadataDecoder { ...@@ -458,26 +461,9 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
int descriptionEndIndex = indexOfTerminator(data, 0, encoding); int descriptionEndIndex = indexOfTerminator(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset); String description = new String(data, 0, descriptionEndIndex, charset);
// In ID3v2.4, text information frames can contain multiple values delimited by a null List<String> values = decodeTextInformationFrameValues(
// terminator. Thus, we after each "end of stream" marker we actually need to keep looking data, encoding, descriptionEndIndex + delimiterLength(encoding));
// for more data, at least until the index is equal to the data length. return new TextInformationFrame("TXXX", description, values);
ArrayList<String> values = new ArrayList<>();
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 @Nullable
...@@ -489,31 +475,38 @@ public final class Id3Decoder extends SimpleMetadataDecoder { ...@@ -489,31 +475,38 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
} }
int encoding = id3Data.readUnsignedByte(); int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] data = new byte[frameSize - 1]; byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1); id3Data.readBytes(data, 0, frameSize - 1);
// In ID3v2.4, text information frames can contain multiple values delimited by a null List<String> values = decodeTextInformationFrameValues(data, encoding, 0);
// terminator. Thus, we after each "end of stream" marker we actually need to keep looking return new TextInformationFrame(id, null, values);
// for more data, at least until the index is equal to the data length. }
@NonNull
private static List<String> decodeTextInformationFrameValues(
byte[] data, final int encoding, final int index) throws UnsupportedEncodingException {
ArrayList<String> values = new ArrayList<>(); ArrayList<String> values = new ArrayList<>();
int valueStartIndex = 0; if (index >= data.length) {
if (valueStartIndex >= data.length) { return Collections.emptyList();
return new TextInformationFrame(id, null, new String[0]);
} }
// 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.
String charset = getCharsetName(encoding);
int valueStartIndex = index;
int valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); int valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding);
while (valueStartIndex < valueEndIndex) { while (valueStartIndex < valueEndIndex) {
String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
values.add(value); values.add(value);
valueStartIndex = valueEndIndex + delimiterLength(encoding); valueStartIndex = valueEndIndex + delimiterLength(encoding);
valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding);
} }
return new TextInformationFrame(id, null, values.toArray(new String[0])); return values;
} }
@Nullable @Nullable
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.MediaMetadata; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** Text information ID3 frame. */ /** Text information ID3 frame. */
...@@ -38,11 +39,11 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -38,11 +39,11 @@ public final class TextInformationFrame extends Id3Frame {
@NonNull @NonNull
public final ImmutableList<String> values; public final ImmutableList<String> values;
public TextInformationFrame(String id, @Nullable String description, @NonNull String[] values) { public TextInformationFrame(String id, @Nullable String description, @NonNull List<String> values) {
super(id); super(id);
this.description = description; this.description = description;
if( values.length > 0 ) { if( values.size() > 0 ) {
this.values = ImmutableList.copyOf(values); this.values = ImmutableList.copyOf(values);
} else { } else {
this.values = ImmutableList.of(""); this.values = ImmutableList.of("");
...@@ -54,7 +55,7 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -54,7 +55,7 @@ public final class TextInformationFrame extends Id3Frame {
/** @deprecated Use {@code TextInformationFrame(String id, String description, String[] values} instead */ /** @deprecated Use {@code TextInformationFrame(String id, String description, String[] values} instead */
@Deprecated @Deprecated
public TextInformationFrame(String id, @Nullable String description, String value) { public TextInformationFrame(String id, @Nullable String description, String value) {
this(id, description, new String[] {value } ); this(id, description, Collections.singletonList(value) );
} }
/* package */ TextInformationFrame(Parcel in) { /* package */ TextInformationFrame(Parcel in) {
......
...@@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; ...@@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -32,7 +33,7 @@ public class TextInformationFrameTest { ...@@ -32,7 +33,7 @@ public class TextInformationFrameTest {
@Test @Test
public void parcelable() { public void parcelable() {
TextInformationFrame textInformationFrameToParcel = new TextInformationFrame("", "", ""); TextInformationFrame textInformationFrameToParcel = new TextInformationFrame("", "", Collections.singletonList(""));
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
textInformationFrameToParcel.writeToParcel(parcel, 0); textInformationFrameToParcel.writeToParcel(parcel, 0);
...@@ -62,28 +63,29 @@ public class TextInformationFrameTest { ...@@ -62,28 +63,29 @@ public class TextInformationFrameTest {
List<Metadata.Entry> entries = List<Metadata.Entry> entries =
ImmutableList.of( ImmutableList.of(
new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ new String[] { title }), new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */
new TextInformationFrame(/* id= */ "TP1", /* description= */ null, /* value= */ new String[] { artist }), Collections.singletonList(title)),
new TextInformationFrame(/* id= */ "TP1", /* description= */ null, /* values= */ Collections.singletonList( artist )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TAL", /* description= */ null, /* value= */ new String[] { albumTitle }), /* id= */ "TAL", /* description= */ null, /* values= */ Collections.singletonList( albumTitle )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TP2", /* description= */ null, /* value= */ new String[] { albumArtist }), /* id= */ "TP2", /* description= */ null, /* values= */ Collections.singletonList( albumArtist )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TRK", /* description= */ null, /* value= */ new String[] { trackNumberInfo}), /* id= */ "TRK", /* description= */ null, /* values= */ Collections.singletonList( trackNumberInfo)),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TYE", /* description= */ null, /* value= */ new String[] { recordingYear }), /* id= */ "TYE", /* description= */ null, /* values= */ Collections.singletonList( recordingYear )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TDA", /* id= */ "TDA",
/* description= */ null, /* description= */ null,
/* value= */ new String[] { recordingDay + recordingMonth }), /* value= */ Collections.singletonList( recordingDay + recordingMonth )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TDRL", /* description= */ null, /* value= */ new String[] { releaseDate }), /* id= */ "TDRL", /* description= */ null, /* values= */ Collections.singletonList( releaseDate )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TCM", /* description= */ null, /* value= */ new String[] { composer }), /* id= */ "TCM", /* description= */ null, /* values= */ Collections.singletonList( composer )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TP3", /* description= */ null, /* value= */ new String[] { conductor }), /* id= */ "TP3", /* description= */ null, /* values= */ Collections.singletonList( conductor )),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TXT", /* description= */ null, /* value= */ new String[] { writer })); /* id= */ "TXT", /* description= */ null, /* values= */ Collections.singletonList( writer )));
MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon(); MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon();
for (Metadata.Entry entry : entries) { for (Metadata.Entry entry : entries) {
...@@ -110,8 +112,8 @@ public class TextInformationFrameTest { ...@@ -110,8 +112,8 @@ public class TextInformationFrameTest {
// Test empty value array // Test empty value array
entries = entries =
ImmutableList.of( Collections.singletonList(
new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ new String[0]) new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* values= */ Collections.emptyList())
); );
builder = MediaMetadata.EMPTY.buildUpon(); builder = MediaMetadata.EMPTY.buildUpon();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment