Merge pull request #1054 from jekopena:main

PiperOrigin-RevId: 619573181
(cherry picked from commit 8fe70332ee68faf8945ddb9cf2c897d809af8e49)
This commit is contained in:
Copybara-Service 2024-03-27 10:24:32 -07:00 committed by SheenaChhabra
parent 4caed3cfb2
commit 7b5522fc58
33 changed files with 352 additions and 16 deletions

View File

@ -2,6 +2,8 @@
### Unreleased changes ### Unreleased changes
* 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.
@ -28,7 +30,7 @@
from WAV files ([#1117](https://github.com/androidx/media/pull/1117)). from WAV files ([#1117](https://github.com/androidx/media/pull/1117)).
* MP3: Populate `Format.averageBitrate` from metadata frames such as * MP3: Populate `Format.averageBitrate` from metadata frames such as
`XING` and `VBRI`. `XING` and `VBRI`.
* Audio: * Audio:
* Allow renderer recovery by disabling offload if audio track fails to * Allow renderer recovery by disabling offload if audio track fails to
initialize in offload mode. initialize in offload mode.
* Video: * Video:
@ -60,10 +62,13 @@
* Fix issue where `MediaMetadata` with just non-null `extras` is not * Fix issue where `MediaMetadata` with just non-null `extras` is not
transmitted between media controllers and sessions transmitted between media controllers and sessions
([#1176](https://github.com/androidx/media/issues/1176)). ([#1176](https://github.com/androidx/media/issues/1176)).
* UI: * UI:
* Fallback to include audio track language name if `Locale` cannot * Fallback to include audio track language name if `Locale` cannot
identify a display name identify a display name
([#988](https://github.com/androidx/media/issues/988)). ([#988](https://github.com/androidx/media/issues/988)).
* DASH Extension:
* Populate all `Label` elements from the manifest into `Format.labels`
([#1054](https://github.com/androidx/media/pull/1054)).
* RTSP Extension: * RTSP Extension:
* Skip empty session information values (i-tags) in SDP parsing * Skip empty session information values (i-tags) in SDP parsing
([#1087](https://github.com/androidx/media/issues/1087)). ([#1087](https://github.com/androidx/media/issues/1087)).

View File

@ -15,15 +15,18 @@
*/ */
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;
import android.text.TextUtils;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleCollectionUtil; import androidx.media3.common.util.BundleCollectionUtil;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -50,6 +53,7 @@ import java.util.UUID;
* <ul> * <ul>
* <li>{@link #id} * <li>{@link #id}
* <li>{@link #label} * <li>{@link #label}
* <li>{@link #labels}
* <li>{@link #language} * <li>{@link #language}
* <li>{@link #selectionFlags} * <li>{@link #selectionFlags}
* <li>{@link #roleFlags} * <li>{@link #roleFlags}
@ -137,6 +141,7 @@ public final class Format implements Bundleable {
@Nullable private String id; @Nullable private String id;
@Nullable private String label; @Nullable private String label;
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;
@ -192,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.
@ -225,6 +231,7 @@ public final class Format implements Bundleable {
private Builder(Format format) { private Builder(Format format) {
this.id = format.id; this.id = format.id;
this.label = format.label; this.label = format.label;
this.labels = format.labels;
this.language = format.language; this.language = format.language;
this.selectionFlags = format.selectionFlags; this.selectionFlags = format.selectionFlags;
this.roleFlags = format.roleFlags; this.roleFlags = format.roleFlags;
@ -293,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.
*/ */
@ -302,6 +312,21 @@ public final class Format implements Bundleable {
return this; return this;
} }
/**
* 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}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setLabels(List<Label> labels) {
this.labels = ImmutableList.copyOf(labels);
return this;
}
/** /**
* Sets {@link Format#language}. The default value is {@code null}. * Sets {@link Format#language}. The default value is {@code null}.
* *
@ -740,9 +765,22 @@ 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 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;
/** 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. */
@Nullable public final String language; @Nullable public final String language;
@ -931,8 +969,20 @@ public final class Format implements Bundleable {
private Format(Builder builder) { private Format(Builder builder) {
id = builder.id; id = builder.id;
label = builder.label;
language = Util.normalizeLanguageCode(builder.language); language = Util.normalizeLanguageCode(builder.language);
if (builder.labels.isEmpty() && builder.label != null) {
labels = ImmutableList.of(new Label(language, builder.label));
label = builder.label;
} else if (!builder.labels.isEmpty() && builder.label == null) {
labels = builder.labels;
label = getDefaultLabel(builder.labels, language);
} else {
checkState(
(builder.labels.isEmpty() && builder.label == null)
|| (builder.labels.stream().anyMatch(l -> l.value.equals(builder.label))));
labels = builder.labels;
label = builder.label;
}
selectionFlags = builder.selectionFlags; selectionFlags = builder.selectionFlags;
roleFlags = builder.roleFlags; roleFlags = builder.roleFlags;
averageBitrate = builder.averageBitrate; averageBitrate = builder.averageBitrate;
@ -1003,6 +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.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) {
@ -1044,6 +1095,7 @@ public final class Format implements Bundleable {
return buildUpon() return buildUpon()
.setId(id) .setId(id)
.setLabel(label) .setLabel(label)
.setLabels(labels)
.setLanguage(language) .setLanguage(language)
.setSelectionFlags(selectionFlags) .setSelectionFlags(selectionFlags)
.setRoleFlags(roleFlags) .setRoleFlags(roleFlags)
@ -1111,7 +1163,8 @@ public final class Format implements Bundleable {
// Some fields for which hashing is expensive are deliberately omitted. // Some fields for which hashing is expensive are deliberately omitted.
int result = 17; int result = 17;
result = 31 * result + (id == null ? 0 : id.hashCode()); result = 31 * result + (id == null ? 0 : id.hashCode());
result = 31 * result + (label != null ? label.hashCode() : 0); result = 31 * result + (label == null ? 0 : label.hashCode());
result = 31 * result + labels.hashCode();
result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + (language == null ? 0 : language.hashCode());
result = 31 * result + selectionFlags; result = 31 * result + selectionFlags;
result = 31 * result + roleFlags; result = 31 * result + roleFlags;
@ -1190,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)
@ -1281,8 +1335,10 @@ 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.label != null) { if (!format.labels.isEmpty()) {
builder.append(", label=").append(format.label); builder.append(", labels=[");
Joiner.on(',').appendTo(builder, format.labels);
builder.append("]");
} }
if (format.selectionFlags != 0) { if (format.selectionFlags != 0) {
builder.append(", selectionFlags=["); builder.append(", selectionFlags=[");
@ -1331,6 +1387,7 @@ public final class Format implements Bundleable {
private static final String FIELD_CRYPTO_TYPE = Util.intToStringMaxRadix(29); private static final String FIELD_CRYPTO_TYPE = Util.intToStringMaxRadix(29);
private static final String FIELD_TILE_COUNT_HORIZONTAL = Util.intToStringMaxRadix(30); private static final String FIELD_TILE_COUNT_HORIZONTAL = Util.intToStringMaxRadix(30);
private static final String FIELD_TILE_COUNT_VERTICAL = Util.intToStringMaxRadix(31); private static final String FIELD_TILE_COUNT_VERTICAL = Util.intToStringMaxRadix(31);
private static final String FIELD_LABELS = Util.intToStringMaxRadix(32);
@UnstableApi @UnstableApi
@Override @Override
@ -1347,6 +1404,8 @@ public final class Format implements Bundleable {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(FIELD_ID, id); bundle.putString(FIELD_ID, id);
bundle.putString(FIELD_LABEL, label); bundle.putString(FIELD_LABEL, label);
bundle.putParcelableArrayList(
FIELD_LABELS, BundleCollectionUtil.toBundleArrayList(labels, Label::toBundle));
bundle.putString(FIELD_LANGUAGE, language); bundle.putString(FIELD_LANGUAGE, language);
bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags); bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags);
bundle.putInt(FIELD_ROLE_FLAGS, roleFlags); bundle.putInt(FIELD_ROLE_FLAGS, roleFlags);
@ -1413,7 +1472,14 @@ public final class Format implements Bundleable {
BundleCollectionUtil.ensureClassLoader(bundle); BundleCollectionUtil.ensureClassLoader(bundle);
builder builder
.setId(defaultIfNull(bundle.getString(FIELD_ID), DEFAULT.id)) .setId(defaultIfNull(bundle.getString(FIELD_ID), DEFAULT.id))
.setLabel(defaultIfNull(bundle.getString(FIELD_LABEL), DEFAULT.label)) .setLabel(defaultIfNull(bundle.getString(FIELD_LABEL), DEFAULT.label));
@Nullable List<Bundle> labelsBundles = bundle.getParcelableArrayList(FIELD_LABELS);
List<Label> labels =
labelsBundles == null
? ImmutableList.of()
: BundleCollectionUtil.fromBundleList(Label::fromBundle, labelsBundles);
builder
.setLabels(labels)
.setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language)) .setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language))
.setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags)) .setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags))
.setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags)) .setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags))
@ -1492,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

@ -0,0 +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;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
/** A label for a {@link Format}. */
@UnstableApi
public class Label {
/**
* 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. */
public final String value;
/**
* Creates a label.
*
* @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 language, String value) {
this.language = Util.normalizeLanguageCode(language);
this.value = value;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Label label = (Label) o;
return Util.areEqual(language, label.language) && Util.areEqual(value, label.value);
}
@Override
public int hashCode() {
int result = value.hashCode();
result = 31 * result + (language != null ? language.hashCode() : 0);
return result;
}
private static final String FIELD_LANGUAGE_INDEX = Util.intToStringMaxRadix(0);
private static final String FIELD_VALUE_INDEX = Util.intToStringMaxRadix(1);
/** Serializes this instance to a {@link Bundle}. */
public Bundle toBundle() {
Bundle bundle = new Bundle();
if (language != null) {
bundle.putString(FIELD_LANGUAGE_INDEX, language);
}
bundle.putString(FIELD_VALUE_INDEX, value);
return bundle;
}
/** Deserializes an instance from a {@link Bundle} produced by {@link #toBundle()}. */
public static Label fromBundle(Bundle bundle) {
return new Label(
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;
@ -57,6 +59,62 @@ public final class FormatTest {
assertThat(formatWithMetadataExcluded).isEqualTo(format.buildUpon().setMetadata(null).build()); assertThat(formatWithMetadataExcluded).isEqualTo(format.buildUpon().setMetadata(null).build());
} }
@Test
public void formatBuild_withLabelAndWithoutLabels_labelIsInLabels() {
Format format = new Format.Builder().setLabel("label").setLabels(ImmutableList.of()).build();
assertThat(format.label).isEqualTo("label");
assertThat(format.labels).hasSize(1);
assertThat(format.labels.get(0).value).isEqualTo("label");
}
@Test
public void formatBuild_withLabelsAndLanguageMatchingAndWithoutLabel_theLanguageMatchIsInLabel() {
Format format =
new Format.Builder()
.setLabel(null)
.setLabels(
ImmutableList.of(
new Label("en", "nonDefaultLabel"), new Label("zh", "matchingLabel")))
.setLanguage("zh")
.build();
assertThat(format.label).isEqualTo("matchingLabel");
}
@Test
public void formatBuild_withLabelsAndNoLanguageMatchingAndWithoutLabel_theFirstIsInLabel() {
Format format =
new Format.Builder()
.setLabel(null)
.setLabels(
ImmutableList.of(new Label("fr", "firstLabel"), new Label("de", "secondLabel")))
.setLanguage("en")
.build();
assertThat(format.label).isEqualTo("firstLabel");
}
@Test
public void formatBuild_withoutLabelsOrLabel_bothEmpty() {
Format format = createTestFormat();
format = format.buildUpon().setLabel(null).setLabels(ImmutableList.of()).build();
assertThat(format.label).isNull();
assertThat(format.labels).isEmpty();
}
@Test
public void formatBuild_withLabelAndLabelsSetButNoMatch_throwsException() {
assertThrows(
IllegalStateException.class,
() ->
new Format.Builder()
.setLabel("otherLabel")
.setLabels(ImmutableList.of(new Label("en", "label")))
.build());
}
private static Format createTestFormat() { private static Format createTestFormat() {
byte[] initData1 = new byte[] {1, 2, 3}; byte[] initData1 = new byte[] {1, 2, 3};
byte[] initData2 = new byte[] {4, 5, 6}; byte[] initData2 = new byte[] {4, 5, 6};
@ -86,6 +144,7 @@ public final class FormatTest {
return new Format.Builder() return new Format.Builder()
.setId("id") .setId("id")
.setLabel("label") .setLabel("label")
.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

@ -451,6 +451,7 @@ public abstract class DecoderAudioRenderer<
.setMetadata(inputFormat.metadata) .setMetadata(inputFormat.metadata)
.setId(inputFormat.id) .setId(inputFormat.id)
.setLabel(inputFormat.label) .setLabel(inputFormat.label)
.setLabels(inputFormat.labels)
.setLanguage(inputFormat.language) .setLanguage(inputFormat.language)
.setSelectionFlags(inputFormat.selectionFlags) .setSelectionFlags(inputFormat.selectionFlags)
.setRoleFlags(inputFormat.roleFlags) .setRoleFlags(inputFormat.roleFlags)

View File

@ -552,6 +552,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
.setMetadata(format.metadata) .setMetadata(format.metadata)
.setId(format.id) .setId(format.id)
.setLabel(format.label) .setLabel(format.label)
.setLabels(format.labels)
.setLanguage(format.language) .setLanguage(format.language)
.setSelectionFlags(format.selectionFlags) .setSelectionFlags(format.selectionFlags)
.setRoleFlags(format.roleFlags) .setRoleFlags(format.roleFlags)

View File

@ -518,6 +518,7 @@ public final class OutputConsumerAdapterV30 implements MediaParser.OutputConsume
.setRoleFlags(muxedCaptionFormat.roleFlags) .setRoleFlags(muxedCaptionFormat.roleFlags)
.setSelectionFlags(muxedCaptionFormat.selectionFlags) .setSelectionFlags(muxedCaptionFormat.selectionFlags)
.setLabel(muxedCaptionFormat.label) .setLabel(muxedCaptionFormat.label)
.setLabels(muxedCaptionFormat.labels)
.setMetadata(muxedCaptionFormat.metadata); .setMetadata(muxedCaptionFormat.metadata);
break; break;
} }

View File

@ -29,6 +29,7 @@ import androidx.media3.common.C;
import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData;
import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.Label;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
@ -405,6 +406,7 @@ public class DashManifestParser extends DefaultHandler
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE);
String language = xpp.getAttributeValue(null, "lang"); String language = xpp.getAttributeValue(null, "lang");
String label = xpp.getAttributeValue(null, "label"); String label = xpp.getAttributeValue(null, "label");
List<Label> labels = new ArrayList<>();
String drmSchemeType = null; String drmSchemeType = null;
ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>(); ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();
ArrayList<Descriptor> inbandEventStreams = new ArrayList<>(); ArrayList<Descriptor> inbandEventStreams = new ArrayList<>();
@ -504,7 +506,7 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
} else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) {
label = parseLabel(xpp); labels.add(parseLabel(xpp));
} else if (XmlPullParserUtil.isStartTag(xpp)) { } else if (XmlPullParserUtil.isStartTag(xpp)) {
parseAdaptationSetChild(xpp); parseAdaptationSetChild(xpp);
} }
@ -517,6 +519,7 @@ public class DashManifestParser extends DefaultHandler
buildRepresentation( buildRepresentation(
representationInfos.get(i), representationInfos.get(i),
label, label,
labels,
drmSchemeType, drmSchemeType,
drmSchemeDatas, drmSchemeDatas,
inbandEventStreams)); inbandEventStreams));
@ -856,12 +859,15 @@ public class DashManifestParser extends DefaultHandler
protected Representation buildRepresentation( protected Representation buildRepresentation(
RepresentationInfo representationInfo, RepresentationInfo representationInfo,
@Nullable String label, @Nullable String label,
List<Label> labels,
@Nullable String extraDrmSchemeType, @Nullable String extraDrmSchemeType,
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);
} }
@Nullable String drmSchemeType = representationInfo.drmSchemeType; @Nullable String drmSchemeType = representationInfo.drmSchemeType;
if (drmSchemeType == null) { if (drmSchemeType == null) {
@ -1405,8 +1411,10 @@ public class DashManifestParser extends DefaultHandler
* @throws IOException If an error occurs reading the element. * @throws IOException If an error occurs reading the element.
* @return The parsed label. * @return The parsed label.
*/ */
protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { protected Label parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException {
return parseText(xpp, "Label"); String lang = xpp.getAttributeValue(null, "lang");
String value = parseText(xpp, "Label");
return new Label(lang, value);
} }
/** /**

View File

@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.Label;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.dash.manifest.Representation.MultiSegmentRepresentation; import androidx.media3.exoplayer.dash.manifest.Representation.MultiSegmentRepresentation;
@ -280,6 +281,12 @@ 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).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)
.isEqualTo("video label");
} }
@Test @Test
@ -431,13 +438,26 @@ public class DashManifestParserTest {
@Test @Test
public void parseLabel() throws Exception { public void parseLabel() throws Exception {
DashManifestParser parser = new DashManifestParser();
XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
xpp.setInput(new StringReader("<Label lang=\"en\">test label</Label>" + NEXT_TAG));
xpp.next();
Label label = parser.parseLabel(xpp);
assertThat(label.language).isEqualTo("en");
assertThat(label.value).isEqualTo("test label");
assertNextTag(xpp);
}
@Test
public void parseLabel_noLang() throws Exception {
DashManifestParser parser = new DashManifestParser(); DashManifestParser parser = new DashManifestParser();
XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
xpp.setInput(new StringReader("<Label>test label</Label>" + NEXT_TAG)); xpp.setInput(new StringReader("<Label>test label</Label>" + NEXT_TAG));
xpp.next(); xpp.next();
String label = parser.parseLabel(xpp); Label label = parser.parseLabel(xpp);
assertThat(label).isEqualTo("test label"); assertThat(label.language).isNull();
assertNextTag(xpp); assertNextTag(xpp);
} }
@ -448,8 +468,9 @@ public class DashManifestParserTest {
xpp.setInput(new StringReader("<Label/>" + NEXT_TAG)); xpp.setInput(new StringReader("<Label/>" + NEXT_TAG));
xpp.next(); xpp.next();
String label = parser.parseLabel(xpp); Label label = parser.parseLabel(xpp);
assertThat(label).isEqualTo(""); assertThat(label.value).isNotNull();
assertThat(label.value).isEmpty();
assertNextTag(xpp); assertNextTag(xpp);
} }

View File

@ -21,6 +21,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.Label;
import androidx.media3.common.Metadata; import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey; import androidx.media3.common.StreamKey;
@ -852,6 +853,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return new Format.Builder() return new Format.Builder()
.setId(variantFormat.id) .setId(variantFormat.id)
.setLabel(variantFormat.label) .setLabel(variantFormat.label)
.setLabels(variantFormat.labels)
.setContainerMimeType(variantFormat.containerMimeType) .setContainerMimeType(variantFormat.containerMimeType)
.setSampleMimeType(sampleMimeType) .setSampleMimeType(sampleMimeType)
.setCodecs(codecs) .setCodecs(codecs)
@ -875,6 +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;
List<Label> labels = ImmutableList.of();
if (mediaTagFormat != null) { if (mediaTagFormat != null) {
codecs = mediaTagFormat.codecs; codecs = mediaTagFormat.codecs;
metadata = mediaTagFormat.metadata; metadata = mediaTagFormat.metadata;
@ -883,6 +886,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
roleFlags = mediaTagFormat.roleFlags; roleFlags = mediaTagFormat.roleFlags;
language = mediaTagFormat.language; language = mediaTagFormat.language;
label = mediaTagFormat.label; label = mediaTagFormat.label;
labels = mediaTagFormat.labels;
} else { } else {
codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO);
metadata = variantFormat.metadata; metadata = variantFormat.metadata;
@ -892,6 +896,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
roleFlags = variantFormat.roleFlags; roleFlags = variantFormat.roleFlags;
language = variantFormat.language; language = variantFormat.language;
label = variantFormat.label; label = variantFormat.label;
labels = variantFormat.labels;
} }
} }
@Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs); @Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs);
@ -900,6 +905,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return new Format.Builder() return new Format.Builder()
.setId(variantFormat.id) .setId(variantFormat.id)
.setLabel(label) .setLabel(label)
.setLabels(labels)
.setContainerMimeType(variantFormat.containerMimeType) .setContainerMimeType(variantFormat.containerMimeType)
.setSampleMimeType(sampleMimeType) .setSampleMimeType(sampleMimeType)
.setCodecs(codecs) .setCodecs(codecs)

View File

@ -1584,6 +1584,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
.buildUpon() .buildUpon()
.setId(playlistFormat.id) .setId(playlistFormat.id)
.setLabel(playlistFormat.label) .setLabel(playlistFormat.label)
.setLabels(playlistFormat.labels)
.setLanguage(playlistFormat.language) .setLanguage(playlistFormat.language)
.setSelectionFlags(playlistFormat.selectionFlags) .setSelectionFlags(playlistFormat.selectionFlags)
.setRoleFlags(playlistFormat.roleFlags) .setRoleFlags(playlistFormat.roleFlags)

View File

@ -52,5 +52,6 @@ public final class SsManifestParserTest {
TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_ISMC_1)); TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_ISMC_1));
assertThat(ssManifest.streamElements[0].formats[0].label).isEqualTo("video"); assertThat(ssManifest.streamElements[0].formats[0].label).isEqualTo("video");
assertThat(ssManifest.streamElements[0].formats[0].labels.get(0).value).isEqualTo("video");
} }
} }

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -275,6 +275,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -276,6 +276,9 @@ track 3:
selectionFlags = [default] selectionFlags = [default]
language = en language = en
label = Subs Label label = Subs Label
labels:
lang = en
value = Subs Label
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1

View File

@ -101,6 +101,17 @@ public final class DumpableFormat implements Dumper.Dumpable {
format -> Util.getRoleFlagStrings(format.roleFlags)); format -> Util.getRoleFlagStrings(format.roleFlags));
addIfNonDefault(dumper, "language", format, DEFAULT_FORMAT, format -> format.language); addIfNonDefault(dumper, "language", format, DEFAULT_FORMAT, format -> format.language);
addIfNonDefault(dumper, "label", format, DEFAULT_FORMAT, format -> format.label); addIfNonDefault(dumper, "label", format, DEFAULT_FORMAT, format -> format.label);
if (!format.labels.isEmpty()) {
dumper.startBlock("labels");
for (int i = 0; i < format.labels.size(); i++) {
String lang = format.labels.get(i).language;
if (lang != null) {
dumper.add("lang", lang);
}
dumper.add("value", format.labels.get(i).value);
}
dumper.endBlock();
}
if (format.drmInitData != null) { if (format.drmInitData != null) {
dumper.add("drmInitData", format.drmInitData.hashCode()); dumper.add("drmInitData", format.drmInitData.hashCode());
} }