From 6fb1e94370d260ff06b461d3891c2879b48b908d Mon Sep 17 00:00:00 2001 From: AndyWangLYN Date: Sat, 23 May 2026 23:34:59 -0700 Subject: [PATCH 1/2] Common: add Format.selectionPriority and parse HLS SCORE --- RELEASENOTES.md | 7 +++ .../java/androidx/media3/common/Format.java | 38 ++++++++++++++ .../androidx/media3/common/FormatTest.java | 11 ++++ .../HlsMultivariantPlaylistParserTest.java | 50 +++++++++++++++++++ .../hls/playlist/HlsPlaylistParser.java | 8 +++ 5 files changed, 114 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 645a46b9788..8e33487aa67 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,10 @@ ### Unreleased changes * Common library: + * Add `Format.selectionPriority`, a relative preference among formats in + the same `TrackGroup`. Higher values are more preferred. The default is + `Format.NO_VALUE`, and apps can rank formats by this signal via + `AdaptiveTrackSelection.Factory#setTrackFormatComparator`. * Add `Format.channelMask` to explicitly represent the audio channel mask, and a new `Util.getAudioTrackChannelConfig(Format)` overload to safely resolve it. @@ -59,6 +63,9 @@ `AdaptiveTrackSelection.Factory.setTrackFormatComparator` to allow custom format ordering and ABR selection priority beyond bitrate-only ordering. +* HLS: + * Parse the `SCORE` attribute on `EXT-X-STREAM-INF` and + `EXT-X-I-FRAME-STREAM-INF` into `Format.selectionPriority`. * Extractors: * MP4: Add support for extracting ITU-T T.35 (`it35`) timed metadata tracks. 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 dcfd34f1b86..13eda1ff8f6 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Format.java +++ b/libraries/common/src/main/java/androidx/media3/common/Format.java @@ -62,6 +62,7 @@ *
  • {@link #language} *
  • {@link #selectionFlags} *
  • {@link #roleFlags} + *
  • {@link #selectionPriority} *
  • {@link #averageBitrate} *
  • {@link #peakBitrate} *
  • {@link #codecs} @@ -155,6 +156,7 @@ public static final class Builder { @Nullable private String language; private @C.SelectionFlags int selectionFlags; private @C.RoleFlags int roleFlags; + private float selectionPriority; private @C.AuxiliaryTrackType int auxiliaryTrackType; private int averageBitrate; private int peakBitrate; @@ -219,6 +221,7 @@ public static final class Builder { /** Creates a new instance with default values. */ public Builder() { labels = ImmutableList.of(); + selectionPriority = NO_VALUE; averageBitrate = NO_VALUE; peakBitrate = NO_VALUE; // Sample specific. @@ -263,6 +266,7 @@ private Builder(Format format) { this.language = format.language; this.selectionFlags = format.selectionFlags; this.roleFlags = format.roleFlags; + this.selectionPriority = format.selectionPriority; this.averageBitrate = format.averageBitrate; this.peakBitrate = format.peakBitrate; this.codecs = format.codecs; @@ -404,6 +408,18 @@ public Builder setRoleFlags(@C.RoleFlags int roleFlags) { return this; } + /** + * Sets {@link Format#selectionPriority}. The default value is {@link Format#NO_VALUE}. + * + * @param selectionPriority The {@link Format#selectionPriority}. + * @return The builder. + */ + @CanIgnoreReturnValue + public Builder setSelectionPriority(float selectionPriority) { + this.selectionPriority = selectionPriority; + return this; + } + /** * Sets {@link Format#auxiliaryTrackType}. The default value is {@link * C#AUXILIARY_TRACK_TYPE_UNDEFINED}. @@ -988,6 +1004,16 @@ public Format build() { /** Track role flags. */ public final @C.RoleFlags int roleFlags; + /** + * A relative preference among formats in the same {@link TrackGroup}. A higher value indicates a + * more preferred format, or {@link #NO_VALUE} if unset. + * + *

    Populated from the HLS {@code SCORE} attribute on {@code EXT-X-STREAM-INF} and {@code + * EXT-X-I-FRAME-STREAM-INF}, and from the DASH {@code @selectionPriority} attribute on {@code + * Representation} and {@code AdaptationSet}. + */ + @UnstableApi public final float selectionPriority; + /** The auxiliary track type. */ @UnstableApi public final @C.AuxiliaryTrackType int auxiliaryTrackType; @@ -1291,6 +1317,7 @@ private Format(Builder builder) { "Auxiliary track type must only be set to a value other than AUXILIARY_TRACK_TYPE_UNDEFINED" + " only when ROLE_FLAG_AUXILIARY is set"); roleFlags = builder.roleFlags; + selectionPriority = builder.selectionPriority; auxiliaryTrackType = builder.auxiliaryTrackType; averageBitrate = builder.averageBitrate; peakBitrate = builder.peakBitrate; @@ -1372,6 +1399,7 @@ public Format withManifestFormatInfo(Format manifestFormat) { // Use manifest value only. @Nullable String id = manifestFormat.id; + float selectionPriority = manifestFormat.selectionPriority; int tileCountHorizontal = manifestFormat.tileCountHorizontal; int tileCountVertical = manifestFormat.tileCountVertical; @@ -1428,6 +1456,7 @@ public Format withManifestFormatInfo(Format manifestFormat) { .setLanguage(language) .setSelectionFlags(selectionFlags) .setRoleFlags(roleFlags) + .setSelectionPriority(selectionPriority) .setAverageBitrate(averageBitrate) .setPeakBitrate(peakBitrate) .setCodecs(codecs) @@ -1500,6 +1529,7 @@ public int hashCode() { result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + selectionFlags; result = 31 * result + roleFlags; + result = 31 * result + Float.floatToIntBits(selectionPriority); result = 31 * result + auxiliaryTrackType; result = 31 * result + averageBitrate; result = 31 * result + peakBitrate; @@ -1588,6 +1618,7 @@ public boolean equals(@Nullable Object obj) { && tileCountVertical == other.tileCountVertical && cryptoType == other.cryptoType && Float.compare(frameRate, other.frameRate) == 0 + && Float.compare(selectionPriority, other.selectionPriority) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 && Objects.equals(id, other.id) && Objects.equals(label, other.label) @@ -1646,6 +1677,9 @@ public static String toLogString(@Nullable Format format) { if (format.bitrate != NO_VALUE) { builder.append(", bitrate=").append(format.bitrate); } + if (format.selectionPriority != NO_VALUE) { + builder.append(", selectionPriority=").append(format.selectionPriority); + } if (format.codecs != null) { builder.append(", codecs=").append(format.codecs); } @@ -1783,6 +1817,7 @@ public static String toLogString(@Nullable Format format) { private static final String FIELD_CHANNEL_MASK = Util.intToStringMaxRadix(38); private static final String FIELD_MIRROR_HORIZONTAL = Util.intToStringMaxRadix(39); private static final String FIELD_PIXEL_FORMAT = Util.intToStringMaxRadix(40); + private static final String FIELD_SELECTION_PRIORITY = Util.intToStringMaxRadix(41); /** Returns a {@link Bundle} representing the information stored in this object. */ @UnstableApi @@ -1795,6 +1830,7 @@ public Bundle toBundle() { bundle.putString(FIELD_LANGUAGE, language); bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags); bundle.putInt(FIELD_ROLE_FLAGS, roleFlags); + bundle.putFloat(FIELD_SELECTION_PRIORITY, selectionPriority); if (auxiliaryTrackType != DEFAULT.auxiliaryTrackType) { bundle.putInt(FIELD_AUXILIARY_TRACK_TYPE, auxiliaryTrackType); } @@ -1868,6 +1904,8 @@ public static Format fromBundle(Bundle bundle) { .setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language)) .setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags)) .setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags)) + .setSelectionPriority( + bundle.getFloat(FIELD_SELECTION_PRIORITY, DEFAULT.selectionPriority)) .setAuxiliaryTrackType( bundle.getInt(FIELD_AUXILIARY_TRACK_TYPE, DEFAULT.auxiliaryTrackType)) .setAverageBitrate(bundle.getInt(FIELD_AVERAGE_BITRATE, DEFAULT.averageBitrate)) 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 63efeaca3cb..e5a76e95532 100644 --- a/libraries/common/src/test/java/androidx/media3/common/FormatTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/FormatTest.java @@ -118,6 +118,16 @@ public void formatBuild_withChannelCountAndChannelMaskSetButNoMatch_throwsExcept assertThrows(IllegalStateException.class, () -> builder.build()); } + @Test + public void withManifestFormatInfo_preservesManifestSelectionPriority() { + Format manifestFormat = new Format.Builder().setSelectionPriority(3f).build(); + Format sampleFormat = new Format.Builder().setSelectionPriority(Format.NO_VALUE).build(); + + Format format = sampleFormat.withManifestFormatInfo(manifestFormat); + + assertThat(format.selectionPriority).isEqualTo(3f); + } + private static Format createTestFormat() { byte[] initData1 = new byte[] {1, 2, 3}; byte[] initData2 = new byte[] {4, 5, 6}; @@ -151,6 +161,7 @@ private static Format createTestFormat() { .setLanguage("language") .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) .setRoleFlags(C.ROLE_FLAG_MAIN) + .setSelectionPriority(2f) .setAverageBitrate(1024) .setPeakBitrate(2048) .setCodecs("codec") diff --git a/libraries/exoplayer_hls/src/androidTest/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylistParserTest.java b/libraries/exoplayer_hls/src/androidTest/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylistParserTest.java index 9a654e2f6e0..5758a221a61 100644 --- a/libraries/exoplayer_hls/src/androidTest/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/androidTest/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylistParserTest.java @@ -76,6 +76,15 @@ public class HlsMultivariantPlaylistParserTest { + "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "http://example.com/spaces_in_codecs.m3u8\n"; + private static final String PLAYLIST_WITH_SCORE = + " #EXTM3U \n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"avc1.66.30\"," + + "RESOLUTION=304x128,SCORE=1.5\n" + + "http://example.com/low.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8940000,CODECS=\"avc1.66.30\"," + + "RESOLUTION=1920x1080,SCORE=2.0\n" + + "http://example.com/high.m3u8\n"; + private static final String PLAYLIST_WITH_DOLBY_VISION = " #EXTM3U \n" + "\n" @@ -381,6 +390,15 @@ public class HlsMultivariantPlaylistParserTest { + "8940000/index.m3u8\n" + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,RESOLUTION=1920x1080,CODECS=\"avc1.640028\",URI=\"iframe_1313400/index.m3u8\"\n"; + private static final String PLAYLIST_WITH_IFRAME_VARIANT_SCORE = + "#EXTM3U\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8940000,RESOLUTION=1920x1080," + + "CODECS=\"avc1.640028\"\n" + + "8940000/index.m3u8\n" + + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,SCORE=4.5," + + "RESOLUTION=1920x1080,CODECS=\"avc1.640028\"," + + "URI=\"iframe_1313400/index.m3u8\"\n"; + @Test public void parseMultivariantPlaylist_withSimple_success() throws IOException { HlsMultivariantPlaylist multivariantPlaylist = @@ -435,6 +453,17 @@ public void parseMultivariantPlaylist_withSimple_success() throws IOException { assertThat(multivariantPlaylist.contentSteeringInfo).isNull(); } + @Test + public void parseMultivariantPlaylist_withoutScore_setsSelectionPriorityToNoValue() + throws IOException { + HlsMultivariantPlaylist multivariantPlaylist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); + + for (HlsMultivariantPlaylist.Variant variant : multivariantPlaylist.variants) { + assertThat(variant.format.selectionPriority).isEqualTo((float) Format.NO_VALUE); + } + } + @Test public void parseMultivariantPlaylist_withAverageBandwidth_success() throws IOException { HlsMultivariantPlaylist multivariantPlaylist = @@ -446,6 +475,16 @@ public void parseMultivariantPlaylist_withAverageBandwidth_success() throws IOEx assertThat(variants.get(1).format.bitrate).isEqualTo(1280000); } + @Test + public void parseMultivariantPlaylist_withScore_success() throws IOException { + HlsMultivariantPlaylist multivariantPlaylist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SCORE); + + List variants = multivariantPlaylist.variants; + assertThat(variants.get(0).format.selectionPriority).isEqualTo(1.5f); + assertThat(variants.get(1).format.selectionPriority).isEqualTo(2.0f); + } + @Test public void parseMultivariantPlaylist_withDolbyVisionProfile10_success() throws IOException { HlsMultivariantPlaylist multivariantPlaylist = @@ -892,6 +931,17 @@ public void testIFrameVariant() throws IOException { .isEqualTo(C.ROLE_FLAG_TRICK_PLAY); } + @Test + public void parseMultivariantPlaylist_withIFrameStreamInfScore_success() throws IOException { + HlsMultivariantPlaylist playlist = + parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANT_SCORE); + + assertThat(playlist.variants).hasSize(2); + assertThat(playlist.variants.get(0).format.selectionPriority) + .isEqualTo((float) Format.NO_VALUE); + assertThat(playlist.variants.get(1).format.selectionPriority).isEqualTo(4.5f); + } + private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) { return new Metadata( new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos))); diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java index ff9247a8d07..4ab20c1572b 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java @@ -164,6 +164,7 @@ public static final class DeltaUpdateException extends IOException {} Pattern.compile("SUPPLEMENTAL-CODECS=" + ATTR_QUOTED_STRING_VALUE_PATTERN); private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)"); private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b"); + private static final Pattern REGEX_SCORE = Pattern.compile("SCORE=([\\d\\.]+)\\b"); private static final Pattern REGEX_SERVER_URI = Pattern.compile("SERVER-URI=" + ATTR_QUOTED_STRING_VALUE_PATTERN); private static final Pattern REGEX_PATHWAY_ID = @@ -571,6 +572,12 @@ private static HlsMultivariantPlaylist parseMultivariantPlaylist( if (frameRateString != null) { frameRate = Float.parseFloat(frameRateString); } + float selectionPriority = Format.NO_VALUE; + String selectionPriorityString = + parseOptionalStringAttr(line, REGEX_SCORE, variableDefinitions, matcherCache); + if (selectionPriorityString != null) { + selectionPriority = Float.parseFloat(selectionPriorityString); + } @Nullable String pathwayId = parseOptionalStringAttr(line, REGEX_PATHWAY_ID, variableDefinitions, matcherCache); @@ -610,6 +617,7 @@ private static HlsMultivariantPlaylist parseMultivariantPlaylist( .setWidth(width) .setHeight(height) .setFrameRate(frameRate) + .setSelectionPriority(selectionPriority) .setRoleFlags(roleFlags) .setColorInfo(colorInfo) .build(); From 8946a7cfe355b683ead239e69fce9b5b4fae67d2 Mon Sep 17 00:00:00 2001 From: Heyu Wang <90009289+AndyWangLYN@users.noreply.github.com> Date: Sun, 24 May 2026 06:59:36 +0000 Subject: [PATCH 2/2] revert releasenote.md changes --- RELEASENOTES.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8e33487aa67..645a46b9788 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,10 +3,6 @@ ### Unreleased changes * Common library: - * Add `Format.selectionPriority`, a relative preference among formats in - the same `TrackGroup`. Higher values are more preferred. The default is - `Format.NO_VALUE`, and apps can rank formats by this signal via - `AdaptiveTrackSelection.Factory#setTrackFormatComparator`. * Add `Format.channelMask` to explicitly represent the audio channel mask, and a new `Util.getAudioTrackChannelConfig(Format)` overload to safely resolve it. @@ -63,9 +59,6 @@ `AdaptiveTrackSelection.Factory.setTrackFormatComparator` to allow custom format ordering and ABR selection priority beyond bitrate-only ordering. -* HLS: - * Parse the `SCORE` attribute on `EXT-X-STREAM-INF` and - `EXT-X-I-FRAME-STREAM-INF` into `Format.selectionPriority`. * Extractors: * MP4: Add support for extracting ITU-T T.35 (`it35`) timed metadata tracks.