From fab6c4c83c01720490d33a9720185dce5030ea2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Varga?= Date: Thu, 15 May 2025 16:15:03 +0200 Subject: [PATCH 01/13] #2426 Support updating HLS Interstitials according to the HLS spec - if an interstitial already appeared, then attempt to update it --- .../hls/playlist/HlsPlaylistParser.java | 140 +++++++++++++++--- .../playlist/HlsMediaPlaylistParserTest.java | 36 +++++ 2 files changed, 154 insertions(+), 22 deletions(-) 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 d0aa3bdca3a..2869169d717 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 @@ -62,6 +62,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -758,7 +759,7 @@ private static HlsMediaPlaylist parseMediaPlaylist( @Nullable Part preloadPart = null; List renditionReports = new ArrayList<>(); List tags = new ArrayList<>(); - List interstitials = new ArrayList<>(); + LinkedHashMap interstitialMap = new LinkedHashMap<>(); long segmentDurationUs = 0; String segmentTitle = ""; @@ -1079,8 +1080,13 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe if (assetListUriString != null) { assetListUri = Uri.parse(assetListUriString); } - long startDateUnixUs = - msToUs(parseXsDateTime(parseStringAttr(line, REGEX_START_DATE, variableDefinitions))); + long startDateUnixUs = C.TIME_UNSET; + @Nullable + String startDateUnixMsString = + parseOptionalStringAttr(line, REGEX_START_DATE, variableDefinitions); + if (startDateUnixMsString != null) { + startDateUnixUs = msToUs(parseXsDateTime(startDateUnixMsString)); + } long endDateUnixUs = C.TIME_UNSET; @Nullable String endDateUnixMsString = @@ -1185,24 +1191,50 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe break; } } - if ((assetListUri == null && assetUri != null) - || (assetListUri != null && assetUri == null)) { - interstitials.add( - new Interstitial( - id, - assetUri, - assetListUri, - startDateUnixUs, - endDateUnixUs, - durationUs, - plannedDurationUs, - cue, - endOnNext, - resumeOffsetUs, - playoutLimitUs, - snapTypes, - restrictions, - clientDefinedAttributes.build())); + + if (interstitialMap.containsKey(id)) { + Interstitial interstitial = interstitialMap.get(id); + interstitialMap.put(id, getUpdatedInterstitial( + interstitial, + assetUri, + assetListUri, + startDateUnixUs, + endDateUnixUs, + durationUs, + plannedDurationUs, + cue, + endOnNext, + resumeOffsetUs, + playoutLimitUs, + snapTypes, + restrictions, + clientDefinedAttributes.build() + )); + } else { + if ((assetListUri == null && assetUri != null) + || (assetListUri != null && assetUri == null)) { + if (startDateUnixUs == C.TIME_UNSET) { + throw ParserException.createForMalformedManifest( + "Couldn't match " + REGEX_START_DATE.pattern() + " in " + line, /* cause= */ + null); + } + Interstitial interstitial = new Interstitial( + id, + assetUri, + assetListUri, + startDateUnixUs, + endDateUnixUs, + durationUs, + plannedDurationUs, + cue, + endOnNext, + resumeOffsetUs, + playoutLimitUs, + snapTypes, + restrictions, + clientDefinedAttributes.build()); + interstitialMap.put(id, interstitial); + } } } else if (!line.startsWith("#")) { @Nullable @@ -1310,7 +1342,71 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe trailingParts, serverControl, renditionReportMap, - interstitials); + new ArrayList<>(interstitialMap.values())); + } + + private static Interstitial getUpdatedInterstitial( + Interstitial oldInterstitial, + Uri assetUri, + Uri assetListUri, + long startDateUnixUs, + long endDateUnixUs, + long durationUs, + long plannedDurationUs, + List cue, + boolean endOnNext, + long resumeOffsetUs, + long playoutLimitUs, + List snapTypes, + List restrictions, + ImmutableList clientDefinedAttributes) { + + // If a Playlist contains two EXT-X-DATERANGE tags with the same ID + // attribute value, then any AttributeName that appears in both tags + // MUST have the same AttributeValue. A Server MAY augment a Date Range + // with additional attributes by adding subsequent EXT-X-DATERANGE tags + // with the same ID attribute to a Playlist. The client is responsible + // for consolidating the tags. The subsequent EXT-X-DATERANGE tags can + // appear in a subsequent playlist update, in the case of live or event + // streams. + Uri newAssetUri = (assetUri != null) ? assetUri : oldInterstitial.assetUri; + Uri newAssetListUri = (assetListUri != null) ? assetListUri : oldInterstitial.assetListUri; + long newStartDateUnixUs = + (startDateUnixUs != C.TIME_UNSET) ? startDateUnixUs : oldInterstitial.startDateUnixUs; + long newEndDateUnixUs = + (endDateUnixUs != C.TIME_UNSET) ? endDateUnixUs : oldInterstitial.endDateUnixUs; + long newDurationUs = (durationUs != C.TIME_UNSET) ? durationUs : oldInterstitial.durationUs; + long newPlannedDurationUs = + (plannedDurationUs != C.TIME_UNSET) ? plannedDurationUs : oldInterstitial.plannedDurationUs; + List newCue = (cue != null && !cue.isEmpty()) ? cue : oldInterstitial.cue; + boolean newEndOnNext = oldInterstitial.endOnNext || endOnNext; + long newResumeOffsetUs = + (resumeOffsetUs != C.TIME_UNSET) ? resumeOffsetUs : oldInterstitial.resumeOffsetUs; + long newPlayoutLimitUs = + (playoutLimitUs != C.TIME_UNSET) ? playoutLimitUs : oldInterstitial.playoutLimitUs; + List newSnapTypes = + (snapTypes != null && !snapTypes.isEmpty()) ? snapTypes : oldInterstitial.snapTypes; + List newRestrictions = (restrictions != null && !restrictions.isEmpty()) ? restrictions + : oldInterstitial.restrictions; + ImmutableList newClientDefinedAttributes = + (clientDefinedAttributes != null && !clientDefinedAttributes.isEmpty()) + ? clientDefinedAttributes : oldInterstitial.clientDefinedAttributes; + + return new Interstitial( + oldInterstitial.id, + newAssetUri, + newAssetListUri, + newStartDateUnixUs, + newEndDateUnixUs, + newDurationUs, + newPlannedDurationUs, + newCue, + newEndOnNext, + newResumeOffsetUs, + newPlayoutLimitUs, + newSnapTypes, + newRestrictions, + newClientDefinedAttributes); } private static DrmInitData getPlaylistProtectionSchemes( diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 9cc3807e079..5ea1bdeb817 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1609,6 +1609,42 @@ public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_interstitialI assertThat(playlist.interstitials.get(0).snapTypes).containsExactly(SNAP_TYPE_OUT); } + @Test + public void + parseMediaPlaylist_withInterstitialWithUpdatingDateRange() + throws IOException { + Uri playlistUri = Uri.parse("https://raw.githubusercontent.com/TomVarga/HLS-test-stream/refs/heads/main/ride/audio.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PROGRAM-DATE-TIME:2024-09-20T15:29:20.000Z\n" + + "#EXTINF:10.007800,\n" + + "audio0000.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," + + "X-SNAP=\"OUT,IN\",X-TIMELINE-OCCUPIES=\"RANGE\"," + + "X-TIMELINE-STYLE=\"HIGHLIGHT\",X-CONTENT-MAY-VARY=\"YES\"\n" + + "#EXTINF:10.007800,\n" + + "audio0001.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," + + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," + + "X-RESUME-OFFSET=24.953741497"; + + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) + new HlsPlaylistParser() + .parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))); + + assertThat(playlist.interstitials.get(0).id).isEqualTo("15943"); + assertThat(playlist.interstitials.get(0).resumeOffsetUs).isEqualTo(24953741L); + assertThat(playlist.interstitials.get(0).endDateUnixUs).isEqualTo(1726846189006000L); + assertThat(playlist.interstitials.get(0).playoutLimitUs).isEqualTo(24953741L); + } + @Test public void multipleExtXKeysForSingleSegment() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); From dbdb885b02c65de3c3304acde5fed47616268db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Varga?= Date: Mon, 19 May 2025 16:50:46 +0200 Subject: [PATCH 02/13] Support updating HLS Interstitials according to the HLS spec - use a builder - don't throw parse exception for missing START-DATE instead ignore the interstitial --- .../hls/playlist/HlsMediaPlaylist.java | 200 ++++++++++++++++++ .../hls/playlist/HlsPlaylistParser.java | 136 +++--------- .../playlist/HlsMediaPlaylistParserTest.java | 14 +- 3 files changed, 234 insertions(+), 116 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 0c7a6b27a5d..ab5218e22e5 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -29,6 +29,7 @@ import androidx.media3.common.C; import androidx.media3.common.DrmInitData; import androidx.media3.common.StreamKey; +import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -611,6 +612,205 @@ public int hashCode() { restrictions, clientDefinedAttributes); } + + // If a Playlist contains two EXT-X-DATERANGE tags with the same ID + // attribute value, then any AttributeName that appears in both tags + // MUST have the same AttributeValue. A Server MAY augment a Date Range + // with additional attributes by adding subsequent EXT-X-DATERANGE tags + // with the same ID attribute to a Playlist. The client is responsible + // for consolidating the tags. The subsequent EXT-X-DATERANGE tags can + // appear in a subsequent playlist update, in the case of live or event + // streams. + /** Builder for {@link Interstitial}. */ + public static final class Builder { + + private final String id; + @Nullable private Uri assetUri; + @Nullable private Uri assetListUri; + private long startDateUnixUs = C.TIME_UNSET; + private long endDateUnixUs = C.TIME_UNSET; + private long durationUs = C.TIME_UNSET; + private long plannedDurationUs = C.TIME_UNSET; + private List<@Interstitial.CueTriggerType String> cue = new ArrayList<>(); + private boolean endOnNext; + private long resumeOffsetUs = C.TIME_UNSET; + private long playoutLimitUs = C.TIME_UNSET; + private List<@Interstitial.SnapType String> snapTypes = new ArrayList<>(); + private List<@Interstitial.NavigationRestriction String> restrictions = new ArrayList<>(); + private List clientDefinedAttributes = new ArrayList<>(); + + /** + * Creates the builder. + * + * @param id The id. + */ + public Builder(String id) { + this.id = id; + } + + /** Sets the {@code assetUri}. */ + public Builder setAssetUri(@Nullable Uri assetUri) { + if (assetUri == null) return this; + if (this.assetUri != null) { + Assertions.checkArgument(!this.assetUri.equals(assetUri), + "Can't change assetUri from " + this.assetUri + " to " + assetUri); + } + this.assetUri = assetUri; + return this; + } + + /** Sets the {@code assetListUri}. */ + public Builder setAssetListUri(@Nullable Uri assetListUri) { + if (assetListUri == null) return this; + if (this.assetListUri != null) { + Assertions.checkArgument(!this.assetListUri.equals(assetListUri), + "Can't change assetListUri from " + this.assetListUri + " to " + assetListUri); + } + this.assetListUri = assetListUri; + return this; + } + + /** Sets the {@code startDateUnixUs}. */ + public Builder setStartDateUnixUs(long startDateUnixUs) { + if (startDateUnixUs == C.TIME_UNSET) return this; + if (this.startDateUnixUs != C.TIME_UNSET) { + Assertions.checkArgument(this.startDateUnixUs != startDateUnixUs, + "Can't change startDateUnixUs from " + this.startDateUnixUs + " to " + startDateUnixUs); + } + this.startDateUnixUs = startDateUnixUs; + return this; + } + + /** Sets the {@code endDateUnixUs}. */ + public Builder setEndDateUnixUs(long endDateUnixUs) { + if (endDateUnixUs == C.TIME_UNSET) return this; + if (this.endDateUnixUs != C.TIME_UNSET) { + Assertions.checkArgument(this.endDateUnixUs != endDateUnixUs, + "Can't change endDateUnixUs from " + this.endDateUnixUs + " to " + endDateUnixUs); + } + this.endDateUnixUs = endDateUnixUs; + return this; + } + + /** Sets the {@code durationUs}. */ + public Builder setDurationUs(long durationUs) { + if (durationUs == C.TIME_UNSET) return this; + if (this.durationUs != C.TIME_UNSET) { + Assertions.checkArgument(this.durationUs != durationUs, + "Can't change durationUs from " + this.durationUs + " to " + durationUs); + } + this.durationUs = durationUs; + return this; + } + + /** Sets the {@code plannedDurationUs}. */ + public Builder setPlannedDurationUs(long plannedDurationUs) { + if (plannedDurationUs == C.TIME_UNSET) return this; + if (this.plannedDurationUs != C.TIME_UNSET) { + Assertions.checkArgument(this.plannedDurationUs != plannedDurationUs, + "Can't change plannedDurationUs from " + this.plannedDurationUs + " to " + plannedDurationUs); + } + this.plannedDurationUs = plannedDurationUs; + return this; + } + + /** Sets the trigger {@code cue} types. */ + public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { + if (cue.isEmpty()) return this; + if (!this.cue.isEmpty()) { + Assertions.checkArgument(!this.cue.equals(cue), + "Can't change cue from " + String.join(", ", this.cue) + " to " + String.join(", ", cue)); + } + this.cue = cue; + return this; + } + + /** Sets whether the interstitial {@code endOnNext}. */ + public Builder setEndOnNext(boolean endOnNext) { + if (!endOnNext) return this; + this.endOnNext = endOnNext; + return this; + } + + /** Sets the {@code resumeOffsetUs}. */ + public Builder setResumeOffsetUs(long resumeOffsetUs) { + if (resumeOffsetUs == C.TIME_UNSET) return this; + if (this.resumeOffsetUs != C.TIME_UNSET) { + Assertions.checkArgument(this.resumeOffsetUs != resumeOffsetUs, + "Can't change resumeOffsetUs from " + this.resumeOffsetUs + " to " + resumeOffsetUs); + } + this.resumeOffsetUs = resumeOffsetUs; + return this; + } + + /** Sets the {@code playoutLimitUs}. */ + public Builder setPlayoutLimitUs(long playoutLimitUs) { + if (playoutLimitUs == C.TIME_UNSET) return this; + if (this.playoutLimitUs != C.TIME_UNSET) { + Assertions.checkArgument(this.playoutLimitUs != playoutLimitUs, + "Can't change playoutLimitUs from " + this.playoutLimitUs + " to " + playoutLimitUs); + } + this.playoutLimitUs = playoutLimitUs; + return this; + } + + /** Sets the {@code snapTypes}. */ + public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { + if (snapTypes.isEmpty()) return this; + if (!this.snapTypes.isEmpty()) { + Assertions.checkArgument(!this.snapTypes.equals(snapTypes), + "Can't change snapTypes from " + String.join(", ", this.snapTypes) + " to " + String.join(", ", snapTypes)); + } + this.snapTypes = snapTypes; + return this; + } + + /** Sets the {@code NavigationRestriction}. */ + public Builder setRestrictions(List<@Interstitial.NavigationRestriction String> restrictions) { + if (restrictions.isEmpty()) return this; + if (!this.restrictions.isEmpty()) { + Assertions.checkArgument(!this.restrictions.equals(restrictions), + "Can't change restrictions from " + String.join(", ", this.restrictions) + " to " + String.join(", ", restrictions)); + } + this.restrictions = restrictions; + return this; + } + + /** Sets the {@code clientDefinedAttributes}. */ + public Builder setClientDefinedAttributes( + List clientDefinedAttributes) { + if (clientDefinedAttributes.isEmpty()) return this; + if (!this.clientDefinedAttributes.isEmpty()) { + Assertions.checkArgument(!this.clientDefinedAttributes.equals(clientDefinedAttributes)); + } + this.clientDefinedAttributes = clientDefinedAttributes; + return this; + } + + public @Nullable Interstitial build() { + if ((assetListUri == null && assetUri != null) + || (assetListUri != null && assetUri == null) + && startDateUnixUs != C.TIME_UNSET) { + return new Interstitial( + this.id, + this.assetUri, + this.assetListUri, + this.startDateUnixUs, + this.endDateUnixUs, + this.durationUs, + this.plannedDurationUs, + this.cue, + this.endOnNext, + this.resumeOffsetUs, + this.playoutLimitUs, + this.snapTypes, + this.restrictions, + this.clientDefinedAttributes + ); + } + return null; + } + } } /** A client defined attribute. See RFC 8216bis, section 4.4.5.1. */ 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 2869169d717..196f259b2fa 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 @@ -759,7 +759,7 @@ private static HlsMediaPlaylist parseMediaPlaylist( @Nullable Part preloadPart = null; List renditionReports = new ArrayList<>(); List tags = new ArrayList<>(); - LinkedHashMap interstitialMap = new LinkedHashMap<>(); + LinkedHashMap interstitialBuilderMap = new LinkedHashMap<>(); long segmentDurationUs = 0; String segmentTitle = ""; @@ -1192,50 +1192,22 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe } } - if (interstitialMap.containsKey(id)) { - Interstitial interstitial = interstitialMap.get(id); - interstitialMap.put(id, getUpdatedInterstitial( - interstitial, - assetUri, - assetListUri, - startDateUnixUs, - endDateUnixUs, - durationUs, - plannedDurationUs, - cue, - endOnNext, - resumeOffsetUs, - playoutLimitUs, - snapTypes, - restrictions, - clientDefinedAttributes.build() - )); - } else { - if ((assetListUri == null && assetUri != null) - || (assetListUri != null && assetUri == null)) { - if (startDateUnixUs == C.TIME_UNSET) { - throw ParserException.createForMalformedManifest( - "Couldn't match " + REGEX_START_DATE.pattern() + " in " + line, /* cause= */ - null); - } - Interstitial interstitial = new Interstitial( - id, - assetUri, - assetListUri, - startDateUnixUs, - endDateUnixUs, - durationUs, - plannedDurationUs, - cue, - endOnNext, - resumeOffsetUs, - playoutLimitUs, - snapTypes, - restrictions, - clientDefinedAttributes.build()); - interstitialMap.put(id, interstitial); - } - } + Interstitial.Builder interstitialBuilder = (interstitialBuilderMap.containsKey(id) ? + interstitialBuilderMap.get(id) : new Interstitial.Builder(id)) + .setAssetUri(assetUri) + .setAssetListUri(assetListUri) + .setStartDateUnixUs(startDateUnixUs) + .setEndDateUnixUs(endDateUnixUs) + .setDurationUs(durationUs) + .setPlannedDurationUs(plannedDurationUs) + .setCue(cue) + .setEndOnNext(endOnNext) + .setResumeOffsetUs(resumeOffsetUs) + .setPlayoutLimitUs(playoutLimitUs) + .setSnapTypes(snapTypes) + .setRestrictions(restrictions) + .setClientDefinedAttributes(clientDefinedAttributes.build()); + interstitialBuilderMap.put(id, interstitialBuilder); } else if (!line.startsWith("#")) { @Nullable String segmentEncryptionIV = @@ -1321,6 +1293,14 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe trailingParts.add(preloadPart); } + List interstitials = new ArrayList<>(); + for (Interstitial.Builder interstitialBuilder : interstitialBuilderMap.values()) { + Interstitial interstitial = interstitialBuilder.build(); + if (interstitial != null) { + interstitials.add(interstitial); + } + } + return new HlsMediaPlaylist( playlistType, baseUri, @@ -1342,71 +1322,7 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe trailingParts, serverControl, renditionReportMap, - new ArrayList<>(interstitialMap.values())); - } - - private static Interstitial getUpdatedInterstitial( - Interstitial oldInterstitial, - Uri assetUri, - Uri assetListUri, - long startDateUnixUs, - long endDateUnixUs, - long durationUs, - long plannedDurationUs, - List cue, - boolean endOnNext, - long resumeOffsetUs, - long playoutLimitUs, - List snapTypes, - List restrictions, - ImmutableList clientDefinedAttributes) { - - // If a Playlist contains two EXT-X-DATERANGE tags with the same ID - // attribute value, then any AttributeName that appears in both tags - // MUST have the same AttributeValue. A Server MAY augment a Date Range - // with additional attributes by adding subsequent EXT-X-DATERANGE tags - // with the same ID attribute to a Playlist. The client is responsible - // for consolidating the tags. The subsequent EXT-X-DATERANGE tags can - // appear in a subsequent playlist update, in the case of live or event - // streams. - Uri newAssetUri = (assetUri != null) ? assetUri : oldInterstitial.assetUri; - Uri newAssetListUri = (assetListUri != null) ? assetListUri : oldInterstitial.assetListUri; - long newStartDateUnixUs = - (startDateUnixUs != C.TIME_UNSET) ? startDateUnixUs : oldInterstitial.startDateUnixUs; - long newEndDateUnixUs = - (endDateUnixUs != C.TIME_UNSET) ? endDateUnixUs : oldInterstitial.endDateUnixUs; - long newDurationUs = (durationUs != C.TIME_UNSET) ? durationUs : oldInterstitial.durationUs; - long newPlannedDurationUs = - (plannedDurationUs != C.TIME_UNSET) ? plannedDurationUs : oldInterstitial.plannedDurationUs; - List newCue = (cue != null && !cue.isEmpty()) ? cue : oldInterstitial.cue; - boolean newEndOnNext = oldInterstitial.endOnNext || endOnNext; - long newResumeOffsetUs = - (resumeOffsetUs != C.TIME_UNSET) ? resumeOffsetUs : oldInterstitial.resumeOffsetUs; - long newPlayoutLimitUs = - (playoutLimitUs != C.TIME_UNSET) ? playoutLimitUs : oldInterstitial.playoutLimitUs; - List newSnapTypes = - (snapTypes != null && !snapTypes.isEmpty()) ? snapTypes : oldInterstitial.snapTypes; - List newRestrictions = (restrictions != null && !restrictions.isEmpty()) ? restrictions - : oldInterstitial.restrictions; - ImmutableList newClientDefinedAttributes = - (clientDefinedAttributes != null && !clientDefinedAttributes.isEmpty()) - ? clientDefinedAttributes : oldInterstitial.clientDefinedAttributes; - - return new Interstitial( - oldInterstitial.id, - newAssetUri, - newAssetListUri, - newStartDateUnixUs, - newEndDateUnixUs, - newDurationUs, - newPlannedDurationUs, - newCue, - newEndOnNext, - newResumeOffsetUs, - newPlayoutLimitUs, - newSnapTypes, - newRestrictions, - newClientDefinedAttributes); + interstitials); } private static DrmInitData getPlaylistProtectionSchemes( diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 5ea1bdeb817..76e1ab339a4 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1445,7 +1445,8 @@ public void parseMediaPlaylist_withDateRangeWithMissingClass_dateRangeIgnored() } @Test - public void parseMediaPlaylist_withInterstitialWithoutStartDate_throwsParserException() { + public void parseMediaPlaylist_withInterstitialWithoutStartDate_noInterstitial() + throws IOException { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -1461,11 +1462,12 @@ public void parseMediaPlaylist_withInterstitialWithoutStartDate_throwsParserExce + "X-ASSET-LIST=\"http://example.com/ad2-assets.json\"\n"; HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser(); - assertThrows( - ParserException.class, - () -> + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) hlsPlaylistParser.parse( - playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)))); + playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))); + + assertThat(playlist.interstitials).isEmpty(); } @Test @@ -1639,7 +1641,7 @@ public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_interstitialI new HlsPlaylistParser() .parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))); - assertThat(playlist.interstitials.get(0).id).isEqualTo("15943"); + assertThat(playlist.interstitials.size()).isEqualTo(1); assertThat(playlist.interstitials.get(0).resumeOffsetUs).isEqualTo(24953741L); assertThat(playlist.interstitials.get(0).endDateUnixUs).isEqualTo(1726846189006000L); assertThat(playlist.interstitials.get(0).playoutLimitUs).isEqualTo(24953741L); From 8586d502c9b255347868ca433f941b056dd1542e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Varga?= Date: Tue, 20 May 2025 13:20:00 +0200 Subject: [PATCH 03/13] #2426 Support updating HLS Interstitials according to the HLS spec - some code style cleanup - fix negated expression in checkArguments and add test for it --- .../hls/playlist/HlsMediaPlaylist.java | 122 +++++++++++------- .../playlist/HlsMediaPlaylistParserTest.java | 38 +++++- 2 files changed, 114 insertions(+), 46 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index ab5218e22e5..264c095ff32 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -613,31 +613,29 @@ public int hashCode() { clientDefinedAttributes); } - // If a Playlist contains two EXT-X-DATERANGE tags with the same ID - // attribute value, then any AttributeName that appears in both tags - // MUST have the same AttributeValue. A Server MAY augment a Date Range - // with additional attributes by adding subsequent EXT-X-DATERANGE tags - // with the same ID attribute to a Playlist. The client is responsible - // for consolidating the tags. The subsequent EXT-X-DATERANGE tags can - // appear in a subsequent playlist update, in the case of live or event - // streams. - /** Builder for {@link Interstitial}. */ + + /** + * Builder for {@link Interstitial}. + * + *

See RFC 8216bis, section 4.4.5.1 for how to consolidate multiple interstitials with + * the same {@linkplain HlsMediaPlaylist.Interstitial#id ID}. + */ public static final class Builder { private final String id; @Nullable private Uri assetUri; @Nullable private Uri assetListUri; - private long startDateUnixUs = C.TIME_UNSET; - private long endDateUnixUs = C.TIME_UNSET; - private long durationUs = C.TIME_UNSET; - private long plannedDurationUs = C.TIME_UNSET; - private List<@Interstitial.CueTriggerType String> cue = new ArrayList<>(); + private long startDateUnixUs; + private long endDateUnixUs; + private long durationUs; + private long plannedDurationUs; + private List<@Interstitial.CueTriggerType String> cue; private boolean endOnNext; - private long resumeOffsetUs = C.TIME_UNSET; - private long playoutLimitUs = C.TIME_UNSET; - private List<@Interstitial.SnapType String> snapTypes = new ArrayList<>(); - private List<@Interstitial.NavigationRestriction String> restrictions = new ArrayList<>(); - private List clientDefinedAttributes = new ArrayList<>(); + private long resumeOffsetUs; + private long playoutLimitUs; + private List<@Interstitial.SnapType String> snapTypes; + private List<@Interstitial.NavigationRestriction String> restrictions; + private List clientDefinedAttributes; /** * Creates the builder. @@ -646,13 +644,25 @@ public static final class Builder { */ public Builder(String id) { this.id = id; + this.startDateUnixUs = C.TIME_UNSET; + this.endDateUnixUs = C.TIME_UNSET; + this.durationUs = C.TIME_UNSET; + this.plannedDurationUs = C.TIME_UNSET; + this.cue = new ArrayList<>(); + this.resumeOffsetUs = C.TIME_UNSET; + this.playoutLimitUs = C.TIME_UNSET; + this.snapTypes = new ArrayList<>(); + this.restrictions = new ArrayList<>(); + this.clientDefinedAttributes = new ArrayList<>(); } /** Sets the {@code assetUri}. */ public Builder setAssetUri(@Nullable Uri assetUri) { - if (assetUri == null) return this; + if (assetUri == null) { + return this; + } if (this.assetUri != null) { - Assertions.checkArgument(!this.assetUri.equals(assetUri), + checkArgument(this.assetUri.equals(assetUri), "Can't change assetUri from " + this.assetUri + " to " + assetUri); } this.assetUri = assetUri; @@ -661,9 +671,11 @@ public Builder setAssetUri(@Nullable Uri assetUri) { /** Sets the {@code assetListUri}. */ public Builder setAssetListUri(@Nullable Uri assetListUri) { - if (assetListUri == null) return this; + if (assetListUri == null) { + return this; + } if (this.assetListUri != null) { - Assertions.checkArgument(!this.assetListUri.equals(assetListUri), + checkArgument(this.assetListUri.equals(assetListUri), "Can't change assetListUri from " + this.assetListUri + " to " + assetListUri); } this.assetListUri = assetListUri; @@ -672,9 +684,11 @@ public Builder setAssetListUri(@Nullable Uri assetListUri) { /** Sets the {@code startDateUnixUs}. */ public Builder setStartDateUnixUs(long startDateUnixUs) { - if (startDateUnixUs == C.TIME_UNSET) return this; + if (startDateUnixUs == C.TIME_UNSET) { + return this; + } if (this.startDateUnixUs != C.TIME_UNSET) { - Assertions.checkArgument(this.startDateUnixUs != startDateUnixUs, + checkArgument(this.startDateUnixUs == startDateUnixUs, "Can't change startDateUnixUs from " + this.startDateUnixUs + " to " + startDateUnixUs); } this.startDateUnixUs = startDateUnixUs; @@ -683,9 +697,11 @@ public Builder setStartDateUnixUs(long startDateUnixUs) { /** Sets the {@code endDateUnixUs}. */ public Builder setEndDateUnixUs(long endDateUnixUs) { - if (endDateUnixUs == C.TIME_UNSET) return this; + if (endDateUnixUs == C.TIME_UNSET) { + return this; + } if (this.endDateUnixUs != C.TIME_UNSET) { - Assertions.checkArgument(this.endDateUnixUs != endDateUnixUs, + checkArgument(this.endDateUnixUs == endDateUnixUs, "Can't change endDateUnixUs from " + this.endDateUnixUs + " to " + endDateUnixUs); } this.endDateUnixUs = endDateUnixUs; @@ -694,9 +710,11 @@ public Builder setEndDateUnixUs(long endDateUnixUs) { /** Sets the {@code durationUs}. */ public Builder setDurationUs(long durationUs) { - if (durationUs == C.TIME_UNSET) return this; + if (durationUs == C.TIME_UNSET) { + return this; + } if (this.durationUs != C.TIME_UNSET) { - Assertions.checkArgument(this.durationUs != durationUs, + checkArgument(this.durationUs == durationUs, "Can't change durationUs from " + this.durationUs + " to " + durationUs); } this.durationUs = durationUs; @@ -705,9 +723,11 @@ public Builder setDurationUs(long durationUs) { /** Sets the {@code plannedDurationUs}. */ public Builder setPlannedDurationUs(long plannedDurationUs) { - if (plannedDurationUs == C.TIME_UNSET) return this; + if (plannedDurationUs == C.TIME_UNSET) { + return this; + } if (this.plannedDurationUs != C.TIME_UNSET) { - Assertions.checkArgument(this.plannedDurationUs != plannedDurationUs, + checkArgument(this.plannedDurationUs == plannedDurationUs, "Can't change plannedDurationUs from " + this.plannedDurationUs + " to " + plannedDurationUs); } this.plannedDurationUs = plannedDurationUs; @@ -716,9 +736,11 @@ public Builder setPlannedDurationUs(long plannedDurationUs) { /** Sets the trigger {@code cue} types. */ public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { - if (cue.isEmpty()) return this; + if (cue.isEmpty()) { + return this; + } if (!this.cue.isEmpty()) { - Assertions.checkArgument(!this.cue.equals(cue), + checkArgument(this.cue.equals(cue), "Can't change cue from " + String.join(", ", this.cue) + " to " + String.join(", ", cue)); } this.cue = cue; @@ -727,16 +749,20 @@ public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { /** Sets whether the interstitial {@code endOnNext}. */ public Builder setEndOnNext(boolean endOnNext) { - if (!endOnNext) return this; + if (!endOnNext) { + return this; + } this.endOnNext = endOnNext; return this; } /** Sets the {@code resumeOffsetUs}. */ public Builder setResumeOffsetUs(long resumeOffsetUs) { - if (resumeOffsetUs == C.TIME_UNSET) return this; + if (resumeOffsetUs == C.TIME_UNSET) { + return this; + } if (this.resumeOffsetUs != C.TIME_UNSET) { - Assertions.checkArgument(this.resumeOffsetUs != resumeOffsetUs, + checkArgument(this.resumeOffsetUs == resumeOffsetUs, "Can't change resumeOffsetUs from " + this.resumeOffsetUs + " to " + resumeOffsetUs); } this.resumeOffsetUs = resumeOffsetUs; @@ -745,9 +771,11 @@ public Builder setResumeOffsetUs(long resumeOffsetUs) { /** Sets the {@code playoutLimitUs}. */ public Builder setPlayoutLimitUs(long playoutLimitUs) { - if (playoutLimitUs == C.TIME_UNSET) return this; + if (playoutLimitUs == C.TIME_UNSET) { + return this; + } if (this.playoutLimitUs != C.TIME_UNSET) { - Assertions.checkArgument(this.playoutLimitUs != playoutLimitUs, + checkArgument(this.playoutLimitUs == playoutLimitUs, "Can't change playoutLimitUs from " + this.playoutLimitUs + " to " + playoutLimitUs); } this.playoutLimitUs = playoutLimitUs; @@ -756,9 +784,11 @@ public Builder setPlayoutLimitUs(long playoutLimitUs) { /** Sets the {@code snapTypes}. */ public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { - if (snapTypes.isEmpty()) return this; + if (snapTypes.isEmpty()) { + return this; + } if (!this.snapTypes.isEmpty()) { - Assertions.checkArgument(!this.snapTypes.equals(snapTypes), + checkArgument(this.snapTypes.equals(snapTypes), "Can't change snapTypes from " + String.join(", ", this.snapTypes) + " to " + String.join(", ", snapTypes)); } this.snapTypes = snapTypes; @@ -767,9 +797,11 @@ public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { /** Sets the {@code NavigationRestriction}. */ public Builder setRestrictions(List<@Interstitial.NavigationRestriction String> restrictions) { - if (restrictions.isEmpty()) return this; + if (restrictions.isEmpty()) { + return this; + } if (!this.restrictions.isEmpty()) { - Assertions.checkArgument(!this.restrictions.equals(restrictions), + checkArgument(this.restrictions.equals(restrictions), "Can't change restrictions from " + String.join(", ", this.restrictions) + " to " + String.join(", ", restrictions)); } this.restrictions = restrictions; @@ -779,9 +811,11 @@ public Builder setRestrictions(List<@Interstitial.NavigationRestriction String> /** Sets the {@code clientDefinedAttributes}. */ public Builder setClientDefinedAttributes( List clientDefinedAttributes) { - if (clientDefinedAttributes.isEmpty()) return this; + if (clientDefinedAttributes.isEmpty()) { + return this; + } if (!this.clientDefinedAttributes.isEmpty()) { - Assertions.checkArgument(!this.clientDefinedAttributes.equals(clientDefinedAttributes)); + checkArgument(this.clientDefinedAttributes.equals(clientDefinedAttributes)); } this.clientDefinedAttributes = clientDefinedAttributes; return this; diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 76e1ab339a4..40b6f6fd305 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1445,7 +1445,7 @@ public void parseMediaPlaylist_withDateRangeWithMissingClass_dateRangeIgnored() } @Test - public void parseMediaPlaylist_withInterstitialWithoutStartDate_noInterstitial() + public void parseMediaPlaylist_interstitialWithoutStartDate_ignored() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = @@ -1615,7 +1615,7 @@ public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_interstitialI public void parseMediaPlaylist_withInterstitialWithUpdatingDateRange() throws IOException { - Uri playlistUri = Uri.parse("https://raw.githubusercontent.com/TomVarga/HLS-test-stream/refs/heads/main/ride/audio.m3u8"); + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" + "#EXT-X-VERSION:3\n" @@ -1647,6 +1647,40 @@ public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_interstitialI assertThat(playlist.interstitials.get(0).playoutLimitUs).isEqualTo(24953741L); } + @Test + public void + parseMediaPlaylist_withInterstitiaStartDateInvalidUpdate_throwsIllegalArgumentException() { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PROGRAM-DATE-TIME:2024-09-20T15:29:20.000Z\n" + + "#EXTINF:10.007800,\n" + + "audio0000.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," + + "X-SNAP=\"OUT,IN\",X-TIMELINE-OCCUPIES=\"RANGE\"," + + "X-TIMELINE-STYLE=\"HIGHLIGHT\",X-CONTENT-MAY-VARY=\"YES\"\n" + + "#EXTINF:10.007800,\n" + + "audio0001.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2024-09-20T15:29:25.006Z\"," + + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," + + "X-RESUME-OFFSET=24.953741497"; + + HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser(); + + assertThrows( + IllegalArgumentException.class, + () -> + hlsPlaylistParser.parse( + playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)))); + } + @Test public void multipleExtXKeysForSingleSegment() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); From 521a91e50beb1b97ee6a68147fcfd5aa6a045a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Varga?= Date: Tue, 20 May 2025 14:08:59 +0200 Subject: [PATCH 04/13] #2426 Support updating HLS Interstitials according to the HLS spec - add validation for clientDefinedAttribute --- .../hls/playlist/HlsMediaPlaylist.java | 27 +++++-- .../hls/playlist/HlsPlaylistParser.java | 9 ++- .../playlist/HlsMediaPlaylistParserTest.java | 76 ++++++++++++++++++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 264c095ff32..6da0cc739e5 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -29,7 +29,6 @@ import androidx.media3.common.C; import androidx.media3.common.DrmInitData; import androidx.media3.common.StreamKey; -import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -39,6 +38,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -635,7 +635,7 @@ public static final class Builder { private long playoutLimitUs; private List<@Interstitial.SnapType String> snapTypes; private List<@Interstitial.NavigationRestriction String> restrictions; - private List clientDefinedAttributes; + private Map clientDefinedAttributes; /** * Creates the builder. @@ -653,7 +653,7 @@ public Builder(String id) { this.playoutLimitUs = C.TIME_UNSET; this.snapTypes = new ArrayList<>(); this.restrictions = new ArrayList<>(); - this.clientDefinedAttributes = new ArrayList<>(); + this.clientDefinedAttributes = new HashMap<>(); } /** Sets the {@code assetUri}. */ @@ -810,14 +810,25 @@ public Builder setRestrictions(List<@Interstitial.NavigationRestriction String> /** Sets the {@code clientDefinedAttributes}. */ public Builder setClientDefinedAttributes( - List clientDefinedAttributes) { + Map clientDefinedAttributes) { if (clientDefinedAttributes.isEmpty()) { return this; } - if (!this.clientDefinedAttributes.isEmpty()) { - checkArgument(this.clientDefinedAttributes.equals(clientDefinedAttributes)); + for (Map.Entry newEntry : clientDefinedAttributes.entrySet()) { + String newKey = newEntry.getKey(); + HlsMediaPlaylist.ClientDefinedAttribute newValue = newEntry.getValue(); + if (this.clientDefinedAttributes.containsKey(newKey)) { + HlsMediaPlaylist.ClientDefinedAttribute existingValue = this.clientDefinedAttributes.get(newKey); + if (existingValue != null) { + checkArgument( + existingValue.equals(newValue), + "Can't change " + newKey + " from " + + existingValue.textValue + " " + existingValue.doubleValue + " to " + + newValue.textValue + " " + newValue.doubleValue); + } + } + this.clientDefinedAttributes.put(newKey, newValue); } - this.clientDefinedAttributes = clientDefinedAttributes; return this; } @@ -839,7 +850,7 @@ public Builder setClientDefinedAttributes( this.playoutLimitUs, this.snapTypes, this.restrictions, - this.clientDefinedAttributes + new ArrayList<>(this.clientDefinedAttributes.values()) ); } return null; 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 196f259b2fa..dd57a5d4999 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 @@ -1167,8 +1167,8 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe } } - ImmutableList.Builder clientDefinedAttributes = - new ImmutableList.Builder<>(); + Map clientDefinedAttributes = + new HashMap<>(); String attributes = line.substring("#EXT-X-DATERANGE:".length()); Matcher matcher = REGEX_CLIENT_DEFINED_ATTRIBUTE_PREFIX.matcher(attributes); while (matcher.find()) { @@ -1183,7 +1183,8 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe // ignore interstitial attributes break; default: - clientDefinedAttributes.add( + clientDefinedAttributes.put( + attributePrefix, parseClientDefinedAttribute( attributes, attributePrefix.substring(0, attributePrefix.length() - 1), @@ -1206,7 +1207,7 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe .setPlayoutLimitUs(playoutLimitUs) .setSnapTypes(snapTypes) .setRestrictions(restrictions) - .setClientDefinedAttributes(clientDefinedAttributes.build()); + .setClientDefinedAttributes(clientDefinedAttributes); interstitialBuilderMap.put(id, interstitialBuilder); } else if (!line.startsWith("#")) { @Nullable diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 40b6f6fd305..563252f4bb9 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1673,12 +1673,80 @@ public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_interstitialI + "X-RESUME-OFFSET=24.953741497"; HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); assertThrows( - IllegalArgumentException.class, - () -> - hlsPlaylistParser.parse( - playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)))); + IllegalArgumentException.class, () -> hlsPlaylistParser.parse(playlistUri, inputStream)); + } + + @Test + public void + parseMediaPlaylist_withInterstitialClientDefinedAttributeInvalidUpdate_throwsIllegalArgumentException() { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PROGRAM-DATE-TIME:2024-09-20T15:29:20.000Z\n" + + "#EXTINF:10.007800,\n" + + "audio0000.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," + + "X-SNAP=\"OUT,IN\",X-TIMELINE-OCCUPIES=\"RANGE\"," + + "X-TIMELINE-STYLE=\"HIGHLIGHT\",X-CONTENT-MAY-VARY=\"YES\"\n" + + "#EXTINF:10.007800,\n" + + "audio0001.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," + + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," + + "X-CONTENT-MAY-VARY=\"NO\"," + + "X-RESUME-OFFSET=24.953741497"; + + HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + + assertThrows( + IllegalArgumentException.class, () -> hlsPlaylistParser.parse(playlistUri, inputStream)); + } + + @Test + public void parseMediaPlaylist_withInterstitialClientDefinedAttribute() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PROGRAM-DATE-TIME:2024-09-20T15:29:20.000Z\n" + + "#EXTINF:10.007800,\n" + + "audio0000.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," + + "X-SNAP=\"OUT,IN\"," + + "X-CONTENT-MAY-VARY=\"YES\"\n" + + "#EXTINF:10.007800,\n" + + "audio0001.ts\n" + + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," + + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," + + "X-CONTENT-MAY-VARY=\"YES\"," + + "X-RESUME-OFFSET=24.953741497"; + + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) + new HlsPlaylistParser() + .parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))); + + assertThat(playlist.interstitials.size()).isEqualTo(1); + ImmutableList clientDefinedAttributes = + playlist.interstitials.get(0).clientDefinedAttributes; + assertThat(clientDefinedAttributes.size()).isEqualTo(1); + HlsMediaPlaylist.ClientDefinedAttribute clientDefinedAttribute = clientDefinedAttributes.get(0); + assertThat(clientDefinedAttribute.name).isEqualTo("X-CONTENT-MAY-VARY"); + assertThat(clientDefinedAttribute.getTextValue()).isEqualTo("YES"); } @Test From d580062a5305bc652e94157d1ad1fa3d95270414 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Tue, 20 May 2025 23:08:40 +0200 Subject: [PATCH 05/13] Format with google-java-format --- .../hls/playlist/HlsMediaPlaylist.java | 91 +++++++++++++------ .../hls/playlist/HlsPlaylistParser.java | 35 +++---- .../playlist/HlsMediaPlaylistParserTest.java | 9 +- 3 files changed, 83 insertions(+), 52 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 6da0cc739e5..881d709e695 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -613,12 +613,11 @@ public int hashCode() { clientDefinedAttributes); } - /** * Builder for {@link Interstitial}. * - *

See RFC 8216bis, section 4.4.5.1 for how to consolidate multiple interstitials with - * the same {@linkplain HlsMediaPlaylist.Interstitial#id ID}. + *

See RFC 8216bis, section 4.4.5.1 for how to consolidate multiple interstitials with the + * same {@linkplain HlsMediaPlaylist.Interstitial#id ID}. */ public static final class Builder { @@ -662,7 +661,8 @@ public Builder setAssetUri(@Nullable Uri assetUri) { return this; } if (this.assetUri != null) { - checkArgument(this.assetUri.equals(assetUri), + checkArgument( + this.assetUri.equals(assetUri), "Can't change assetUri from " + this.assetUri + " to " + assetUri); } this.assetUri = assetUri; @@ -675,7 +675,8 @@ public Builder setAssetListUri(@Nullable Uri assetListUri) { return this; } if (this.assetListUri != null) { - checkArgument(this.assetListUri.equals(assetListUri), + checkArgument( + this.assetListUri.equals(assetListUri), "Can't change assetListUri from " + this.assetListUri + " to " + assetListUri); } this.assetListUri = assetListUri; @@ -688,8 +689,12 @@ public Builder setStartDateUnixUs(long startDateUnixUs) { return this; } if (this.startDateUnixUs != C.TIME_UNSET) { - checkArgument(this.startDateUnixUs == startDateUnixUs, - "Can't change startDateUnixUs from " + this.startDateUnixUs + " to " + startDateUnixUs); + checkArgument( + this.startDateUnixUs == startDateUnixUs, + "Can't change startDateUnixUs from " + + this.startDateUnixUs + + " to " + + startDateUnixUs); } this.startDateUnixUs = startDateUnixUs; return this; @@ -701,7 +706,8 @@ public Builder setEndDateUnixUs(long endDateUnixUs) { return this; } if (this.endDateUnixUs != C.TIME_UNSET) { - checkArgument(this.endDateUnixUs == endDateUnixUs, + checkArgument( + this.endDateUnixUs == endDateUnixUs, "Can't change endDateUnixUs from " + this.endDateUnixUs + " to " + endDateUnixUs); } this.endDateUnixUs = endDateUnixUs; @@ -714,7 +720,8 @@ public Builder setDurationUs(long durationUs) { return this; } if (this.durationUs != C.TIME_UNSET) { - checkArgument(this.durationUs == durationUs, + checkArgument( + this.durationUs == durationUs, "Can't change durationUs from " + this.durationUs + " to " + durationUs); } this.durationUs = durationUs; @@ -727,8 +734,12 @@ public Builder setPlannedDurationUs(long plannedDurationUs) { return this; } if (this.plannedDurationUs != C.TIME_UNSET) { - checkArgument(this.plannedDurationUs == plannedDurationUs, - "Can't change plannedDurationUs from " + this.plannedDurationUs + " to " + plannedDurationUs); + checkArgument( + this.plannedDurationUs == plannedDurationUs, + "Can't change plannedDurationUs from " + + this.plannedDurationUs + + " to " + + plannedDurationUs); } this.plannedDurationUs = plannedDurationUs; return this; @@ -740,8 +751,12 @@ public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { return this; } if (!this.cue.isEmpty()) { - checkArgument(this.cue.equals(cue), - "Can't change cue from " + String.join(", ", this.cue) + " to " + String.join(", ", cue)); + checkArgument( + this.cue.equals(cue), + "Can't change cue from " + + String.join(", ", this.cue) + + " to " + + String.join(", ", cue)); } this.cue = cue; return this; @@ -762,7 +777,8 @@ public Builder setResumeOffsetUs(long resumeOffsetUs) { return this; } if (this.resumeOffsetUs != C.TIME_UNSET) { - checkArgument(this.resumeOffsetUs == resumeOffsetUs, + checkArgument( + this.resumeOffsetUs == resumeOffsetUs, "Can't change resumeOffsetUs from " + this.resumeOffsetUs + " to " + resumeOffsetUs); } this.resumeOffsetUs = resumeOffsetUs; @@ -775,7 +791,8 @@ public Builder setPlayoutLimitUs(long playoutLimitUs) { return this; } if (this.playoutLimitUs != C.TIME_UNSET) { - checkArgument(this.playoutLimitUs == playoutLimitUs, + checkArgument( + this.playoutLimitUs == playoutLimitUs, "Can't change playoutLimitUs from " + this.playoutLimitUs + " to " + playoutLimitUs); } this.playoutLimitUs = playoutLimitUs; @@ -788,21 +805,30 @@ public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { return this; } if (!this.snapTypes.isEmpty()) { - checkArgument(this.snapTypes.equals(snapTypes), - "Can't change snapTypes from " + String.join(", ", this.snapTypes) + " to " + String.join(", ", snapTypes)); + checkArgument( + this.snapTypes.equals(snapTypes), + "Can't change snapTypes from " + + String.join(", ", this.snapTypes) + + " to " + + String.join(", ", snapTypes)); } this.snapTypes = snapTypes; return this; } /** Sets the {@code NavigationRestriction}. */ - public Builder setRestrictions(List<@Interstitial.NavigationRestriction String> restrictions) { + public Builder setRestrictions( + List<@Interstitial.NavigationRestriction String> restrictions) { if (restrictions.isEmpty()) { return this; } if (!this.restrictions.isEmpty()) { - checkArgument(this.restrictions.equals(restrictions), - "Can't change restrictions from " + String.join(", ", this.restrictions) + " to " + String.join(", ", restrictions)); + checkArgument( + this.restrictions.equals(restrictions), + "Can't change restrictions from " + + String.join(", ", this.restrictions) + + " to " + + String.join(", ", restrictions)); } this.restrictions = restrictions; return this; @@ -814,17 +840,26 @@ public Builder setClientDefinedAttributes( if (clientDefinedAttributes.isEmpty()) { return this; } - for (Map.Entry newEntry : clientDefinedAttributes.entrySet()) { + for (Map.Entry newEntry : + clientDefinedAttributes.entrySet()) { String newKey = newEntry.getKey(); HlsMediaPlaylist.ClientDefinedAttribute newValue = newEntry.getValue(); if (this.clientDefinedAttributes.containsKey(newKey)) { - HlsMediaPlaylist.ClientDefinedAttribute existingValue = this.clientDefinedAttributes.get(newKey); + HlsMediaPlaylist.ClientDefinedAttribute existingValue = + this.clientDefinedAttributes.get(newKey); if (existingValue != null) { checkArgument( existingValue.equals(newValue), - "Can't change " + newKey + " from " - + existingValue.textValue + " " + existingValue.doubleValue + " to " - + newValue.textValue + " " + newValue.doubleValue); + "Can't change " + + newKey + + " from " + + existingValue.textValue + + " " + + existingValue.doubleValue + + " to " + + newValue.textValue + + " " + + newValue.doubleValue); } } this.clientDefinedAttributes.put(newKey, newValue); @@ -834,8 +869,7 @@ public Builder setClientDefinedAttributes( public @Nullable Interstitial build() { if ((assetListUri == null && assetUri != null) - || (assetListUri != null && assetUri == null) - && startDateUnixUs != C.TIME_UNSET) { + || (assetListUri != null && assetUri == null) && startDateUnixUs != C.TIME_UNSET) { return new Interstitial( this.id, this.assetUri, @@ -850,8 +884,7 @@ public Builder setClientDefinedAttributes( this.playoutLimitUs, this.snapTypes, this.restrictions, - new ArrayList<>(this.clientDefinedAttributes.values()) - ); + new ArrayList<>(this.clientDefinedAttributes.values())); } return null; } 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 dd57a5d4999..ac3d5aec661 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 @@ -50,7 +50,6 @@ import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant; import androidx.media3.exoplayer.upstream.ParsingLoadable; import androidx.media3.extractor.mp4.PsshAtomUtil; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import java.io.BufferedReader; import java.io.IOException; @@ -1193,22 +1192,24 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe } } - Interstitial.Builder interstitialBuilder = (interstitialBuilderMap.containsKey(id) ? - interstitialBuilderMap.get(id) : new Interstitial.Builder(id)) - .setAssetUri(assetUri) - .setAssetListUri(assetListUri) - .setStartDateUnixUs(startDateUnixUs) - .setEndDateUnixUs(endDateUnixUs) - .setDurationUs(durationUs) - .setPlannedDurationUs(plannedDurationUs) - .setCue(cue) - .setEndOnNext(endOnNext) - .setResumeOffsetUs(resumeOffsetUs) - .setPlayoutLimitUs(playoutLimitUs) - .setSnapTypes(snapTypes) - .setRestrictions(restrictions) - .setClientDefinedAttributes(clientDefinedAttributes); - interstitialBuilderMap.put(id, interstitialBuilder); + Interstitial.Builder interstitialBuilder = + (interstitialBuilderMap.containsKey(id) + ? interstitialBuilderMap.get(id) + : new Interstitial.Builder(id)) + .setAssetUri(assetUri) + .setAssetListUri(assetListUri) + .setStartDateUnixUs(startDateUnixUs) + .setEndDateUnixUs(endDateUnixUs) + .setDurationUs(durationUs) + .setPlannedDurationUs(plannedDurationUs) + .setCue(cue) + .setEndOnNext(endOnNext) + .setResumeOffsetUs(resumeOffsetUs) + .setPlayoutLimitUs(playoutLimitUs) + .setSnapTypes(snapTypes) + .setRestrictions(restrictions) + .setClientDefinedAttributes(clientDefinedAttributes); + interstitialBuilderMap.put(id, interstitialBuilder); } else if (!line.startsWith("#")) { @Nullable String segmentEncryptionIV = diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 563252f4bb9..55ce17956fa 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1445,8 +1445,7 @@ public void parseMediaPlaylist_withDateRangeWithMissingClass_dateRangeIgnored() } @Test - public void parseMediaPlaylist_interstitialWithoutStartDate_ignored() - throws IOException { + public void parseMediaPlaylist_interstitialWithoutStartDate_ignored() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -1612,9 +1611,7 @@ public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_interstitialI } @Test - public void - parseMediaPlaylist_withInterstitialWithUpdatingDateRange() - throws IOException { + public void parseMediaPlaylist_withInterstitialWithUpdatingDateRange() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -1649,7 +1646,7 @@ public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_interstitialI @Test public void - parseMediaPlaylist_withInterstitiaStartDateInvalidUpdate_throwsIllegalArgumentException() { + parseMediaPlaylist_withInterstitiaStartDateInvalidUpdate_throwsIllegalArgumentException() { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" From 0f5ec84ed263cb1d17f2003d30007a585a4d8c41 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Tue, 20 May 2025 23:13:28 +0200 Subject: [PATCH 06/13] Add release notes --- RELEASENOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 029c5f9e67d..ccd16d808c1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -48,6 +48,8 @@ * Fix playlist parsing to accept `\f` (form feed) in quoted string attribute values ([#2420](https://github.com/androidx/media/issues/2420)). + * Support updating interstitials with the same ID + ([#2427](https://github.com/androidx/media/pull/2427)). * DASH extension: * Smooth Streaming extension: * RTSP extension: From 33f8b0682ceba799594380c122c8a81b32b5ce88 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Tue, 20 May 2025 23:22:22 +0200 Subject: [PATCH 07/13] minor cleanup --- .../hls/playlist/HlsMediaPlaylist.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 881d709e695..deaa2b55979 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -622,6 +622,8 @@ public int hashCode() { public static final class Builder { private final String id; + private final Map clientDefinedAttributes; + @Nullable private Uri assetUri; @Nullable private Uri assetListUri; private long startDateUnixUs; @@ -634,7 +636,6 @@ public static final class Builder { private long playoutLimitUs; private List<@Interstitial.SnapType String> snapTypes; private List<@Interstitial.NavigationRestriction String> restrictions; - private Map clientDefinedAttributes; /** * Creates the builder. @@ -643,20 +644,21 @@ public static final class Builder { */ public Builder(String id) { this.id = id; - this.startDateUnixUs = C.TIME_UNSET; - this.endDateUnixUs = C.TIME_UNSET; - this.durationUs = C.TIME_UNSET; - this.plannedDurationUs = C.TIME_UNSET; - this.cue = new ArrayList<>(); - this.resumeOffsetUs = C.TIME_UNSET; - this.playoutLimitUs = C.TIME_UNSET; - this.snapTypes = new ArrayList<>(); - this.restrictions = new ArrayList<>(); - this.clientDefinedAttributes = new HashMap<>(); + startDateUnixUs = C.TIME_UNSET; + endDateUnixUs = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + plannedDurationUs = C.TIME_UNSET; + cue = new ArrayList<>(); + resumeOffsetUs = C.TIME_UNSET; + playoutLimitUs = C.TIME_UNSET; + snapTypes = new ArrayList<>(); + restrictions = new ArrayList<>(); + clientDefinedAttributes = new HashMap<>(); } /** Sets the {@code assetUri}. */ - public Builder setAssetUri(@Nullable Uri assetUri) { + @CanIgnoreReturnValue + public Builder setAssetUri(@Nullable Uri a1ssetUri) { if (assetUri == null) { return this; } @@ -670,6 +672,7 @@ public Builder setAssetUri(@Nullable Uri assetUri) { } /** Sets the {@code assetListUri}. */ + @CanIgnoreReturnValue public Builder setAssetListUri(@Nullable Uri assetListUri) { if (assetListUri == null) { return this; @@ -701,6 +704,7 @@ public Builder setStartDateUnixUs(long startDateUnixUs) { } /** Sets the {@code endDateUnixUs}. */ + @CanIgnoreReturnValue public Builder setEndDateUnixUs(long endDateUnixUs) { if (endDateUnixUs == C.TIME_UNSET) { return this; From e2ed67feb1b9a0f187e97eceb7a171273ba0ae1f Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 21 May 2025 00:54:06 +0200 Subject: [PATCH 08/13] Minor cleanup --- .../hls/playlist/HlsMediaPlaylist.java | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index deaa2b55979..cd89e0d51e9 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -687,6 +688,7 @@ public Builder setAssetListUri(@Nullable Uri assetListUri) { } /** Sets the {@code startDateUnixUs}. */ + @CanIgnoreReturnValue public Builder setStartDateUnixUs(long startDateUnixUs) { if (startDateUnixUs == C.TIME_UNSET) { return this; @@ -719,6 +721,7 @@ public Builder setEndDateUnixUs(long endDateUnixUs) { } /** Sets the {@code durationUs}. */ + @CanIgnoreReturnValue public Builder setDurationUs(long durationUs) { if (durationUs == C.TIME_UNSET) { return this; @@ -733,6 +736,7 @@ public Builder setDurationUs(long durationUs) { } /** Sets the {@code plannedDurationUs}. */ + @CanIgnoreReturnValue public Builder setPlannedDurationUs(long plannedDurationUs) { if (plannedDurationUs == C.TIME_UNSET) { return this; @@ -750,6 +754,7 @@ public Builder setPlannedDurationUs(long plannedDurationUs) { } /** Sets the trigger {@code cue} types. */ + @CanIgnoreReturnValue public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { if (cue.isEmpty()) { return this; @@ -767,6 +772,7 @@ public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { } /** Sets whether the interstitial {@code endOnNext}. */ + @CanIgnoreReturnValue public Builder setEndOnNext(boolean endOnNext) { if (!endOnNext) { return this; @@ -776,6 +782,7 @@ public Builder setEndOnNext(boolean endOnNext) { } /** Sets the {@code resumeOffsetUs}. */ + @CanIgnoreReturnValue public Builder setResumeOffsetUs(long resumeOffsetUs) { if (resumeOffsetUs == C.TIME_UNSET) { return this; @@ -790,6 +797,7 @@ public Builder setResumeOffsetUs(long resumeOffsetUs) { } /** Sets the {@code playoutLimitUs}. */ + @CanIgnoreReturnValue public Builder setPlayoutLimitUs(long playoutLimitUs) { if (playoutLimitUs == C.TIME_UNSET) { return this; @@ -804,6 +812,7 @@ public Builder setPlayoutLimitUs(long playoutLimitUs) { } /** Sets the {@code snapTypes}. */ + @CanIgnoreReturnValue public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { if (snapTypes.isEmpty()) { return this; @@ -821,6 +830,7 @@ public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { } /** Sets the {@code NavigationRestriction}. */ + @CanIgnoreReturnValue public Builder setRestrictions( List<@Interstitial.NavigationRestriction String> restrictions) { if (restrictions.isEmpty()) { @@ -839,6 +849,7 @@ public Builder setRestrictions( } /** Sets the {@code clientDefinedAttributes}. */ + @CanIgnoreReturnValue public Builder setClientDefinedAttributes( Map clientDefinedAttributes) { if (clientDefinedAttributes.isEmpty()) { @@ -875,20 +886,20 @@ public Builder setClientDefinedAttributes( if ((assetListUri == null && assetUri != null) || (assetListUri != null && assetUri == null) && startDateUnixUs != C.TIME_UNSET) { return new Interstitial( - this.id, - this.assetUri, - this.assetListUri, - this.startDateUnixUs, - this.endDateUnixUs, - this.durationUs, - this.plannedDurationUs, - this.cue, - this.endOnNext, - this.resumeOffsetUs, - this.playoutLimitUs, - this.snapTypes, - this.restrictions, - new ArrayList<>(this.clientDefinedAttributes.values())); + id, + assetUri, + assetListUri, + startDateUnixUs, + endDateUnixUs, + durationUs, + plannedDurationUs, + cue, + endOnNext, + resumeOffsetUs, + playoutLimitUs, + snapTypes, + restrictions, + new ArrayList<>(clientDefinedAttributes.values())); } return null; } From 0f4cda48eba51979d454df5715d8f87411476201 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 21 May 2025 01:01:53 +0200 Subject: [PATCH 09/13] Ensure equality of interstitial --- .../hls/playlist/HlsMediaPlaylist.java | 47 +++++++++---------- .../hls/playlist/HlsPlaylistParser.java | 6 +-- .../playlist/HlsMediaPlaylistParserTest.java | 6 +-- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index cd89e0d51e9..97b043ecfb1 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -567,7 +567,10 @@ public Interstitial( this.playoutLimitUs = playoutLimitUs; this.snapTypes = ImmutableList.copyOf(snapTypes); this.restrictions = ImmutableList.copyOf(restrictions); - this.clientDefinedAttributes = ImmutableList.copyOf(clientDefinedAttributes); + // Sort to ensure equality decoupled from how exactly parsing is implemented. + this.clientDefinedAttributes = + ImmutableList.sortedCopyOf( + (o1, o2) -> o1.name.compareTo(o2.name), clientDefinedAttributes); } @Override @@ -851,33 +854,29 @@ public Builder setRestrictions( /** Sets the {@code clientDefinedAttributes}. */ @CanIgnoreReturnValue public Builder setClientDefinedAttributes( - Map clientDefinedAttributes) { + List clientDefinedAttributes) { if (clientDefinedAttributes.isEmpty()) { return this; } - for (Map.Entry newEntry : - clientDefinedAttributes.entrySet()) { - String newKey = newEntry.getKey(); - HlsMediaPlaylist.ClientDefinedAttribute newValue = newEntry.getValue(); - if (this.clientDefinedAttributes.containsKey(newKey)) { - HlsMediaPlaylist.ClientDefinedAttribute existingValue = - this.clientDefinedAttributes.get(newKey); - if (existingValue != null) { - checkArgument( - existingValue.equals(newValue), - "Can't change " - + newKey - + " from " - + existingValue.textValue - + " " - + existingValue.doubleValue - + " to " - + newValue.textValue - + " " - + newValue.doubleValue); - } + for (int i = 0; i < clientDefinedAttributes.size(); i++) { + ClientDefinedAttribute newAttribute = clientDefinedAttributes.get(i); + String newName = newAttribute.name; + ClientDefinedAttribute existingAttribute = this.clientDefinedAttributes.get(newName); + if (existingAttribute != null) { + checkArgument( + existingAttribute.equals(newAttribute), + "Can't change " + + newName + + " from " + + existingAttribute.textValue + + " " + + existingAttribute.doubleValue + + " to " + + newAttribute.textValue + + " " + + newAttribute.doubleValue); } - this.clientDefinedAttributes.put(newKey, newValue); + this.clientDefinedAttributes.put(newName, newAttribute); } return this; } 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 ac3d5aec661..2d2559eac56 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 @@ -1166,8 +1166,7 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe } } - Map clientDefinedAttributes = - new HashMap<>(); + List clientDefinedAttributes = new ArrayList<>(); String attributes = line.substring("#EXT-X-DATERANGE:".length()); Matcher matcher = REGEX_CLIENT_DEFINED_ATTRIBUTE_PREFIX.matcher(attributes); while (matcher.find()) { @@ -1182,8 +1181,7 @@ && parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDe // ignore interstitial attributes break; default: - clientDefinedAttributes.put( - attributePrefix, + clientDefinedAttributes.add( parseClientDefinedAttribute( attributes, attributePrefix.substring(0, attributePrefix.length() - 1), diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 55ce17956fa..289bd317f2d 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1197,12 +1197,12 @@ public void parseMediaPlaylist_withInterstitialDateRanges() throws IOException, /* snapTypes= */ ImmutableList.of(), /* restrictions= */ ImmutableList.of(), /* clientDefinedAttributes= */ ImmutableList.of( + new HlsMediaPlaylist.ClientDefinedAttribute("X-GOOGLE-TEST-DOUBLE1", 12.123d), + new HlsMediaPlaylist.ClientDefinedAttribute("X-GOOGLE-TEST-DOUBLE2", 1d), new HlsMediaPlaylist.ClientDefinedAttribute( "X-GOOGLE-TEST-HEX", "0XAB10A", - HlsMediaPlaylist.ClientDefinedAttribute.TYPE_HEX_TEXT), - new HlsMediaPlaylist.ClientDefinedAttribute("X-GOOGLE-TEST-DOUBLE1", 12.123d), - new HlsMediaPlaylist.ClientDefinedAttribute("X-GOOGLE-TEST-DOUBLE2", 1d))); + HlsMediaPlaylist.ClientDefinedAttribute.TYPE_HEX_TEXT))); InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); HlsMediaPlaylist playlist = From 170dd7138715295f533adc07b9eb53c23f23e68b Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 21 May 2025 10:54:37 +0200 Subject: [PATCH 10/13] Fix typo --- .../media3/exoplayer/hls/playlist/HlsMediaPlaylist.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 97b043ecfb1..010d70df1b0 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -662,8 +662,8 @@ public Builder(String id) { /** Sets the {@code assetUri}. */ @CanIgnoreReturnValue - public Builder setAssetUri(@Nullable Uri a1ssetUri) { - if (assetUri == null) { + public Builder setAssetUri(@Nullable Uri assetUri) { + if (assetUri == null) { return this; } if (this.assetUri != null) { From 6b167de4c4d9aef9238b265a4db5bb3ecbba6ddc Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 21 May 2025 12:27:01 +0200 Subject: [PATCH 11/13] Resolve warnings from internal linter --- .../exoplayer/hls/playlist/HlsMediaPlaylist.java | 11 ++++++----- .../hls/playlist/HlsMediaPlaylistParserTest.java | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 010d70df1b0..7a6846eddaf 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -663,7 +663,7 @@ public Builder(String id) { /** Sets the {@code assetUri}. */ @CanIgnoreReturnValue public Builder setAssetUri(@Nullable Uri assetUri) { - if (assetUri == null) { + if (assetUri == null) { return this; } if (this.assetUri != null) { @@ -780,7 +780,7 @@ public Builder setEndOnNext(boolean endOnNext) { if (!endOnNext) { return this; } - this.endOnNext = endOnNext; + this.endOnNext = true; return this; } @@ -881,9 +881,10 @@ public Builder setClientDefinedAttributes( return this; } - public @Nullable Interstitial build() { - if ((assetListUri == null && assetUri != null) - || (assetListUri != null && assetUri == null) && startDateUnixUs != C.TIME_UNSET) { + @Nullable + public Interstitial build() { + if (((assetListUri == null && assetUri != null) + || (assetListUri != null && assetUri == null)) && startDateUnixUs != C.TIME_UNSET) { return new Interstitial( id, assetUri, diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 289bd317f2d..8b979df2216 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1638,7 +1638,7 @@ public void parseMediaPlaylist_withInterstitialWithUpdatingDateRange() throws IO new HlsPlaylistParser() .parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))); - assertThat(playlist.interstitials.size()).isEqualTo(1); + assertThat(playlist.interstitials).hasSize(1); assertThat(playlist.interstitials.get(0).resumeOffsetUs).isEqualTo(24953741L); assertThat(playlist.interstitials.get(0).endDateUnixUs).isEqualTo(1726846189006000L); assertThat(playlist.interstitials.get(0).playoutLimitUs).isEqualTo(24953741L); @@ -1737,10 +1737,10 @@ public void parseMediaPlaylist_withInterstitialClientDefinedAttribute() throws I new HlsPlaylistParser() .parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))); - assertThat(playlist.interstitials.size()).isEqualTo(1); + assertThat(playlist.interstitials).hasSize(1); ImmutableList clientDefinedAttributes = playlist.interstitials.get(0).clientDefinedAttributes; - assertThat(clientDefinedAttributes.size()).isEqualTo(1); + assertThat(clientDefinedAttributes).hasSize(1); HlsMediaPlaylist.ClientDefinedAttribute clientDefinedAttribute = clientDefinedAttributes.get(0); assertThat(clientDefinedAttribute.name).isEqualTo("X-CONTENT-MAY-VARY"); assertThat(clientDefinedAttribute.getTextValue()).isEqualTo("YES"); From 4fa449e1817e04447607d7cec7eb06ecdd6c5eb7 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 21 May 2025 13:12:35 +0200 Subject: [PATCH 12/13] Minor reformatting for consistency --- .../playlist/HlsMediaPlaylistParserTest.java | 93 ++++++++++++------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java index 8b979df2216..b1209895bf2 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylistParserTest.java @@ -1620,18 +1620,26 @@ public void parseMediaPlaylist_withInterstitialWithUpdatingDateRange() throws IO + "#EXT-X-MEDIA-SEQUENCE:0\n" + "#EXT-X-PROGRAM-DATE-TIME:2024-09-20T15:29:20.000Z\n" + "#EXTINF:10.007800,\n" - + "audio0000.ts\n" + + "audio0000.ts" + + "\n" + "#EXT-X-DATERANGE:ID=\"15943\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "START-DATE=\"2024-09-20T15:29:24.006Z\"," + + "PLANNED-DURATION=25," + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," - + "X-SNAP=\"OUT,IN\",X-TIMELINE-OCCUPIES=\"RANGE\"," - + "X-TIMELINE-STYLE=\"HIGHLIGHT\",X-CONTENT-MAY-VARY=\"YES\"\n" + + "X-SNAP=\"OUT,IN\"," + + "X-TIMELINE-OCCUPIES=\"RANGE\"," + + "X-TIMELINE-STYLE=\"HIGHLIGHT\"," + + "X-CONTENT-MAY-VARY=\"YES\"" + + "\n" + "#EXTINF:10.007800,\n" - + "audio0001.ts\n" - + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," - + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," - + "X-RESUME-OFFSET=24.953741497"; + + "audio0001.ts" + + "\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "END-DATE=\"2024-09-20T15:29:49.006Z\"," + + "X-PLAYOUT-LIMIT=24.953741497," + + "X-RESUME-OFFSET=24.953741497\n"; HlsMediaPlaylist playlist = (HlsMediaPlaylist) @@ -1646,7 +1654,7 @@ public void parseMediaPlaylist_withInterstitialWithUpdatingDateRange() throws IO @Test public void - parseMediaPlaylist_withInterstitiaStartDateInvalidUpdate_throwsIllegalArgumentException() { + parseMediaPlaylist_withInterstitialStartDateInvalidUpdate_throwsIllegalArgumentException() { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" @@ -1655,20 +1663,27 @@ public void parseMediaPlaylist_withInterstitialWithUpdatingDateRange() throws IO + "#EXT-X-MEDIA-SEQUENCE:0\n" + "#EXT-X-PROGRAM-DATE-TIME:2024-09-20T15:29:20.000Z\n" + "#EXTINF:10.007800,\n" - + "audio0000.ts\n" + + "audio0000.ts" + + "\n" + "#EXT-X-DATERANGE:ID=\"15943\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "START-DATE=\"2024-09-20T15:29:24.006Z\"," + + "PLANNED-DURATION=25," + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," - + "X-SNAP=\"OUT,IN\",X-TIMELINE-OCCUPIES=\"RANGE\"," - + "X-TIMELINE-STYLE=\"HIGHLIGHT\",X-CONTENT-MAY-VARY=\"YES\"\n" + + "X-SNAP=\"OUT,IN\"," + + "X-TIMELINE-OCCUPIES=\"RANGE\"," + + "X-TIMELINE-STYLE=\"HIGHLIGHT\"," + + "X-CONTENT-MAY-VARY=\"YES\"" + + "\n" + "#EXTINF:10.007800,\n" - + "audio0001.ts\n" - + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," + + "audio0001.ts" + + "\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + "START-DATE=\"2024-09-20T15:29:25.006Z\"," - + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," - + "X-RESUME-OFFSET=24.953741497"; - + + "END-DATE=\"2024-09-20T15:29:49.006Z\"," + + "X-PLAYOUT-LIMIT=24.953741497," + + "X-RESUME-OFFSET=24.953741497\n"; HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser(); ByteArrayInputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); @@ -1687,20 +1702,27 @@ public void parseMediaPlaylist_withInterstitialWithUpdatingDateRange() throws IO + "#EXT-X-MEDIA-SEQUENCE:0\n" + "#EXT-X-PROGRAM-DATE-TIME:2024-09-20T15:29:20.000Z\n" + "#EXTINF:10.007800,\n" - + "audio0000.ts\n" + + "audio0000.ts" + + "\n" + "#EXT-X-DATERANGE:ID=\"15943\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "START-DATE=\"2024-09-20T15:29:24.006Z\"," + + "PLANNED-DURATION=25," + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," - + "X-SNAP=\"OUT,IN\",X-TIMELINE-OCCUPIES=\"RANGE\"," - + "X-TIMELINE-STYLE=\"HIGHLIGHT\",X-CONTENT-MAY-VARY=\"YES\"\n" + + "X-SNAP=\"OUT,IN\"," + + "X-TIMELINE-OCCUPIES=\"RANGE\"," + + "X-TIMELINE-STYLE=\"HIGHLIGHT\"," + + "X-CONTENT-MAY-VARY=\"YES\"" + + "\n" + "#EXTINF:10.007800,\n" - + "audio0001.ts\n" - + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," - + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," + + "audio0001.ts" + + "\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "END-DATE=\"2024-09-20T15:29:49.006Z\"," + + "X-PLAYOUT-LIMIT=24.953741497," + "X-CONTENT-MAY-VARY=\"NO\"," - + "X-RESUME-OFFSET=24.953741497"; - + + "X-RESUME-OFFSET=24.953741497\n"; HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser(); ByteArrayInputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); @@ -1721,16 +1743,21 @@ public void parseMediaPlaylist_withInterstitialClientDefinedAttribute() throws I + "audio0000.ts\n" + "#EXT-X-DATERANGE:ID=\"15943\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2024-09-20T15:29:24.006Z\",PLANNED-DURATION=25," + + "START-DATE=\"2024-09-20T15:29:24.006Z\"," + + "PLANNED-DURATION=25," + "X-ASSET-LIST=\"myapp://interstitial/req?_HLS_interstitial_id=15943\"," + "X-SNAP=\"OUT,IN\"," - + "X-CONTENT-MAY-VARY=\"YES\"\n" + + "X-CONTENT-MAY-VARY=\"YES\"" + + "\n" + "#EXTINF:10.007800,\n" - + "audio0001.ts\n" - + "#EXT-X-DATERANGE:ID=\"15943\",CLASS=\"com.apple.hls.interstitial\"," - + "END-DATE=\"2024-09-20T15:29:49.006Z\",X-PLAYOUT-LIMIT=24.953741497," + + "audio0001.ts" + + "\n" + + "#EXT-X-DATERANGE:ID=\"15943\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "END-DATE=\"2024-09-20T15:29:49.006Z\"," + + "X-PLAYOUT-LIMIT=24.953741497," + "X-CONTENT-MAY-VARY=\"YES\"," - + "X-RESUME-OFFSET=24.953741497"; + + "X-RESUME-OFFSET=24.953741497\n"; HlsMediaPlaylist playlist = (HlsMediaPlaylist) From 16b988093ab8ba6821e0e0a12878348764c61ce5 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Thu, 22 May 2025 16:26:32 +0200 Subject: [PATCH 13/13] Address review comments --- .../hls/playlist/HlsMediaPlaylist.java | 108 +++++++++++++++--- 1 file changed, 91 insertions(+), 17 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 7a6846eddaf..c0f917aff69 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -43,6 +43,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Represents an HLS media playlist. */ @UnstableApi @@ -628,8 +629,8 @@ public static final class Builder { private final String id; private final Map clientDefinedAttributes; - @Nullable private Uri assetUri; - @Nullable private Uri assetListUri; + private @MonotonicNonNull Uri assetUri; + private @MonotonicNonNull Uri assetListUri; private long startDateUnixUs; private long endDateUnixUs; private long durationUs; @@ -648,6 +649,7 @@ public static final class Builder { */ public Builder(String id) { this.id = id; + clientDefinedAttributes = new HashMap<>(); startDateUnixUs = C.TIME_UNSET; endDateUnixUs = C.TIME_UNSET; durationUs = C.TIME_UNSET; @@ -657,10 +659,14 @@ public Builder(String id) { playoutLimitUs = C.TIME_UNSET; snapTypes = new ArrayList<>(); restrictions = new ArrayList<>(); - clientDefinedAttributes = new HashMap<>(); } - /** Sets the {@code assetUri}. */ + /** + * Sets the asset URI. + * + * @throws IllegalArgumentException if called with a non-null value that is different to the + * value previously set. + */ @CanIgnoreReturnValue public Builder setAssetUri(@Nullable Uri assetUri) { if (assetUri == null) { @@ -675,7 +681,12 @@ public Builder setAssetUri(@Nullable Uri assetUri) { return this; } - /** Sets the {@code assetListUri}. */ + /** + * Sets the asset list URI. + * + * @throws IllegalArgumentException if called with a non-null value that is different to the + * value previously set. + */ @CanIgnoreReturnValue public Builder setAssetListUri(@Nullable Uri assetListUri) { if (assetListUri == null) { @@ -690,7 +701,12 @@ public Builder setAssetListUri(@Nullable Uri assetListUri) { return this; } - /** Sets the {@code startDateUnixUs}. */ + /** + * Sets the start date as a unix epoch timestamp, in microseconds. + * + * @throws IllegalArgumentException if called with a value different to {@link C#TIME_UNSET} + * and different to the value previously set. + */ @CanIgnoreReturnValue public Builder setStartDateUnixUs(long startDateUnixUs) { if (startDateUnixUs == C.TIME_UNSET) { @@ -708,7 +724,12 @@ public Builder setStartDateUnixUs(long startDateUnixUs) { return this; } - /** Sets the {@code endDateUnixUs}. */ + /** + * Sets the end date as a unix epoch timestamp, in microseconds. + * + * @throws IllegalArgumentException if called with a value different to {@link C#TIME_UNSET} + * and different to the value previously set. + */ @CanIgnoreReturnValue public Builder setEndDateUnixUs(long endDateUnixUs) { if (endDateUnixUs == C.TIME_UNSET) { @@ -723,7 +744,12 @@ public Builder setEndDateUnixUs(long endDateUnixUs) { return this; } - /** Sets the {@code durationUs}. */ + /** + * Sets the duration, in microseconds. + * + * @throws IllegalArgumentException if called with a value different to {@link C#TIME_UNSET} + * and different to the value previously set. + */ @CanIgnoreReturnValue public Builder setDurationUs(long durationUs) { if (durationUs == C.TIME_UNSET) { @@ -738,7 +764,12 @@ public Builder setDurationUs(long durationUs) { return this; } - /** Sets the {@code plannedDurationUs}. */ + /** + * Sets the planned duration, in microseconds. + * + * @throws IllegalArgumentException if called with a value different to {@link C#TIME_UNSET} + * and different to the value previously set. + */ @CanIgnoreReturnValue public Builder setPlannedDurationUs(long plannedDurationUs) { if (plannedDurationUs == C.TIME_UNSET) { @@ -756,7 +787,12 @@ public Builder setPlannedDurationUs(long plannedDurationUs) { return this; } - /** Sets the trigger {@code cue} types. */ + /** + * Sets the {@linkplain Interstitial.CueTriggerType cue trigger types}. + * + * @throws IllegalArgumentException if called with a value different to {@link C#TIME_UNSET} + * and different to the value previously set. + */ @CanIgnoreReturnValue public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { if (cue.isEmpty()) { @@ -774,7 +810,11 @@ public Builder setCue(List<@Interstitial.CueTriggerType String> cue) { return this; } - /** Sets whether the interstitial {@code endOnNext}. */ + /** + * Sets whether the interstitial ends on the start time of the next interstitial. + * + *

Once set to true, it can't be reset to false and doing so would be ignored. + */ @CanIgnoreReturnValue public Builder setEndOnNext(boolean endOnNext) { if (!endOnNext) { @@ -784,7 +824,12 @@ public Builder setEndOnNext(boolean endOnNext) { return this; } - /** Sets the {@code resumeOffsetUs}. */ + /** + * Sets the resume offset, in microseconds. + * + * @throws IllegalArgumentException if called with a value different to {@link C#TIME_UNSET} + * and different to the value previously set. + */ @CanIgnoreReturnValue public Builder setResumeOffsetUs(long resumeOffsetUs) { if (resumeOffsetUs == C.TIME_UNSET) { @@ -799,7 +844,12 @@ public Builder setResumeOffsetUs(long resumeOffsetUs) { return this; } - /** Sets the {@code playoutLimitUs}. */ + /** + * Sets the play out limit, in microseconds. + * + * @throws IllegalArgumentException if called with a value different to {@link C#TIME_UNSET} + * and different to the value previously set. + */ @CanIgnoreReturnValue public Builder setPlayoutLimitUs(long playoutLimitUs) { if (playoutLimitUs == C.TIME_UNSET) { @@ -814,7 +864,12 @@ public Builder setPlayoutLimitUs(long playoutLimitUs) { return this; } - /** Sets the {@code snapTypes}. */ + /** + * Sets the {@linkplain Interstitial.SnapType snap types}. + * + * @throws IllegalArgumentException if called with a non-empty list of snap types that is not + * equal to the non-empty list that was previously set. + */ @CanIgnoreReturnValue public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { if (snapTypes.isEmpty()) { @@ -832,7 +887,12 @@ public Builder setSnapTypes(List<@Interstitial.SnapType String> snapTypes) { return this; } - /** Sets the {@code NavigationRestriction}. */ + /** + * Sets the {@link NavigationRestriction navigation restrictions}. + * + * @throws IllegalArgumentException if called with a non-empty list of restrictions that is + * not equal to the non-empty list that was previously set. + */ @CanIgnoreReturnValue public Builder setRestrictions( List<@Interstitial.NavigationRestriction String> restrictions) { @@ -851,7 +911,15 @@ public Builder setRestrictions( return this; } - /** Sets the {@code clientDefinedAttributes}. */ + /** + * Sets the {@linkplain ClientDefinedAttribute client defined attributes}. + * + *

Equal duplicates are ignored, new attributes are added to those already set. + * + * @throws IllegalArgumentException if called with a list containing a client defined + * attribute that is not equal with an attribute previously set with the same {@linkplain + * ClientDefinedAttribute#name name}. + */ @CanIgnoreReturnValue public Builder setClientDefinedAttributes( List clientDefinedAttributes) { @@ -881,10 +949,16 @@ public Builder setClientDefinedAttributes( return this; } + /** + * Builds and returns a new {@link Interstitial} instance or null if validation of the + * properties fails. The properties are considered invalid, if the start date is missing or + * both asset URI and asset list URI are set at the same time. + */ @Nullable public Interstitial build() { if (((assetListUri == null && assetUri != null) - || (assetListUri != null && assetUri == null)) && startDateUnixUs != C.TIME_UNSET) { + || (assetListUri != null && assetUri == null)) + && startDateUnixUs != C.TIME_UNSET) { return new Interstitial( id, assetUri,