Additional clean-up and formatting

This commit is contained in:
tonihei 2024-03-25 11:59:35 +00:00
parent 8673e6d39a
commit 73c0ebb214
8 changed files with 145 additions and 125 deletions

View File

@ -3,6 +3,7 @@
### Unreleased changes ### Unreleased changes
* Common Library: * Common Library:
* Add `Format.labels` to allow localized or other alternative labels.
* ExoPlayer: * ExoPlayer:
* Fix issue where `PreloadMediaPeriod` cannot retain the streams when it * Fix issue where `PreloadMediaPeriod` cannot retain the streams when it
is preloaded again. is preloaded again.
@ -100,6 +101,8 @@
* RTMP Extension: * RTMP Extension:
* HLS Extension: * HLS Extension:
* DASH Extension: * DASH Extension:
* Populate all `Label` elements from the manifest into `Format.labels`
([#1054](https://github.com/androidx/media/pull/1054)).
* Smooth Streaming Extension: * Smooth Streaming Extension:
* RTSP Extension: * RTSP Extension:
* Skip empty session information values (i-tags) in SDP parsing * Skip empty session information values (i-tags) in SDP parsing

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
@ -140,7 +141,7 @@ public final class Format implements Bundleable {
@Nullable private String id; @Nullable private String id;
@Nullable private String label; @Nullable private String label;
@Nullable private List<Label> labels; private List<Label> labels;
@Nullable private String language; @Nullable private String language;
private @C.SelectionFlags int selectionFlags; private @C.SelectionFlags int selectionFlags;
private @C.RoleFlags int roleFlags; private @C.RoleFlags int roleFlags;
@ -196,6 +197,7 @@ public final class Format implements Bundleable {
/** Creates a new instance with default values. */ /** Creates a new instance with default values. */
public Builder() { public Builder() {
labels = ImmutableList.of();
averageBitrate = NO_VALUE; averageBitrate = NO_VALUE;
peakBitrate = NO_VALUE; peakBitrate = NO_VALUE;
// Sample specific. // Sample specific.
@ -298,6 +300,9 @@ public final class Format implements Bundleable {
/** /**
* Sets {@link Format#label}. The default value is {@code null}. * Sets {@link Format#label}. The default value is {@code null}.
* *
* <p>If both this default label and a list of {@link #setLabels labels} are set, this default
* label must be part of label list.
*
* @param label The {@link Format#label}. * @param label The {@link Format#label}.
* @return The builder. * @return The builder.
*/ */
@ -308,14 +313,17 @@ public final class Format implements Bundleable {
} }
/** /**
* Sets {@link Format#labels}. The default value is {@code null}. * Sets {@link Format#labels}. The default value is an empty list.
*
* <p>If both the default {@linkplain #setLabel label} and this list are set, the default label
* must be part of this list of labels.
* *
* @param labels The {@link Format#labels}. * @param labels The {@link Format#labels}.
* @return The builder. * @return The builder.
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setLabels(@Nullable List<Label> labels) { public Builder setLabels(List<Label> labels) {
this.labels = labels; this.labels = ImmutableList.copyOf(labels);
return this; return this;
} }
@ -757,10 +765,20 @@ public final class Format implements Bundleable {
/** An identifier for the format, or null if unknown or not applicable. */ /** An identifier for the format, or null if unknown or not applicable. */
@Nullable public final String id; @Nullable public final String id;
/** The human readable label, or null if unknown or not applicable. */ /**
* The default human readable label, or null if unknown or not applicable.
*
* <p>If non-null, the same label will be part of {@link #labels} too. If null, {@link #labels}
* will be empty.
*/
@Nullable public final String label; @Nullable public final String label;
/** The human readable list of labels, or null if unknown or not applicable. */ /**
* The human readable list of labels, or an empty list if unknown or not applicable.
*
* <p>If non-empty, the default {@link #label} will be part of this list. If empty, the default
* {@link #label} will be null.
*/
@UnstableApi public final List<Label> labels; @UnstableApi public final List<Label> labels;
/** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */
@ -952,17 +970,19 @@ public final class Format implements Bundleable {
private Format(Builder builder) { private Format(Builder builder) {
id = builder.id; id = builder.id;
language = Util.normalizeLanguageCode(builder.language); language = Util.normalizeLanguageCode(builder.language);
@Nullable String tmpLabel = builder.label; if (builder.labels.isEmpty() && builder.label != null) {
labels = builder.labels == null ? new ArrayList<>() : builder.labels; labels = ImmutableList.of(new Label(language, builder.label));
if (labels.isEmpty() && !TextUtils.isEmpty(tmpLabel)) { label = builder.label;
labels.add(new Label(language, tmpLabel)); } else if (!builder.labels.isEmpty() && builder.label == null) {
label = tmpLabel; labels = builder.labels;
} else if (!labels.isEmpty() && TextUtils.isEmpty(tmpLabel)) { label = getDefaultLabel(builder.labels, language);
label = getDefaultLabel(tmpLabel, labels);
} else { } else {
label = tmpLabel; checkState(
(builder.labels.isEmpty() && builder.label == null)
|| (builder.labels.stream().anyMatch(l -> l.value.equals(builder.label))));
labels = builder.labels;
label = builder.label;
} }
checkLabels(label, labels);
selectionFlags = builder.selectionFlags; selectionFlags = builder.selectionFlags;
roleFlags = builder.roleFlags; roleFlags = builder.roleFlags;
averageBitrate = builder.averageBitrate; averageBitrate = builder.averageBitrate;
@ -1010,29 +1030,6 @@ public final class Format implements Bundleable {
} }
} }
private @Nullable String getDefaultLabel(@Nullable String label, List<Label> labels) {
if (TextUtils.isEmpty(label)) {
for (Label l : labels) {
if (TextUtils.equals(l.lang, language)) {
return l.value;
}
}
if (!labels.isEmpty()) {
return labels.get(0).value;
}
}
return label;
}
private void checkLabels(@Nullable String label, List<Label> labels)
throws IllegalStateException {
if (!TextUtils.isEmpty(label)
&& !labels.isEmpty()
&& labels.stream().noneMatch(l -> TextUtils.equals(l.value, label))) {
throw new IllegalStateException();
}
}
/** Returns a {@link Format.Builder} initialized with the values of this instance. */ /** Returns a {@link Format.Builder} initialized with the values of this instance. */
@UnstableApi @UnstableApi
public Builder buildUpon() { public Builder buildUpon() {
@ -1056,7 +1053,7 @@ public final class Format implements Bundleable {
// Prefer manifest values, but fill in from sample format if missing. // Prefer manifest values, but fill in from sample format if missing.
@Nullable String label = manifestFormat.label != null ? manifestFormat.label : this.label; @Nullable String label = manifestFormat.label != null ? manifestFormat.label : this.label;
List<Label> labels = manifestFormat.labels; List<Label> labels = !manifestFormat.labels.isEmpty() ? manifestFormat.labels : this.labels;
@Nullable String language = this.language; @Nullable String language = this.language;
if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO) if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO)
&& manifestFormat.language != null) { && manifestFormat.language != null) {
@ -1246,6 +1243,7 @@ public final class Format implements Bundleable {
&& Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0
&& Util.areEqual(id, other.id) && Util.areEqual(id, other.id)
&& Util.areEqual(label, other.label) && Util.areEqual(label, other.label)
&& labels.equals(other.labels)
&& Util.areEqual(codecs, other.codecs) && Util.areEqual(codecs, other.codecs)
&& Util.areEqual(containerMimeType, other.containerMimeType) && Util.areEqual(containerMimeType, other.containerMimeType)
&& Util.areEqual(sampleMimeType, other.sampleMimeType) && Util.areEqual(sampleMimeType, other.sampleMimeType)
@ -1254,8 +1252,7 @@ public final class Format implements Bundleable {
&& Util.areEqual(metadata, other.metadata) && Util.areEqual(metadata, other.metadata)
&& Util.areEqual(colorInfo, other.colorInfo) && Util.areEqual(colorInfo, other.colorInfo)
&& Util.areEqual(drmInitData, other.drmInitData) && Util.areEqual(drmInitData, other.drmInitData)
&& initializationDataEquals(other) && initializationDataEquals(other);
&& labels.equals(other.labels);
} }
/** /**
@ -1338,7 +1335,7 @@ public final class Format implements Bundleable {
if (format.language != null) { if (format.language != null) {
builder.append(", language=").append(format.language); builder.append(", language=").append(format.language);
} }
if (format.labels.size() > 0) { if (!format.labels.isEmpty()) {
builder.append(", labels=["); builder.append(", labels=[");
Joiner.on(',').appendTo(builder, format.labels); Joiner.on(',').appendTo(builder, format.labels);
builder.append("]"); builder.append("]");
@ -1561,4 +1558,13 @@ public final class Format implements Bundleable {
private static <T> T defaultIfNull(@Nullable T value, @Nullable T defaultValue) { private static <T> T defaultIfNull(@Nullable T value, @Nullable T defaultValue) {
return value != null ? value : defaultValue; return value != null ? value : defaultValue;
} }
private static String getDefaultLabel(List<Label> labels, @Nullable String language) {
for (Label l : labels) {
if (TextUtils.equals(l.language, language)) {
return l.value;
}
}
return labels.get(0).value;
}
} }

View File

@ -1,94 +1,86 @@
/*
* Copyright 2024 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 androidx.media3.common; package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
/** A Label, as defined by ISO 23009-1, 4th edition, 5.3.7.2. */ /** A label for a {@link Format}. */
@UnstableApi @UnstableApi
public class Label implements Parcelable, Bundleable { public class Label {
/** Declares the language code(s) for this Label. */ /**
@Nullable public final String lang; * The language of this label, as an IETF BCP 47 conformant tag, or null if unknown or not
* applicable.
*/
@Nullable public final String language;
/** The value for this Label. */ /** The value for this label. */
public final String value; public final String value;
/** /**
* @param lang The lang code. * Creates a label.
* @param value The value. *
* @param language The language of this label, as an IETF BCP 47 conformant tag, or null if
* unknown or not applicable.
* @param value The label value.
*/ */
public Label(@Nullable String lang, String value) { public Label(@Nullable String language, String value) {
this.lang = lang; this.language = Util.normalizeLanguageCode(language);
this.value = value; this.value = value;
} }
/* package */ Label(Parcel in) {
lang = in.readString();
value = in.readString();
}
@Override @Override
public boolean equals(@Nullable Object o) { public boolean equals(@Nullable Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Label label = (Label) o; Label label = (Label) o;
return Util.areEqual(lang, label.lang) && Util.areEqual(value, label.value); return Util.areEqual(language, label.language) && Util.areEqual(value, label.value);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = value.hashCode(); int result = value.hashCode();
result = 31 * result + (lang != null ? lang.hashCode() : 0); result = 31 * result + (language != null ? language.hashCode() : 0);
return result; return result;
} }
@Override private static final String FIELD_LANGUAGE_INDEX = Util.intToStringMaxRadix(0);
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(lang);
dest.writeString(value);
}
public static final Parcelable.Creator<Label> CREATOR =
new Parcelable.Creator<Label>() {
@Override
public Label createFromParcel(Parcel in) {
return new Label(in);
}
@Override
public Label[] newArray(int size) {
return new Label[size];
}
};
private static final String FIELD_LANG_INDEX = Util.intToStringMaxRadix(0);
private static final String FIELD_VALUE_INDEX = Util.intToStringMaxRadix(1); private static final String FIELD_VALUE_INDEX = Util.intToStringMaxRadix(1);
@Override /** Serializes this instance to a {@link Bundle}. */
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(FIELD_LANG_INDEX, lang); if (language != null) {
bundle.putString(FIELD_LANGUAGE_INDEX, language);
}
bundle.putString(FIELD_VALUE_INDEX, value); bundle.putString(FIELD_VALUE_INDEX, value);
return bundle; return bundle;
} }
/** /** Deserializes an instance from a {@link Bundle} produced by {@link #toBundle()}. */
* Constructs an instance of {@link Label} from a {@link Bundle} produced by {@link #toBundle()}.
*/
public static Label fromBundle(Bundle bundle) { public static Label fromBundle(Bundle bundle) {
return new Label( return new Label(
bundle.getString(FIELD_LANG_INDEX), checkNotNull(bundle.getString(FIELD_VALUE_INDEX))); bundle.getString(FIELD_LANGUAGE_INDEX), checkNotNull(bundle.getString(FIELD_VALUE_INDEX)));
} }
} }

View File

@ -20,10 +20,12 @@ import static androidx.media3.common.MimeTypes.VIDEO_MP4;
import static androidx.media3.common.MimeTypes.VIDEO_WEBM; import static androidx.media3.common.MimeTypes.VIDEO_WEBM;
import static androidx.media3.test.utils.TestUtil.buildTestData; import static androidx.media3.test.utils.TestUtil.buildTestData;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.os.Bundle; import android.os.Bundle;
import androidx.media3.test.utils.FakeMetadataEntry; import androidx.media3.test.utils.FakeMetadataEntry;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
@ -59,44 +61,58 @@ public final class FormatTest {
@Test @Test
public void formatBuild_withLabelAndWithoutLabels_labelIsInLabels() { public void formatBuild_withLabelAndWithoutLabels_labelIsInLabels() {
Format format = createTestFormat(); Format format = new Format.Builder().setLabel("label").setLabels(ImmutableList.of()).build();
format = format.buildUpon().setLabels(null).build();
assertThat(format.labels.size()).isEqualTo(1); assertThat(format.label).isEqualTo("label");
assertThat(format.labels.get(0).value).isEqualTo(format.label); assertThat(format.labels).hasSize(1);
assertThat(format.labels.get(0).value).isEqualTo("label");
} }
@Test @Test
public void formatBuild_withLabelsAndLanguageMatchingAndWithoutLabel_theLanguageMatchIsInLabel() { public void formatBuild_withLabelsAndLanguageMatchingAndWithoutLabel_theLanguageMatchIsInLabel() {
Format format = createTestFormat(); Format format =
format.labels.add(new Label("language", "matchingLabel")); new Format.Builder()
format = format.buildUpon().setLabel(null).build(); .setLabel(null)
.setLabels(
ImmutableList.of(
new Label("en", "nonDefaultLabel"), new Label("zh", "matchingLabel")))
.setLanguage("zh")
.build();
assertThat(format.label).isEqualTo("matchingLabel"); assertThat(format.label).isEqualTo("matchingLabel");
} }
@Test @Test
public void formatBuild_withLabelsAndNoLanguageMatchingAndWithoutLabel_theFirstIsInLabel() { public void formatBuild_withLabelsAndNoLanguageMatchingAndWithoutLabel_theFirstIsInLabel() {
Format format = createTestFormat(); Format format =
format.labels.add(new Label("otherLanguage", "otherLabel")); new Format.Builder()
format = format.buildUpon().setLabel(null).build(); .setLabel(null)
.setLabels(
ImmutableList.of(new Label("fr", "firstLabel"), new Label("de", "secondLabel")))
.setLanguage("en")
.build();
assertThat(format.label).isEqualTo("label"); assertThat(format.label).isEqualTo("firstLabel");
} }
@Test @Test
public void formatBuild_withoutLabelsOrLabel_theyAreEmpty() { public void formatBuild_withoutLabelsOrLabel_bothEmpty() {
Format format = createTestFormat(); Format format = createTestFormat();
format = format.buildUpon().setLabel(null).setLabels(null).build(); format = format.buildUpon().setLabel(null).setLabels(ImmutableList.of()).build();
assertThat(format.label).isEqualTo(null); assertThat(format.label).isNull();
assertThat(format.labels.size()).isEqualTo(0); assertThat(format.labels).isEmpty();
} }
@Test(expected = IllegalStateException.class) @Test
public void formatBuild_withLabelAndLabelsSetButNotMatching_throwsException() { public void formatBuild_withLabelAndLabelsSetButNoMatch_throwsException() {
Format format = createTestFormat(); assertThrows(
format.buildUpon().setLabel("otherLabel").build(); IllegalStateException.class,
() ->
new Format.Builder()
.setLabel("otherLabel")
.setLabels(ImmutableList.of(new Label("en", "label")))
.build());
} }
private static Format createTestFormat() { private static Format createTestFormat() {
@ -125,12 +141,10 @@ public final class FormatTest {
.setChromaBitdepth(11) .setChromaBitdepth(11)
.build(); .build();
List<Label> labels = new ArrayList<>();
labels.add(new Label("en", "label"));
return new Format.Builder() return new Format.Builder()
.setId("id") .setId("id")
.setLabel("label") .setLabel("label")
.setLabels(labels) .setLabels(ImmutableList.of(new Label("en", "label")))
.setLanguage("language") .setLanguage("language")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT) .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setRoleFlags(C.ROLE_FLAG_MAIN) .setRoleFlags(C.ROLE_FLAG_MAIN)

View File

@ -864,10 +864,11 @@ public class DashManifestParser extends DefaultHandler
ArrayList<SchemeData> extraDrmSchemeDatas, ArrayList<SchemeData> extraDrmSchemeDatas,
ArrayList<Descriptor> extraInbandEventStreams) { ArrayList<Descriptor> extraInbandEventStreams) {
Format.Builder formatBuilder = representationInfo.format.buildUpon(); Format.Builder formatBuilder = representationInfo.format.buildUpon();
if (label != null) { if (label != null && labels.isEmpty()) {
formatBuilder.setLabel(label); formatBuilder.setLabel(label);
} else {
formatBuilder.setLabels(labels);
} }
formatBuilder.setLabels(labels);
@Nullable String drmSchemeType = representationInfo.drmSchemeType; @Nullable String drmSchemeType = representationInfo.drmSchemeType;
if (drmSchemeType == null) { if (drmSchemeType == null) {
drmSchemeType = extraDrmSchemeType; drmSchemeType = extraDrmSchemeType;

View File

@ -281,7 +281,10 @@ public class DashManifestParserTest {
assertThat(adaptationSets.get(0).representations.get(0).format.label).isEqualTo("audio label"); assertThat(adaptationSets.get(0).representations.get(0).format.label).isEqualTo("audio label");
assertThat(adaptationSets.get(1).representations.get(0).format.label).isEqualTo("video label"); assertThat(adaptationSets.get(1).representations.get(0).format.label).isEqualTo("video label");
assertThat(adaptationSets.get(0).representations.get(0).format.labels.size()).isEqualTo(0); assertThat(adaptationSets.get(0).representations.get(0).format.labels).hasSize(1);
assertThat(adaptationSets.get(0).representations.get(0).format.labels.get(0).value)
.isEqualTo("audio label");
assertThat(adaptationSets.get(1).representations.get(0).format.labels).hasSize(1);
assertThat(adaptationSets.get(1).representations.get(0).format.labels.get(0).value) assertThat(adaptationSets.get(1).representations.get(0).format.labels.get(0).value)
.isEqualTo("video label"); .isEqualTo("video label");
} }
@ -441,7 +444,7 @@ public class DashManifestParserTest {
xpp.next(); xpp.next();
Label label = parser.parseLabel(xpp); Label label = parser.parseLabel(xpp);
assertThat(label.lang).isEqualTo("en"); assertThat(label.language).isEqualTo("en");
assertThat(label.value).isEqualTo("test label"); assertThat(label.value).isEqualTo("test label");
assertNextTag(xpp); assertNextTag(xpp);
} }
@ -454,7 +457,7 @@ public class DashManifestParserTest {
xpp.next(); xpp.next();
Label label = parser.parseLabel(xpp); Label label = parser.parseLabel(xpp);
assertThat(label.lang).isEqualTo(null); assertThat(label.language).isNull();
assertNextTag(xpp); assertNextTag(xpp);
} }
@ -466,7 +469,8 @@ public class DashManifestParserTest {
xpp.next(); xpp.next();
Label label = parser.parseLabel(xpp); Label label = parser.parseLabel(xpp);
assertThat(label.value).isEqualTo(""); assertThat(label.value).isNotNull();
assertThat(label.value).isEmpty();
assertNextTag(xpp); assertNextTag(xpp);
} }

View File

@ -877,7 +877,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
int roleFlags = 0; int roleFlags = 0;
@Nullable String language = null; @Nullable String language = null;
@Nullable String label = null; @Nullable String label = null;
@Nullable List<Label> labels = null; List<Label> labels = ImmutableList.of();
if (mediaTagFormat != null) { if (mediaTagFormat != null) {
codecs = mediaTagFormat.codecs; codecs = mediaTagFormat.codecs;
metadata = mediaTagFormat.metadata; metadata = mediaTagFormat.metadata;

View File

@ -104,7 +104,7 @@ public final class DumpableFormat implements Dumper.Dumpable {
if (!format.labels.isEmpty()) { if (!format.labels.isEmpty()) {
dumper.startBlock("labels"); dumper.startBlock("labels");
for (int i = 0; i < format.labels.size(); i++) { for (int i = 0; i < format.labels.size(); i++) {
String lang = format.labels.get(i).lang; String lang = format.labels.get(i).language;
if (lang != null) { if (lang != null) {
dumper.add("lang", lang); dumper.add("lang", lang);
} }