diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0ccde8e7026..fc4a0204e69 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,7 @@ ### Unreleased changes * Common Library: + * Add `Format.labels` to allow localized or other alternative labels. * ExoPlayer: * Fix issue where `PreloadMediaPeriod` cannot retain the streams when it is preloaded again. @@ -100,6 +101,8 @@ * RTMP Extension: * HLS Extension: * DASH Extension: + * Populate all `Label` elements from the manifest into `Format.labels` + ([#1054](https://github.com/androidx/media/pull/1054)). * Smooth Streaming Extension: * RTSP Extension: * Skip empty session information values (i-tags) in SDP parsing diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java index 1a9a77a2141..cda7ac23e9d 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Format.java +++ b/libraries/common/src/main/java/androidx/media3/common/Format.java @@ -15,15 +15,18 @@ */ package androidx.media3.common; +import static androidx.media3.common.util.Assertions.checkState; import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.BundleCollectionUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -50,6 +53,7 @@ *
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}. * @return The builder. */ @@ -302,6 +312,21 @@ public Builder setLabel(@Nullable String label) { return this; } + /** + * Sets {@link Format#labels}. The default value is an empty list. + * + *
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 labels) { + this.labels = ImmutableList.copyOf(labels); + return this; + } + /** * Sets {@link Format#language}. The default value is {@code null}. * @@ -740,9 +765,22 @@ public Format build() { /** An identifier for the format, or null if unknown or not applicable. */ @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. + * + * 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; + /** + * The human readable list of labels, or an empty list if unknown or not applicable. + * + * 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 labels; + /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ @Nullable public final String language; @@ -931,8 +969,20 @@ public Format build() { private Format(Builder builder) { id = builder.id; - label = builder.label; 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; roleFlags = builder.roleFlags; averageBitrate = builder.averageBitrate; @@ -1003,6 +1053,7 @@ public Format withManifestFormatInfo(Format manifestFormat) { // Prefer manifest values, but fill in from sample format if missing. @Nullable String label = manifestFormat.label != null ? manifestFormat.label : this.label; + List labels = !manifestFormat.labels.isEmpty() ? manifestFormat.labels : this.labels; @Nullable String language = this.language; if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO) && manifestFormat.language != null) { @@ -1044,6 +1095,7 @@ public Format withManifestFormatInfo(Format manifestFormat) { return buildUpon() .setId(id) .setLabel(label) + .setLabels(labels) .setLanguage(language) .setSelectionFlags(selectionFlags) .setRoleFlags(roleFlags) @@ -1111,7 +1163,8 @@ public int hashCode() { // Some fields for which hashing is expensive are deliberately omitted. int result = 17; 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 + selectionFlags; result = 31 * result + roleFlags; @@ -1190,6 +1243,7 @@ public boolean equals(@Nullable Object obj) { && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 && Util.areEqual(id, other.id) && Util.areEqual(label, other.label) + && labels.equals(other.labels) && Util.areEqual(codecs, other.codecs) && Util.areEqual(containerMimeType, other.containerMimeType) && Util.areEqual(sampleMimeType, other.sampleMimeType) @@ -1281,8 +1335,10 @@ public static String toLogString(@Nullable Format format) { if (format.language != null) { builder.append(", language=").append(format.language); } - if (format.label != null) { - builder.append(", label=").append(format.label); + if (!format.labels.isEmpty()) { + builder.append(", labels=["); + Joiner.on(',').appendTo(builder, format.labels); + builder.append("]"); } if (format.selectionFlags != 0) { builder.append(", selectionFlags=["); @@ -1331,6 +1387,7 @@ public static String toLogString(@Nullable Format format) { 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_VERTICAL = Util.intToStringMaxRadix(31); + private static final String FIELD_LABELS = Util.intToStringMaxRadix(32); @UnstableApi @Override @@ -1347,6 +1404,8 @@ public Bundle toBundle(boolean excludeMetadata) { Bundle bundle = new Bundle(); bundle.putString(FIELD_ID, id); bundle.putString(FIELD_LABEL, label); + bundle.putParcelableArrayList( + FIELD_LABELS, BundleCollectionUtil.toBundleArrayList(labels, Label::toBundle)); bundle.putString(FIELD_LANGUAGE, language); bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags); bundle.putInt(FIELD_ROLE_FLAGS, roleFlags); @@ -1413,7 +1472,14 @@ public static Format fromBundle(Bundle bundle) { BundleCollectionUtil.ensureClassLoader(bundle); builder .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 labelsBundles = bundle.getParcelableArrayList(FIELD_LABELS); + List labels = + labelsBundles == null + ? ImmutableList.of() + : BundleCollectionUtil.fromBundleList(Label::fromBundle, labelsBundles); + builder + .setLabels(labels) .setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language)) .setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags)) .setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags)) @@ -1492,4 +1558,13 @@ private static String keyForInitializationData(int initialisationDataIndex) { private static T defaultIfNull(@Nullable T value, @Nullable T defaultValue) { return value != null ? value : defaultValue; } + + private static String getDefaultLabel(List labels, @Nullable String language) { + for (Label l : labels) { + if (TextUtils.equals(l.language, language)) { + return l.value; + } + } + return labels.get(0).value; + } } diff --git a/libraries/common/src/main/java/androidx/media3/common/Label.java b/libraries/common/src/main/java/androidx/media3/common/Label.java new file mode 100644 index 00000000000..455ed70dc03 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/Label.java @@ -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))); + } +} diff --git a/libraries/common/src/test/java/androidx/media3/common/FormatTest.java b/libraries/common/src/test/java/androidx/media3/common/FormatTest.java index 42cfb2f53c2..1710b232ede 100644 --- a/libraries/common/src/test/java/androidx/media3/common/FormatTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/FormatTest.java @@ -20,10 +20,12 @@ import static androidx.media3.common.MimeTypes.VIDEO_WEBM; import static androidx.media3.test.utils.TestUtil.buildTestData; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.os.Bundle; import androidx.media3.test.utils.FakeMetadataEntry; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import org.junit.Test; @@ -57,6 +59,62 @@ public void roundTripViaBundle_excludeMetadata_hasMetadataExcluded() { 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() { byte[] initData1 = new byte[] {1, 2, 3}; byte[] initData2 = new byte[] {4, 5, 6}; @@ -86,6 +144,7 @@ private static Format createTestFormat() { return new Format.Builder() .setId("id") .setLabel("label") + .setLabels(ImmutableList.of(new Label("en", "label"))) .setLanguage("language") .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) .setRoleFlags(C.ROLE_FLAG_MAIN) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java index bc895f68463..5f10a0bfbd0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java @@ -451,6 +451,7 @@ private boolean drainOutputBuffer() .setMetadata(inputFormat.metadata) .setId(inputFormat.id) .setLabel(inputFormat.label) + .setLabels(inputFormat.labels) .setLanguage(inputFormat.language) .setSelectionFlags(inputFormat.selectionFlags) .setRoleFlags(inputFormat.roleFlags) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index ddf7164cd10..c4468563d86 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -552,6 +552,7 @@ protected void onOutputFormatChanged(Format format, @Nullable MediaFormat mediaF .setMetadata(format.metadata) .setId(format.id) .setLabel(format.label) + .setLabels(format.labels) .setLanguage(format.language) .setSelectionFlags(format.selectionFlags) .setRoleFlags(format.roleFlags) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java index 8d2116897ce..ab15bba669f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java @@ -518,6 +518,7 @@ private Format toExoPlayerFormat(TrackData trackData) { .setRoleFlags(muxedCaptionFormat.roleFlags) .setSelectionFlags(muxedCaptionFormat.selectionFlags) .setLabel(muxedCaptionFormat.label) + .setLabels(muxedCaptionFormat.labels) .setMetadata(muxedCaptionFormat.metadata); break; } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java index 6ce3993665a..4fa9eea43c7 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java @@ -29,6 +29,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; +import androidx.media3.common.Label; import androidx.media3.common.MimeTypes; import androidx.media3.common.ParserException; import androidx.media3.common.util.Assertions; @@ -405,6 +406,7 @@ protected AdaptationSet parseAdaptationSet( int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); String label = xpp.getAttributeValue(null, "label"); + List labels = new ArrayList<>(); String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); @@ -504,7 +506,7 @@ protected AdaptationSet parseAdaptationSet( } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { - label = parseLabel(xpp); + labels.add(parseLabel(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } @@ -517,6 +519,7 @@ protected AdaptationSet parseAdaptationSet( buildRepresentation( representationInfos.get(i), label, + labels, drmSchemeType, drmSchemeDatas, inbandEventStreams)); @@ -856,12 +859,15 @@ protected Format buildFormat( protected Representation buildRepresentation( RepresentationInfo representationInfo, @Nullable String label, + List labels, @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format.Builder formatBuilder = representationInfo.format.buildUpon(); - if (label != null) { + if (label != null && labels.isEmpty()) { formatBuilder.setLabel(label); + } else { + formatBuilder.setLabels(labels); } @Nullable String drmSchemeType = representationInfo.drmSchemeType; if (drmSchemeType == null) { @@ -1405,8 +1411,10 @@ protected ProgramInformation parseProgramInformation(XmlPullParser xpp) * @throws IOException If an error occurs reading the element. * @return The parsed label. */ - protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { - return parseText(xpp, "Label"); + protected Label parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { + String lang = xpp.getAttributeValue(null, "lang"); + String value = parseText(xpp, "Label"); + return new Label(lang, value); } /** diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java index 29510717d7c..2e6fa372b2e 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java @@ -22,6 +22,7 @@ import androidx.media3.common.C; import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; +import androidx.media3.common.Label; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.dash.manifest.Representation.MultiSegmentRepresentation; @@ -280,6 +281,12 @@ public void parseMediaPresentationDescription_labels() throws IOException { 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(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 @@ -431,13 +438,26 @@ public void parseSegmentTimeline_timeOffsetsAndUndefinedRepeatCount() throws Exc @Test public void parseLabel() throws Exception { + DashManifestParser parser = new DashManifestParser(); + XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput(new StringReader("test 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(); XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); xpp.setInput(new StringReader("test label" + NEXT_TAG)); xpp.next(); - String label = parser.parseLabel(xpp); - assertThat(label).isEqualTo("test label"); + Label label = parser.parseLabel(xpp); + assertThat(label.language).isNull(); assertNextTag(xpp); } @@ -448,8 +468,9 @@ public void parseLabel_noText() throws Exception { xpp.setInput(new StringReader("" + NEXT_TAG)); xpp.next(); - String label = parser.parseLabel(xpp); - assertThat(label).isEqualTo(""); + Label label = parser.parseLabel(xpp); + assertThat(label.value).isNotNull(); + assertThat(label.value).isEmpty(); assertNextTag(xpp); } diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java index d43cea69d58..cfb7e825259 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java @@ -21,6 +21,7 @@ import androidx.media3.common.C; import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; +import androidx.media3.common.Label; import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; @@ -852,6 +853,7 @@ private static Format deriveVideoFormat(Format variantFormat) { return new Format.Builder() .setId(variantFormat.id) .setLabel(variantFormat.label) + .setLabels(variantFormat.labels) .setContainerMimeType(variantFormat.containerMimeType) .setSampleMimeType(sampleMimeType) .setCodecs(codecs) @@ -875,6 +877,7 @@ private static Format deriveAudioFormat( int roleFlags = 0; @Nullable String language = null; @Nullable String label = null; + List labels = ImmutableList.of(); if (mediaTagFormat != null) { codecs = mediaTagFormat.codecs; metadata = mediaTagFormat.metadata; @@ -883,6 +886,7 @@ private static Format deriveAudioFormat( roleFlags = mediaTagFormat.roleFlags; language = mediaTagFormat.language; label = mediaTagFormat.label; + labels = mediaTagFormat.labels; } else { codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); metadata = variantFormat.metadata; @@ -892,6 +896,7 @@ private static Format deriveAudioFormat( roleFlags = variantFormat.roleFlags; language = variantFormat.language; label = variantFormat.label; + labels = variantFormat.labels; } } @Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs); @@ -900,6 +905,7 @@ private static Format deriveAudioFormat( return new Format.Builder() .setId(variantFormat.id) .setLabel(label) + .setLabels(labels) .setContainerMimeType(variantFormat.containerMimeType) .setSampleMimeType(sampleMimeType) .setCodecs(codecs) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java index ede85752016..aebff9592de 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java @@ -1584,6 +1584,7 @@ private static Format deriveFormat( .buildUpon() .setId(playlistFormat.id) .setLabel(playlistFormat.label) + .setLabels(playlistFormat.labels) .setLanguage(playlistFormat.language) .setSelectionFlags(playlistFormat.selectionFlags) .setRoleFlags(playlistFormat.roleFlags) diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java index 61e921293b2..9f45a58d56e 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java @@ -52,5 +52,6 @@ public void parse_populatesFormatLabelWithStreamIndexName() throws Exception { TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_ISMC_1)); assertThat(ssManifest.streamElements[0].formats[0].label).isEqualTo("video"); + assertThat(ssManifest.streamElements[0].formats[0].labels.get(0).value).isEqualTo("video"); } } diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java index 13f4453d4da..59cd6af7264 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java @@ -101,6 +101,17 @@ public void dump(Dumper dumper) { format -> Util.getRoleFlagStrings(format.roleFlags)); addIfNonDefault(dumper, "language", format, DEFAULT_FORMAT, format -> format.language); 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) { dumper.add("drmInitData", format.drmInitData.hashCode()); }
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; + /** + * The human readable list of labels, or an empty list if unknown or not applicable. + * + *
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 labels; + /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ @Nullable public final String language; @@ -931,8 +969,20 @@ public Format build() { private Format(Builder builder) { id = builder.id; - label = builder.label; 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; roleFlags = builder.roleFlags; averageBitrate = builder.averageBitrate; @@ -1003,6 +1053,7 @@ public Format withManifestFormatInfo(Format manifestFormat) { // Prefer manifest values, but fill in from sample format if missing. @Nullable String label = manifestFormat.label != null ? manifestFormat.label : this.label; + List labels = !manifestFormat.labels.isEmpty() ? manifestFormat.labels : this.labels; @Nullable String language = this.language; if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO) && manifestFormat.language != null) { @@ -1044,6 +1095,7 @@ public Format withManifestFormatInfo(Format manifestFormat) { return buildUpon() .setId(id) .setLabel(label) + .setLabels(labels) .setLanguage(language) .setSelectionFlags(selectionFlags) .setRoleFlags(roleFlags) @@ -1111,7 +1163,8 @@ public int hashCode() { // Some fields for which hashing is expensive are deliberately omitted. int result = 17; 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 + selectionFlags; result = 31 * result + roleFlags; @@ -1190,6 +1243,7 @@ public boolean equals(@Nullable Object obj) { && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 && Util.areEqual(id, other.id) && Util.areEqual(label, other.label) + && labels.equals(other.labels) && Util.areEqual(codecs, other.codecs) && Util.areEqual(containerMimeType, other.containerMimeType) && Util.areEqual(sampleMimeType, other.sampleMimeType) @@ -1281,8 +1335,10 @@ public static String toLogString(@Nullable Format format) { if (format.language != null) { builder.append(", language=").append(format.language); } - if (format.label != null) { - builder.append(", label=").append(format.label); + if (!format.labels.isEmpty()) { + builder.append(", labels=["); + Joiner.on(',').appendTo(builder, format.labels); + builder.append("]"); } if (format.selectionFlags != 0) { builder.append(", selectionFlags=["); @@ -1331,6 +1387,7 @@ public static String toLogString(@Nullable Format format) { 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_VERTICAL = Util.intToStringMaxRadix(31); + private static final String FIELD_LABELS = Util.intToStringMaxRadix(32); @UnstableApi @Override @@ -1347,6 +1404,8 @@ public Bundle toBundle(boolean excludeMetadata) { Bundle bundle = new Bundle(); bundle.putString(FIELD_ID, id); bundle.putString(FIELD_LABEL, label); + bundle.putParcelableArrayList( + FIELD_LABELS, BundleCollectionUtil.toBundleArrayList(labels, Label::toBundle)); bundle.putString(FIELD_LANGUAGE, language); bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags); bundle.putInt(FIELD_ROLE_FLAGS, roleFlags); @@ -1413,7 +1472,14 @@ public static Format fromBundle(Bundle bundle) { BundleCollectionUtil.ensureClassLoader(bundle); builder .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 labelsBundles = bundle.getParcelableArrayList(FIELD_LABELS); + List labels = + labelsBundles == null + ? ImmutableList.of() + : BundleCollectionUtil.fromBundleList(Label::fromBundle, labelsBundles); + builder + .setLabels(labels) .setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language)) .setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags)) .setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags)) @@ -1492,4 +1558,13 @@ private static String keyForInitializationData(int initialisationDataIndex) { private static T defaultIfNull(@Nullable T value, @Nullable T defaultValue) { return value != null ? value : defaultValue; } + + private static String getDefaultLabel(List labels, @Nullable String language) { + for (Label l : labels) { + if (TextUtils.equals(l.language, language)) { + return l.value; + } + } + return labels.get(0).value; + } } diff --git a/libraries/common/src/main/java/androidx/media3/common/Label.java b/libraries/common/src/main/java/androidx/media3/common/Label.java new file mode 100644 index 00000000000..455ed70dc03 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/Label.java @@ -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))); + } +} diff --git a/libraries/common/src/test/java/androidx/media3/common/FormatTest.java b/libraries/common/src/test/java/androidx/media3/common/FormatTest.java index 42cfb2f53c2..1710b232ede 100644 --- a/libraries/common/src/test/java/androidx/media3/common/FormatTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/FormatTest.java @@ -20,10 +20,12 @@ import static androidx.media3.common.MimeTypes.VIDEO_WEBM; import static androidx.media3.test.utils.TestUtil.buildTestData; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.os.Bundle; import androidx.media3.test.utils.FakeMetadataEntry; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import org.junit.Test; @@ -57,6 +59,62 @@ public void roundTripViaBundle_excludeMetadata_hasMetadataExcluded() { 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() { byte[] initData1 = new byte[] {1, 2, 3}; byte[] initData2 = new byte[] {4, 5, 6}; @@ -86,6 +144,7 @@ private static Format createTestFormat() { return new Format.Builder() .setId("id") .setLabel("label") + .setLabels(ImmutableList.of(new Label("en", "label"))) .setLanguage("language") .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) .setRoleFlags(C.ROLE_FLAG_MAIN) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java index bc895f68463..5f10a0bfbd0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java @@ -451,6 +451,7 @@ private boolean drainOutputBuffer() .setMetadata(inputFormat.metadata) .setId(inputFormat.id) .setLabel(inputFormat.label) + .setLabels(inputFormat.labels) .setLanguage(inputFormat.language) .setSelectionFlags(inputFormat.selectionFlags) .setRoleFlags(inputFormat.roleFlags) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index ddf7164cd10..c4468563d86 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -552,6 +552,7 @@ protected void onOutputFormatChanged(Format format, @Nullable MediaFormat mediaF .setMetadata(format.metadata) .setId(format.id) .setLabel(format.label) + .setLabels(format.labels) .setLanguage(format.language) .setSelectionFlags(format.selectionFlags) .setRoleFlags(format.roleFlags) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java index 8d2116897ce..ab15bba669f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/mediaparser/OutputConsumerAdapterV30.java @@ -518,6 +518,7 @@ private Format toExoPlayerFormat(TrackData trackData) { .setRoleFlags(muxedCaptionFormat.roleFlags) .setSelectionFlags(muxedCaptionFormat.selectionFlags) .setLabel(muxedCaptionFormat.label) + .setLabels(muxedCaptionFormat.labels) .setMetadata(muxedCaptionFormat.metadata); break; } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java index 6ce3993665a..4fa9eea43c7 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java @@ -29,6 +29,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; +import androidx.media3.common.Label; import androidx.media3.common.MimeTypes; import androidx.media3.common.ParserException; import androidx.media3.common.util.Assertions; @@ -405,6 +406,7 @@ protected AdaptationSet parseAdaptationSet( int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); String label = xpp.getAttributeValue(null, "label"); + List labels = new ArrayList<>(); String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); @@ -504,7 +506,7 @@ protected AdaptationSet parseAdaptationSet( } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { - label = parseLabel(xpp); + labels.add(parseLabel(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } @@ -517,6 +519,7 @@ protected AdaptationSet parseAdaptationSet( buildRepresentation( representationInfos.get(i), label, + labels, drmSchemeType, drmSchemeDatas, inbandEventStreams)); @@ -856,12 +859,15 @@ protected Format buildFormat( protected Representation buildRepresentation( RepresentationInfo representationInfo, @Nullable String label, + List labels, @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format.Builder formatBuilder = representationInfo.format.buildUpon(); - if (label != null) { + if (label != null && labels.isEmpty()) { formatBuilder.setLabel(label); + } else { + formatBuilder.setLabels(labels); } @Nullable String drmSchemeType = representationInfo.drmSchemeType; if (drmSchemeType == null) { @@ -1405,8 +1411,10 @@ protected ProgramInformation parseProgramInformation(XmlPullParser xpp) * @throws IOException If an error occurs reading the element. * @return The parsed label. */ - protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { - return parseText(xpp, "Label"); + protected Label parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException { + String lang = xpp.getAttributeValue(null, "lang"); + String value = parseText(xpp, "Label"); + return new Label(lang, value); } /** diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java index 29510717d7c..2e6fa372b2e 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java @@ -22,6 +22,7 @@ import androidx.media3.common.C; import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; +import androidx.media3.common.Label; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.dash.manifest.Representation.MultiSegmentRepresentation; @@ -280,6 +281,12 @@ public void parseMediaPresentationDescription_labels() throws IOException { 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(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 @@ -431,13 +438,26 @@ public void parseSegmentTimeline_timeOffsetsAndUndefinedRepeatCount() throws Exc @Test public void parseLabel() throws Exception { + DashManifestParser parser = new DashManifestParser(); + XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput(new StringReader("test 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(); XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); xpp.setInput(new StringReader("test label" + NEXT_TAG)); xpp.next(); - String label = parser.parseLabel(xpp); - assertThat(label).isEqualTo("test label"); + Label label = parser.parseLabel(xpp); + assertThat(label.language).isNull(); assertNextTag(xpp); } @@ -448,8 +468,9 @@ public void parseLabel_noText() throws Exception { xpp.setInput(new StringReader("" + NEXT_TAG)); xpp.next(); - String label = parser.parseLabel(xpp); - assertThat(label).isEqualTo(""); + Label label = parser.parseLabel(xpp); + assertThat(label.value).isNotNull(); + assertThat(label.value).isEmpty(); assertNextTag(xpp); } diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java index d43cea69d58..cfb7e825259 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java @@ -21,6 +21,7 @@ import androidx.media3.common.C; import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; +import androidx.media3.common.Label; import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; @@ -852,6 +853,7 @@ private static Format deriveVideoFormat(Format variantFormat) { return new Format.Builder() .setId(variantFormat.id) .setLabel(variantFormat.label) + .setLabels(variantFormat.labels) .setContainerMimeType(variantFormat.containerMimeType) .setSampleMimeType(sampleMimeType) .setCodecs(codecs) @@ -875,6 +877,7 @@ private static Format deriveAudioFormat( int roleFlags = 0; @Nullable String language = null; @Nullable String label = null; + List labels = ImmutableList.of(); if (mediaTagFormat != null) { codecs = mediaTagFormat.codecs; metadata = mediaTagFormat.metadata; @@ -883,6 +886,7 @@ private static Format deriveAudioFormat( roleFlags = mediaTagFormat.roleFlags; language = mediaTagFormat.language; label = mediaTagFormat.label; + labels = mediaTagFormat.labels; } else { codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); metadata = variantFormat.metadata; @@ -892,6 +896,7 @@ private static Format deriveAudioFormat( roleFlags = variantFormat.roleFlags; language = variantFormat.language; label = variantFormat.label; + labels = variantFormat.labels; } } @Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs); @@ -900,6 +905,7 @@ private static Format deriveAudioFormat( return new Format.Builder() .setId(variantFormat.id) .setLabel(label) + .setLabels(labels) .setContainerMimeType(variantFormat.containerMimeType) .setSampleMimeType(sampleMimeType) .setCodecs(codecs) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java index ede85752016..aebff9592de 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java @@ -1584,6 +1584,7 @@ private static Format deriveFormat( .buildUpon() .setId(playlistFormat.id) .setLabel(playlistFormat.label) + .setLabels(playlistFormat.labels) .setLanguage(playlistFormat.language) .setSelectionFlags(playlistFormat.selectionFlags) .setRoleFlags(playlistFormat.roleFlags) diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java index 61e921293b2..9f45a58d56e 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/manifest/SsManifestParserTest.java @@ -52,5 +52,6 @@ public void parse_populatesFormatLabelWithStreamIndexName() throws Exception { TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_ISMC_1)); assertThat(ssManifest.streamElements[0].formats[0].label).isEqualTo("video"); + assertThat(ssManifest.streamElements[0].formats[0].labels.get(0).value).isEqualTo("video"); } } diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.0.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.1.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.2.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.3.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump index 3c09609d183..c4adbf92c2e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_null_terminated_srt.mkv.unknown_length.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.0.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.1.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.2.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.3.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump index e2c047c404c..5bbcc39d9db 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv/sample_with_srt.mkv.unknown_length.dump @@ -275,6 +275,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.0.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.1.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.2.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.3.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump index 5470deb7aa2..c70250ab63e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_null_terminated_srt.mkv.unknown_length.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.0.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.1.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.2.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.3.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump index 69069591f07..c300df088a2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mkv_subtitle_transcoding/sample_with_srt.mkv.unknown_length.dump @@ -276,6 +276,9 @@ track 3: selectionFlags = [default] language = en label = Subs Label + labels: + lang = en + value = Subs Label sample 0: time = 0 flags = 1 diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java index 13f4453d4da..59cd6af7264 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableFormat.java @@ -101,6 +101,17 @@ public void dump(Dumper dumper) { format -> Util.getRoleFlagStrings(format.roleFlags)); addIfNonDefault(dumper, "language", format, DEFAULT_FORMAT, format -> format.language); 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) { dumper.add("drmInitData", format.drmInitData.hashCode()); }