mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Parse interstitials into HLS media playlist
PiperOrigin-RevId: 696454575
This commit is contained in:
parent
16a15b94ca
commit
6453054878
@ -15,6 +15,9 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
@ -22,6 +25,7 @@ import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringDef;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.DrmInitData;
|
||||
import androidx.media3.common.StreamKey;
|
||||
@ -36,6 +40,7 @@ import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Represents an HLS media playlist. */
|
||||
@UnstableApi
|
||||
@ -372,7 +377,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
/**
|
||||
* A rendition report for an alternative rendition defined in another media playlist.
|
||||
*
|
||||
* <p>See RFC 8216, section 4.4.5.1.4.
|
||||
* <p>See RFC 8216bis, section 4.4.5.1.4.
|
||||
*/
|
||||
public static final class RenditionReport {
|
||||
/** The URI of the media playlist of the reported rendition. */
|
||||
@ -401,6 +406,303 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An interstitial data range.
|
||||
*
|
||||
* <p>See RFC 8216bis, appendix D.2.
|
||||
*/
|
||||
public static final class Interstitial {
|
||||
|
||||
/**
|
||||
* The cue trigger type. One of {@link #CUE_TRIGGER_PRE}, {@link #CUE_TRIGGER_POST} or {@link
|
||||
* #CUE_TRIGGER_ONCE}.
|
||||
*
|
||||
* <p>See RFC 8216bis, section 4.4.5.1.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@StringDef({CUE_TRIGGER_PRE, CUE_TRIGGER_POST, CUE_TRIGGER_ONCE})
|
||||
@Documented
|
||||
@Target(TYPE_USE)
|
||||
public @interface CueTriggerType {}
|
||||
|
||||
/**
|
||||
* Cue trigger type indicating to trigger the interstitial before playback of the primary asset.
|
||||
*/
|
||||
public static final String CUE_TRIGGER_PRE = "PRE";
|
||||
|
||||
/**
|
||||
* Cue trigger type indicating to trigger the interstitial after playback of the primary asset.
|
||||
*/
|
||||
public static final String CUE_TRIGGER_POST = "POST";
|
||||
|
||||
/** Cue trigger type indicating to trigger the interstitial only once. */
|
||||
public static final String CUE_TRIGGER_ONCE = "ONCE";
|
||||
|
||||
/**
|
||||
* The snap identifier. One of {@link #SNAP_TYPE_IN} or {@link #SNAP_TYPE_OUT}.
|
||||
*
|
||||
* <p>See RFC 8216bis, appendix D.2.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@StringDef({SNAP_TYPE_IN, SNAP_TYPE_OUT})
|
||||
@Documented
|
||||
@Target(TYPE_USE)
|
||||
public @interface SnapType {}
|
||||
|
||||
/**
|
||||
* Snap identifier indicating to locate the segment boundary closest to the scheduled resumption
|
||||
* point of the interstitial.
|
||||
*/
|
||||
public static final String SNAP_TYPE_IN = "IN";
|
||||
|
||||
/**
|
||||
* Snap identifier indicating to locate the segment boundary closest to the {@link
|
||||
* Interstitial#startDateUnixUs}.
|
||||
*/
|
||||
public static final String SNAP_TYPE_OUT = "OUT";
|
||||
|
||||
/**
|
||||
* The navigation restriction identifier. One of {@link #NAVIGATION_RESTRICTION_JUMP} or {@link
|
||||
* #NAVIGATION_RESTRICTION_SKIP}.
|
||||
*
|
||||
* <p>See RFC 8216bis, appendix D.2.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@StringDef({NAVIGATION_RESTRICTION_SKIP, NAVIGATION_RESTRICTION_JUMP})
|
||||
@Documented
|
||||
@Target(TYPE_USE)
|
||||
public @interface NavigationRestriction {}
|
||||
|
||||
/**
|
||||
* Navigation restriction identifier indicating to prevent seeking or changing the playback
|
||||
* speed during the interstitial being played.
|
||||
*/
|
||||
public static final String NAVIGATION_RESTRICTION_SKIP = "SKIP";
|
||||
|
||||
/**
|
||||
* Navigation restriction identifier indicating to enforce playback of the interstitial if the
|
||||
* user attempts to seek beyond the interstitial start position.
|
||||
*/
|
||||
public static final String NAVIGATION_RESTRICTION_JUMP = "JUMP";
|
||||
|
||||
/** The required ID. */
|
||||
public final String id;
|
||||
|
||||
/** The asset URI. Required if {@link #assetListUri} is null. */
|
||||
@Nullable public final Uri assetUri;
|
||||
|
||||
/** The asset list URI. Required if {@link #assetUri} is null. */
|
||||
@Nullable public final Uri assetListUri;
|
||||
|
||||
/** The required start time, in microseconds. */
|
||||
public final long startDateUnixUs;
|
||||
|
||||
/** The optional end time, in microseconds. {@link C#TIME_UNSET} if not present. */
|
||||
public final long endDateUnixUs;
|
||||
|
||||
/** The optional duration, in microseconds. {@link C#TIME_UNSET} if not present. */
|
||||
public final long durationUs;
|
||||
|
||||
/** The optional planned duration, in microseconds. {@link C#TIME_UNSET} if not present. */
|
||||
public final long plannedDurationUs;
|
||||
|
||||
/** The trigger cue types. */
|
||||
public final List<@CueTriggerType String> cue;
|
||||
|
||||
/**
|
||||
* Whether the {@link #endDateUnixUs} of the interstitial is equal to the start {@link
|
||||
* #startTimeUs} of the following interstitial. {@code false} if not present.
|
||||
*/
|
||||
public final boolean endOnNext;
|
||||
|
||||
/**
|
||||
* The offset from {@link #startTimeUs} indicating where in the primary asset to resume playback
|
||||
* after completing playback of the interstitial. {@link C#TIME_UNSET} if not present. If not
|
||||
* present, the value is considered to be the duration of the interstitial.
|
||||
*/
|
||||
public final long resumeOffsetUs;
|
||||
|
||||
/** The playout limit indicating the limit of the playback time of the interstitial. */
|
||||
public final long playoutLimitUs;
|
||||
|
||||
/** The snap types. */
|
||||
public final ImmutableList<@SnapType String> snapTypes;
|
||||
|
||||
/** The navigation restrictions. */
|
||||
public final ImmutableList<@NavigationRestriction String> restrictions;
|
||||
|
||||
/** The attributes defined by a client. For informational purpose only. */
|
||||
public final ImmutableList<ClientDefinedAttribute> clientDefinedAttributes;
|
||||
|
||||
/** Creates an instance. */
|
||||
public Interstitial(
|
||||
String id,
|
||||
@Nullable Uri assetUri,
|
||||
@Nullable Uri assetListUri,
|
||||
long startDateUnixUs,
|
||||
long endDateUnixUs,
|
||||
long durationUs,
|
||||
long plannedDurationUs,
|
||||
List<@CueTriggerType String> cue,
|
||||
boolean endOnNext,
|
||||
long resumeOffsetUs,
|
||||
long playoutLimitUs,
|
||||
List<@SnapType String> snapTypes,
|
||||
List<@NavigationRestriction String> restrictions,
|
||||
List<ClientDefinedAttribute> clientDefinedAttributes) {
|
||||
checkArgument(
|
||||
(assetUri == null || assetListUri == null) && (assetUri != null || assetListUri != null));
|
||||
this.id = id;
|
||||
this.assetUri = assetUri;
|
||||
this.assetListUri = assetListUri;
|
||||
this.startDateUnixUs = startDateUnixUs;
|
||||
this.endDateUnixUs = endDateUnixUs;
|
||||
this.durationUs = durationUs;
|
||||
this.plannedDurationUs = plannedDurationUs;
|
||||
this.cue = cue;
|
||||
this.endOnNext = endOnNext;
|
||||
this.resumeOffsetUs = resumeOffsetUs;
|
||||
this.playoutLimitUs = playoutLimitUs;
|
||||
this.snapTypes = ImmutableList.copyOf(snapTypes);
|
||||
this.restrictions = ImmutableList.copyOf(restrictions);
|
||||
this.clientDefinedAttributes = ImmutableList.copyOf(clientDefinedAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Interstitial)) {
|
||||
return false;
|
||||
}
|
||||
Interstitial that = (Interstitial) o;
|
||||
return startDateUnixUs == that.startDateUnixUs
|
||||
&& endDateUnixUs == that.endDateUnixUs
|
||||
&& durationUs == that.durationUs
|
||||
&& plannedDurationUs == that.plannedDurationUs
|
||||
&& endOnNext == that.endOnNext
|
||||
&& resumeOffsetUs == that.resumeOffsetUs
|
||||
&& playoutLimitUs == that.playoutLimitUs
|
||||
&& Objects.equals(id, that.id)
|
||||
&& Objects.equals(assetUri, that.assetUri)
|
||||
&& Objects.equals(assetListUri, that.assetListUri)
|
||||
&& Objects.equals(cue, that.cue)
|
||||
&& Objects.equals(snapTypes, that.snapTypes)
|
||||
&& Objects.equals(restrictions, that.restrictions)
|
||||
&& Objects.equals(clientDefinedAttributes, that.clientDefinedAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
id,
|
||||
assetUri,
|
||||
assetListUri,
|
||||
startDateUnixUs,
|
||||
endDateUnixUs,
|
||||
durationUs,
|
||||
plannedDurationUs,
|
||||
cue,
|
||||
endOnNext,
|
||||
resumeOffsetUs,
|
||||
playoutLimitUs,
|
||||
snapTypes,
|
||||
restrictions,
|
||||
clientDefinedAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
/** A client defined attribute. See RFC 8216bis, section 4.4.5.1. */
|
||||
public static class ClientDefinedAttribute {
|
||||
|
||||
/**
|
||||
* The type of the client defined attribute. One of {@link #TYPE_TEXT}, {@link #TYPE_HEX_TEXT}
|
||||
* or {@link #TYPE_DOUBLE}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({TYPE_DOUBLE, TYPE_HEX_TEXT, TYPE_TEXT})
|
||||
@Documented
|
||||
@Target(TYPE_USE)
|
||||
public @interface Type {}
|
||||
|
||||
/** Type text. See RFC 8216bis, section 4.2, quoted-string. */
|
||||
public static final int TYPE_TEXT = 0;
|
||||
|
||||
/** Type hex text. See RFC 8216bis, section 4.2, hexadecimal-sequence. */
|
||||
public static final int TYPE_HEX_TEXT = 1;
|
||||
|
||||
/** Type double. See RFC 8216bis, section 4.2, decimal-floating-point. */
|
||||
public static final int TYPE_DOUBLE = 2;
|
||||
|
||||
/** The name of the client defined attribute. */
|
||||
public final String name;
|
||||
|
||||
/** The type of the client defined attribute. */
|
||||
public final int type;
|
||||
|
||||
private final double doubleValue;
|
||||
@Nullable private final String textValue;
|
||||
|
||||
/** Creates an instance of type {@link #TYPE_DOUBLE}. */
|
||||
public ClientDefinedAttribute(String name, double value) {
|
||||
this.name = name;
|
||||
this.type = TYPE_DOUBLE;
|
||||
this.doubleValue = value;
|
||||
textValue = null;
|
||||
}
|
||||
|
||||
/** Creates an instance of type {@link #TYPE_TEXT} or {@link #TYPE_HEX_TEXT}. */
|
||||
public ClientDefinedAttribute(String name, String value, @Type int type) {
|
||||
checkState(type != TYPE_HEX_TEXT || value.startsWith("0x") || value.startsWith("0X"));
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.textValue = value;
|
||||
doubleValue = 0.0d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value if the attribute is of {@link #TYPE_DOUBLE}.
|
||||
*
|
||||
* @throws IllegalStateException if the attribute is not of type {@link #TYPE_TEXT} or {@link
|
||||
* #TYPE_HEX_TEXT}.
|
||||
*/
|
||||
public double getDoubleValue() {
|
||||
checkState(type == TYPE_DOUBLE);
|
||||
return doubleValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text value if the attribute is of {@link #TYPE_TEXT} or {@link #TYPE_HEX_TEXT}.
|
||||
*
|
||||
* @throws IllegalStateException if the attribute is not of type {@link #TYPE_DOUBLE}.
|
||||
*/
|
||||
public String getTextValue() {
|
||||
checkState(type != TYPE_DOUBLE);
|
||||
return checkNotNull(textValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ClientDefinedAttribute)) {
|
||||
return false;
|
||||
}
|
||||
ClientDefinedAttribute that = (ClientDefinedAttribute) o;
|
||||
return type == that.type
|
||||
&& Double.compare(doubleValue, that.doubleValue) == 0
|
||||
&& Objects.equals(name, that.name)
|
||||
&& Objects.equals(textValue, that.textValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, type, doubleValue, textValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. One of {@link
|
||||
* #PLAYLIST_TYPE_UNKNOWN}, {@link #PLAYLIST_TYPE_VOD} or {@link #PLAYLIST_TYPE_EVENT}.
|
||||
@ -497,6 +799,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
/** The attributes of the #EXT-X-SERVER-CONTROL header. */
|
||||
public final ServerControl serverControl;
|
||||
|
||||
/**
|
||||
* The interstitials declared as {@code #EXT-X-DATERANGE} with {@code
|
||||
* CLASS="com.apple.hls.interstitial"}
|
||||
*/
|
||||
public final ImmutableList<Interstitial> interstitials;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
*
|
||||
@ -520,6 +828,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
* @param trailingParts See {@link #trailingParts}.
|
||||
* @param serverControl See {@link #serverControl}
|
||||
* @param renditionReports See {@link #renditionReports}.
|
||||
* @param interstitials See {@link #interstitials}.
|
||||
*/
|
||||
public HlsMediaPlaylist(
|
||||
@PlaylistType int playlistType,
|
||||
@ -541,7 +850,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
List<Segment> segments,
|
||||
List<Part> trailingParts,
|
||||
ServerControl serverControl,
|
||||
Map<Uri, RenditionReport> renditionReports) {
|
||||
Map<Uri, RenditionReport> renditionReports,
|
||||
List<Interstitial> interstitials) {
|
||||
super(baseUri, tags, hasIndependentSegments);
|
||||
this.playlistType = playlistType;
|
||||
this.startTimeUs = startTimeUs;
|
||||
@ -558,6 +868,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
this.segments = ImmutableList.copyOf(segments);
|
||||
this.trailingParts = ImmutableList.copyOf(trailingParts);
|
||||
this.renditionReports = ImmutableMap.copyOf(renditionReports);
|
||||
this.interstitials = ImmutableList.copyOf(interstitials);
|
||||
if (!trailingParts.isEmpty()) {
|
||||
Part lastPart = Iterables.getLast(trailingParts);
|
||||
durationUs = lastPart.relativeStartTimeUs + lastPart.durationUs;
|
||||
@ -567,7 +878,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
} else {
|
||||
durationUs = 0;
|
||||
}
|
||||
// From RFC 8216, section 4.4.2.2: If startOffsetUs is negative, it indicates the offset from
|
||||
// From RFC 8216bis, section 4.4.2.2: If startOffsetUs is negative, it indicates the offset from
|
||||
// the end of the playlist. If the absolute value exceeds the duration of the playlist, it
|
||||
// indicates the beginning (if negative) or the end (if positive) of the playlist.
|
||||
this.startOffsetUs =
|
||||
@ -644,7 +955,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
segments,
|
||||
trailingParts,
|
||||
serverControl,
|
||||
renditionReports);
|
||||
renditionReports,
|
||||
interstitials);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -675,6 +987,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
segments,
|
||||
trailingParts,
|
||||
serverControl,
|
||||
renditionReports);
|
||||
renditionReports,
|
||||
interstitials);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,11 @@ package androidx.media3.exoplayer.hls.playlist;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
import static androidx.media3.common.util.Util.msToUs;
|
||||
import static androidx.media3.common.util.Util.parseXsDateTime;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_ONCE;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_POST;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_PRE;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
@ -37,6 +42,7 @@ import androidx.media3.common.util.UriUtil;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry;
|
||||
import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry.VariantInfo;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.RenditionReport;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
@ -44,6 +50,7 @@ import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Rendition;
|
||||
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;
|
||||
@ -105,6 +112,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
private static final String TAG_SKIP = "#EXT-X-SKIP";
|
||||
private static final String TAG_PRELOAD_HINT = "#EXT-X-PRELOAD-HINT";
|
||||
private static final String TAG_RENDITION_REPORT = "#EXT-X-RENDITION-REPORT";
|
||||
private static final String TAG_DATERANGE = "#EXT-X-DATERANGE";
|
||||
|
||||
private static final String TYPE_AUDIO = "AUDIO";
|
||||
private static final String TYPE_VIDEO = "VIDEO";
|
||||
@ -222,8 +230,28 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
private static final Pattern REGEX_PRECISE = compileBooleanAttrPattern("PRECISE");
|
||||
private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
|
||||
private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
|
||||
private static final Pattern REGEX_ID = Pattern.compile("ID=\"(.+?)\"");
|
||||
private static final Pattern REGEX_CLASS = Pattern.compile("CLASS=\"(.+?)\"");
|
||||
private static final Pattern REGEX_START_DATE = Pattern.compile("START-DATE=\"(.+?)\"");
|
||||
private static final Pattern REGEX_CUE = Pattern.compile("CUE=\"(.+?)\"");
|
||||
private static final Pattern REGEX_END_DATE = Pattern.compile("END-DATE=\"(.+?)\"");
|
||||
private static final Pattern REGEX_PLANNED_DURATION =
|
||||
Pattern.compile("PLANNED-DURATION=([\\d\\.]+)\\b");
|
||||
private static final Pattern REGEX_END_ON_NEXT = compileBooleanAttrPattern("END-ON-NEXT");
|
||||
private static final Pattern REGEX_ASSET_URI = Pattern.compile("X-ASSET-URI=\"(.+?)\"");
|
||||
private static final Pattern REGEX_ASSET_LIST_URI = Pattern.compile("X-ASSET-LIST=\"(.+?)\"");
|
||||
private static final Pattern REGEX_RESUME_OFFSET =
|
||||
Pattern.compile("X-RESUME-OFFSET=(-?[\\d\\.]+)\\b");
|
||||
private static final Pattern REGEX_PLAYOUT_LIMIT =
|
||||
Pattern.compile("X-PLAYOUT-LIMIT=([\\d\\.]+)\\b");
|
||||
private static final Pattern REGEX_SNAP = Pattern.compile("X-SNAP=\"(.+?)\"");
|
||||
private static final Pattern REGEX_RESTRICT = Pattern.compile("X-RESTRICT=\"(.+?)\"");
|
||||
private static final Pattern REGEX_VARIABLE_REFERENCE =
|
||||
Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}");
|
||||
private static final Pattern REGEX_CLIENT_DEFINED_ATTRIBUTE_PREFIX =
|
||||
Pattern.compile("\\b(X-[A-Z0-9-]+)=");
|
||||
|
||||
private static final String DATERANGE_CLASS_INTERSTITIALS = "com.apple.hls.interstitial";
|
||||
|
||||
private final HlsMultivariantPlaylist multivariantPlaylist;
|
||||
@Nullable private final HlsMediaPlaylist previousMediaPlaylist;
|
||||
@ -652,6 +680,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
@Nullable Part preloadPart = null;
|
||||
List<RenditionReport> renditionReports = new ArrayList<>();
|
||||
List<String> tags = new ArrayList<>();
|
||||
List<Interstitial> interstitials = new ArrayList<>();
|
||||
|
||||
long segmentDurationUs = 0;
|
||||
String segmentTitle = "";
|
||||
@ -848,7 +877,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
} else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
|
||||
if (playlistStartTimeUs == 0) {
|
||||
long programDatetimeUs =
|
||||
Util.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
|
||||
msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
|
||||
playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
|
||||
}
|
||||
} else if (line.equals(TAG_GAP)) {
|
||||
@ -957,6 +986,149 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
if (partByteRangeLength != C.LENGTH_UNSET) {
|
||||
partByteRangeOffset += partByteRangeLength;
|
||||
}
|
||||
} else if (line.startsWith(TAG_DATERANGE)
|
||||
&& parseOptionalStringAttr(line, REGEX_CLASS, /* defaultValue= */ "", variableDefinitions)
|
||||
.equals(DATERANGE_CLASS_INTERSTITIALS)) {
|
||||
String id = parseStringAttr(line, REGEX_ID, variableDefinitions);
|
||||
@Nullable Uri assetUri = null;
|
||||
String assetUriString = parseOptionalStringAttr(line, REGEX_ASSET_URI, variableDefinitions);
|
||||
if (assetUriString != null) {
|
||||
assetUri = Uri.parse(assetUriString);
|
||||
}
|
||||
@Nullable Uri assetListUri = null;
|
||||
String assetListUriString =
|
||||
parseOptionalStringAttr(line, REGEX_ASSET_LIST_URI, variableDefinitions);
|
||||
if (assetListUriString != null) {
|
||||
assetListUri = Uri.parse(assetListUriString);
|
||||
}
|
||||
if ((assetUri == null && assetListUri == null)
|
||||
|| (assetUri != null && assetListUri != null)) {
|
||||
throw ParserException.createForMalformedManifest(
|
||||
"#EXT-X-DATARANGE: Exactly one of X-ASSET-URI or X-ASSET-LIST must be included",
|
||||
/* cause= */ null);
|
||||
}
|
||||
long startDateUnixUs =
|
||||
msToUs(parseXsDateTime(parseStringAttr(line, REGEX_START_DATE, variableDefinitions)));
|
||||
long endDateUnixUs = C.TIME_UNSET;
|
||||
@Nullable
|
||||
String endDateUnixMsString =
|
||||
parseOptionalStringAttr(line, REGEX_END_DATE, variableDefinitions);
|
||||
if (endDateUnixMsString != null) {
|
||||
endDateUnixUs = msToUs(parseXsDateTime(endDateUnixMsString));
|
||||
}
|
||||
List<@Interstitial.CueTriggerType String> cue = new ArrayList<>();
|
||||
String cueString = parseOptionalStringAttr(line, REGEX_CUE, variableDefinitions);
|
||||
if (cueString != null) {
|
||||
String[] identifiers = Util.split(/* value= */ cueString, /* regex= */ ",");
|
||||
for (String identifier : identifiers) {
|
||||
identifier = identifier.trim();
|
||||
switch (identifier) {
|
||||
case CUE_TRIGGER_ONCE:
|
||||
case CUE_TRIGGER_POST:
|
||||
case CUE_TRIGGER_PRE:
|
||||
cue.add(identifier);
|
||||
break;
|
||||
default:
|
||||
break; // ignore for forward compatibility
|
||||
}
|
||||
}
|
||||
}
|
||||
double durationSec = parseOptionalDoubleAttr(line, REGEX_ATTR_DURATION, -1.0d);
|
||||
long durationUs = C.TIME_UNSET;
|
||||
if (durationSec >= 0) {
|
||||
durationUs = (long) (durationSec * C.MICROS_PER_SECOND);
|
||||
}
|
||||
double plannedDurationSec = parseOptionalDoubleAttr(line, REGEX_PLANNED_DURATION, -1.0d);
|
||||
long plannedDurationUs = C.TIME_UNSET;
|
||||
if (plannedDurationSec >= 0) {
|
||||
plannedDurationUs = (long) (plannedDurationSec * C.MICROS_PER_SECOND);
|
||||
}
|
||||
boolean endOnNext = parseOptionalBooleanAttribute(line, REGEX_END_ON_NEXT, false);
|
||||
double resumeOffsetUsDouble =
|
||||
parseOptionalDoubleAttr(line, REGEX_RESUME_OFFSET, Double.MIN_VALUE);
|
||||
long resumeOffsetUs = C.TIME_UNSET;
|
||||
if (resumeOffsetUsDouble != Double.MIN_VALUE) {
|
||||
resumeOffsetUs = (long) (resumeOffsetUsDouble * C.MICROS_PER_SECOND);
|
||||
}
|
||||
double playoutLimitSec = parseOptionalDoubleAttr(line, REGEX_PLAYOUT_LIMIT, -1.0d);
|
||||
long playoutLimitUs = C.TIME_UNSET;
|
||||
if (playoutLimitSec >= 0) {
|
||||
playoutLimitUs = (long) (playoutLimitSec * C.MICROS_PER_SECOND);
|
||||
}
|
||||
List<@Interstitial.SnapType String> snapTypes = new ArrayList<>();
|
||||
String snapTypesString = parseOptionalStringAttr(line, REGEX_SNAP, variableDefinitions);
|
||||
if (snapTypesString != null) {
|
||||
String[] snapTypesSplit = Util.split(snapTypesString, ",");
|
||||
for (String snapType : snapTypesSplit) {
|
||||
snapType = snapType.trim();
|
||||
switch (snapType) {
|
||||
case Interstitial.SNAP_TYPE_IN:
|
||||
case Interstitial.SNAP_TYPE_OUT:
|
||||
snapTypes.add(snapType);
|
||||
break;
|
||||
default:
|
||||
break; // ignore for forward compatibility
|
||||
}
|
||||
}
|
||||
}
|
||||
List<@Interstitial.NavigationRestriction String> restrictions = new ArrayList<>();
|
||||
String restrictionsString =
|
||||
parseOptionalStringAttr(line, REGEX_RESTRICT, variableDefinitions);
|
||||
if (restrictionsString != null) {
|
||||
String[] restrictionsSplit = Util.split(restrictionsString, ",");
|
||||
for (String restriction : restrictionsSplit) {
|
||||
restriction = restriction.trim();
|
||||
switch (restriction) {
|
||||
case Interstitial.NAVIGATION_RESTRICTION_JUMP:
|
||||
case Interstitial.NAVIGATION_RESTRICTION_SKIP:
|
||||
restrictions.add(restriction);
|
||||
break;
|
||||
default:
|
||||
break; // ignore for forward compatibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImmutableList.Builder<HlsMediaPlaylist.ClientDefinedAttribute> clientDefinedAttributes =
|
||||
new ImmutableList.Builder<>();
|
||||
String attributes = line.substring("#EXT-X-DATERANGE:".length());
|
||||
Matcher matcher = REGEX_CLIENT_DEFINED_ATTRIBUTE_PREFIX.matcher(attributes);
|
||||
while (matcher.find()) {
|
||||
String attributePrefix = matcher.group();
|
||||
switch (attributePrefix) {
|
||||
case "X-ASSET-URI=": // fall through
|
||||
case "X-ASSET-LIST=": // fall through
|
||||
case "X-RESUME-OFFSET=": // fall through
|
||||
case "X-PLAYOUT-LIMIT=": // fall through
|
||||
case "X-SNAP=": // fall through
|
||||
case "X-RESTRICT=": // fall through
|
||||
// ignore interstitial attributes
|
||||
break;
|
||||
default:
|
||||
clientDefinedAttributes.add(
|
||||
parseClientDefinedAttribute(
|
||||
attributes,
|
||||
attributePrefix.substring(0, attributePrefix.length() - 1),
|
||||
variableDefinitions));
|
||||
break;
|
||||
}
|
||||
}
|
||||
interstitials.add(
|
||||
new Interstitial(
|
||||
id,
|
||||
assetUri,
|
||||
assetListUri,
|
||||
startDateUnixUs,
|
||||
endDateUnixUs,
|
||||
durationUs,
|
||||
plannedDurationUs,
|
||||
cue,
|
||||
endOnNext,
|
||||
resumeOffsetUs,
|
||||
playoutLimitUs,
|
||||
snapTypes,
|
||||
restrictions,
|
||||
clientDefinedAttributes.build()));
|
||||
} else if (!line.startsWith("#")) {
|
||||
@Nullable
|
||||
String segmentEncryptionIV =
|
||||
@ -1062,7 +1234,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
segments,
|
||||
trailingParts,
|
||||
serverControl,
|
||||
renditionReportMap);
|
||||
renditionReportMap,
|
||||
interstitials);
|
||||
}
|
||||
|
||||
private static DrmInitData getPlaylistProtectionSchemes(
|
||||
@ -1253,6 +1426,35 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static HlsMediaPlaylist.ClientDefinedAttribute parseClientDefinedAttribute(
|
||||
String attributes, String clientAttribute, Map<String, String> variableDefinitions)
|
||||
throws ParserException {
|
||||
String prefix = clientAttribute + "=";
|
||||
int index = attributes.indexOf(prefix);
|
||||
int valueBeginIndex = index + prefix.length();
|
||||
int valueBeginMaxLength = attributes.length() == valueBeginIndex + 1 ? 1 : 2;
|
||||
String valueBegin =
|
||||
attributes.substring(valueBeginIndex, valueBeginIndex + valueBeginMaxLength);
|
||||
if (valueBegin.startsWith("\"")) {
|
||||
// a quoted string value
|
||||
Pattern pattern = Pattern.compile(clientAttribute + "=\"(.+?)\"");
|
||||
String value = parseStringAttr(attributes, pattern, variableDefinitions);
|
||||
return new HlsMediaPlaylist.ClientDefinedAttribute(
|
||||
clientAttribute, value, HlsMediaPlaylist.ClientDefinedAttribute.TYPE_TEXT);
|
||||
} else if (valueBegin.equals("0x") || valueBegin.equals("0X")) {
|
||||
// a hexadecimal sequence value
|
||||
Pattern pattern = Pattern.compile(clientAttribute + "=(0[xX][A-F0-9]+)");
|
||||
String value = parseStringAttr(attributes, pattern, variableDefinitions);
|
||||
return new HlsMediaPlaylist.ClientDefinedAttribute(
|
||||
clientAttribute, value, HlsMediaPlaylist.ClientDefinedAttribute.TYPE_HEX_TEXT);
|
||||
} else {
|
||||
// a decimal-floating-point value
|
||||
Pattern pattern = Pattern.compile(clientAttribute + "=([\\d\\.]+)\\b");
|
||||
return new HlsMediaPlaylist.ClientDefinedAttribute(
|
||||
clientAttribute, parseDoubleAttr(attributes, pattern));
|
||||
}
|
||||
}
|
||||
|
||||
private static String replaceVariableReferences(
|
||||
String string, Map<String, String> variableDefinitions) {
|
||||
Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string);
|
||||
|
@ -15,7 +15,16 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import static androidx.media3.common.util.Util.msToUs;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_ONCE;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_POST;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_PRE;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.NAVIGATION_RESTRICTION_JUMP;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.NAVIGATION_RESTRICTION_SKIP;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_IN;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_OUT;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.net.Uri;
|
||||
@ -24,13 +33,18 @@ import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
import androidx.media3.extractor.mp4.PsshAtomUtil;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -1104,6 +1118,405 @@ public class HlsMediaPlaylistParserTest {
|
||||
assertThat(report0.lastPartIndex).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMediaPlaylist_withInterstitialDateRanges() throws IOException, ParseException {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "ID=\"ad1\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
|
||||
+ "END-DATE=\"2020-01-02T21:55:59.000Z\","
|
||||
+ "DURATION=15.0,"
|
||||
+ "PLANNED-DURATION=15.13,"
|
||||
+ "CUE=\" PRE \","
|
||||
+ "END-ON-NEXT=YES, "
|
||||
+ "X-ASSET-URI=\"http://example.com/ad1.m3u8\","
|
||||
+ "X-RESUME-OFFSET=-1.1, "
|
||||
+ "X-PLAYOUT-LIMIT=15.112, "
|
||||
+ "X-SNAP=\" IN , OUT \", "
|
||||
+ "X-RESTRICT=\"SKIP , JUMP\","
|
||||
+ "X-GOOGLE-TEST-TEXT=\"Value at the end\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "X-GOOGLE-TEST-HEX=0XAB10A,"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T22:55:44.000Z\","
|
||||
+ "END-DATE=\"2020-01-02T22:55:49.120Z\","
|
||||
+ "DURATION=5.12,"
|
||||
+ "PLANNED-DURATION=5.11,"
|
||||
+ "CUE=\" POST, ONCE\","
|
||||
+ "X-GOOGLE-TEST-DOUBLE1=12.123,"
|
||||
+ "X-ASSET-LIST=\"http://example.com/ad2-assets.json\""
|
||||
+ "X-GOOGLE-TEST-DOUBLE2=1\n";
|
||||
long playlistStartTimeUs = msToUs(Util.parseXsDateTime("2020-01-02T21:55:40.000Z"));
|
||||
Interstitial interstitial1 =
|
||||
new Interstitial(
|
||||
/* id= */ "ad1",
|
||||
/* assetUri= */ Uri.parse("http://example.com/ad1.m3u8"),
|
||||
/* assetListUri= */ null,
|
||||
/* startDateUnixUs= */ playlistStartTimeUs + 4_000_000L,
|
||||
/* endDateUnixUs= */ playlistStartTimeUs + 4_000_000L + 15_000_000L,
|
||||
/* durationUs= */ 15_000_000L,
|
||||
/* plannedDurationUs= */ 15_130_000L,
|
||||
/* cue= */ ImmutableList.of(CUE_TRIGGER_PRE),
|
||||
/* endOnNext= */ true,
|
||||
/* resumeOffsetUs= */ -1_100_000L,
|
||||
/* playoutLimitUs= */ 15_112_000L,
|
||||
ImmutableList.of(SNAP_TYPE_IN, SNAP_TYPE_OUT),
|
||||
ImmutableList.of(NAVIGATION_RESTRICTION_SKIP, NAVIGATION_RESTRICTION_JUMP),
|
||||
/* clientDefinedAttributes= */ ImmutableList.of(
|
||||
new HlsMediaPlaylist.ClientDefinedAttribute(
|
||||
"X-GOOGLE-TEST-TEXT",
|
||||
"Value at the end",
|
||||
HlsMediaPlaylist.ClientDefinedAttribute.TYPE_TEXT)));
|
||||
Interstitial interstitial2 =
|
||||
new Interstitial(
|
||||
/* id= */ "ad2",
|
||||
/* assetUri= */ null,
|
||||
/* assetListUri= */ Uri.parse("http://example.com/ad2-assets.json"),
|
||||
/* startDateUnixUs= */ playlistStartTimeUs + 4_000_000L + (60L * 60L * 1_000_000L),
|
||||
/* endDateUnixUs= */ playlistStartTimeUs
|
||||
+ 4_000_000L
|
||||
+ 5_120_000L
|
||||
+ (60L * 60L * 1_000_000L),
|
||||
/* durationUs= */ 5_120_000L,
|
||||
/* plannedDurationUs= */ 5_110_000L,
|
||||
ImmutableList.of(CUE_TRIGGER_POST, CUE_TRIGGER_ONCE),
|
||||
/* endOnNext= */ false,
|
||||
/* resumeOffsetUs= */ C.TIME_UNSET,
|
||||
/* playoutLimitUs= */ C.TIME_UNSET,
|
||||
/* snapTypes= */ ImmutableList.of(),
|
||||
/* restrictions= */ ImmutableList.of(),
|
||||
/* clientDefinedAttributes= */ ImmutableList.of(
|
||||
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)));
|
||||
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
|
||||
|
||||
HlsMediaPlaylist playlist =
|
||||
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
|
||||
assertThat(playlist.interstitials).containsExactly(interstitial1, interstitial2).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMediaPlaylist_withInterstitialsVodWithAssetUriOnly_correctlyParseAllAttributes()
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse("http://example.com/interstitials_vod_asset_uri.m3u8");
|
||||
InputStream inputStream =
|
||||
TestUtil.getInputStream(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
"media/hls/interstitials/interstitials_vod_asset_uri.m3u8");
|
||||
Interstitial interstitial1 =
|
||||
new Interstitial(
|
||||
"preroll",
|
||||
/* assetUri= */ Uri.parse("https://example.com/0.m3u8?parse=1"),
|
||||
/* assetListUri= */ null,
|
||||
/* startDateUnixUs= */ msToUs(Util.parseXsDateTime("2024-11-11T04:35:19.353-08:00")),
|
||||
/* endDateUnixUs= */ C.TIME_UNSET,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
/* plannedDurationUs= */ C.TIME_UNSET,
|
||||
/* cue= */ ImmutableList.of("PRE"),
|
||||
/* endOnNext= */ false,
|
||||
/* resumeOffsetUs= */ 0L,
|
||||
/* playoutLimitUs= */ C.TIME_UNSET,
|
||||
/* snapTypes= */ ImmutableList.of(),
|
||||
/* restrictions= */ ImmutableList.of("JUMP", "SKIP"),
|
||||
/* clientDefinedAttributes= */ ImmutableList.of());
|
||||
Interstitial interstitial2 =
|
||||
new Interstitial(
|
||||
"midroll-1",
|
||||
/* assetUri= */ Uri.parse("https://example.com/1.m3u8?parse=1"),
|
||||
/* assetListUri= */ null,
|
||||
/* startDateUnixUs= */ msToUs(Util.parseXsDateTime("2024-11-11T04:35:34.353-08:00")),
|
||||
/* endDateUnixUs= */ C.TIME_UNSET,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
/* plannedDurationUs= */ C.TIME_UNSET,
|
||||
/* cue= */ ImmutableList.of(),
|
||||
/* endOnNext= */ false,
|
||||
/* resumeOffsetUs= */ 0L,
|
||||
/* playoutLimitUs= */ C.TIME_UNSET,
|
||||
/* snapTypes= */ ImmutableList.of(),
|
||||
/* restrictions= */ ImmutableList.of("JUMP", "SKIP"),
|
||||
/* clientDefinedAttributes= */ ImmutableList.of());
|
||||
Interstitial interstitial3 =
|
||||
new Interstitial(
|
||||
"postroll",
|
||||
/* assetUri= */ Uri.parse("https://example.com/2.m3u8?parse=1"),
|
||||
/* assetListUri= */ null,
|
||||
/* startDateUnixUs= */ msToUs(Util.parseXsDateTime("2024-11-11T04:36:31.353-08:00")),
|
||||
/* endDateUnixUs= */ C.TIME_UNSET,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
/* plannedDurationUs= */ C.TIME_UNSET,
|
||||
/* cue= */ ImmutableList.of("POST"),
|
||||
/* endOnNext= */ false,
|
||||
/* resumeOffsetUs= */ 0L,
|
||||
/* playoutLimitUs= */ C.TIME_UNSET,
|
||||
/* snapTypes= */ ImmutableList.of(),
|
||||
/* restrictions= */ ImmutableList.of(
|
||||
NAVIGATION_RESTRICTION_JUMP, NAVIGATION_RESTRICTION_SKIP),
|
||||
/* clientDefinedAttributes= */ ImmutableList.of());
|
||||
|
||||
HlsMediaPlaylist playlist =
|
||||
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
|
||||
assertThat(playlist.interstitials)
|
||||
.containsExactly(interstitial1, interstitial2, interstitial3)
|
||||
.inOrder();
|
||||
assertThat(playlist.startTimeUs)
|
||||
.isEqualTo(msToUs(Util.parseXsDateTime("2024-11-11T04:35:19.353-08:00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMediaPlaylist_withInterstitialWithoutId_throwsParserException() {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "X-ASSET-URI=\"http://example.com/ad1.m3u8\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\"\n";
|
||||
HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser();
|
||||
|
||||
assertThrows(
|
||||
ParserException.class,
|
||||
() ->
|
||||
hlsPlaylistParser.parse(
|
||||
playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMediaPlaylist_withDateRangeWithUnknownClass_dateRangeIgnored()
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"some.other.class\","
|
||||
+ "X-ASSET-URI=\"http://example.com/ad1.m3u8\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\"\n";
|
||||
HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser();
|
||||
|
||||
HlsMediaPlaylist playlist =
|
||||
(HlsMediaPlaylist)
|
||||
hlsPlaylistParser.parse(
|
||||
playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)));
|
||||
|
||||
assertThat(playlist.interstitials).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMediaPlaylist_withDateRangeWithMissingClass_dateRangeIgnored()
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "X-ASSET-URI=\"http://example.com/ad1.m3u8\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\"\n";
|
||||
HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser();
|
||||
|
||||
HlsMediaPlaylist playlist =
|
||||
(HlsMediaPlaylist)
|
||||
hlsPlaylistParser.parse(
|
||||
playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)));
|
||||
|
||||
assertThat(playlist.interstitials).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMediaPlaylist_withInterstitialWithoutStartDate_throwsParserException() {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "X-ASSET-LIST=\"http://example.com/ad2-assets.json\"\n";
|
||||
HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser();
|
||||
|
||||
assertThrows(
|
||||
ParserException.class,
|
||||
() ->
|
||||
hlsPlaylistParser.parse(
|
||||
playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseMediaPlaylist_withInterstitialWithAssetUriAndList_throwsParserException() {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media.m3u8\","
|
||||
+ "X-ASSET-LIST=\"http://example.com/ad2-assets.json\"\n";
|
||||
HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser();
|
||||
|
||||
assertThrows(
|
||||
ParserException.class,
|
||||
() ->
|
||||
hlsPlaylistParser.parse(
|
||||
playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
parseMediaPlaylist_withInterstitialWithNeitherAssetUriNorAssetList_throwsParserException() {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\"\n";
|
||||
HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser();
|
||||
|
||||
assertThrows(
|
||||
ParserException.class,
|
||||
() ->
|
||||
hlsPlaylistParser.parse(
|
||||
playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
parseMediaPlaylist_withInterstitialWithUnknownRestrictions_ignoredForForwardCompatibility()
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "X-RESTRICT=\"SKIP , FLY , ROW , JUMP\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media.m3u8\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\"\n";
|
||||
|
||||
HlsMediaPlaylist playlist =
|
||||
(HlsMediaPlaylist)
|
||||
new HlsPlaylistParser()
|
||||
.parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)));
|
||||
|
||||
assertThat(playlist.interstitials.get(0).restrictions)
|
||||
.containsExactly(NAVIGATION_RESTRICTION_SKIP, NAVIGATION_RESTRICTION_JUMP)
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
parseMediaPlaylist_withInterstitialWithUnknownCueIdentifier_ignoredForForwardCompatibility()
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "X-CUE=\"ALONE , PRE , INBETWEEN , BEYOND\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media.m3u8\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\"\n";
|
||||
|
||||
HlsMediaPlaylist playlist =
|
||||
(HlsMediaPlaylist)
|
||||
new HlsPlaylistParser()
|
||||
.parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)));
|
||||
|
||||
assertThat(playlist.interstitials.get(0).cue).containsExactly(CUE_TRIGGER_PRE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
parseMediaPlaylist_withInterstitialWithUnknownSnapTypes_ignoredForForwardCompatibility()
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "X-SNAP=\" FOR , OUT , AT\", "
|
||||
+ "X-ASSET-URI=\"http://example.com/media.m3u8\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\"\n";
|
||||
|
||||
HlsMediaPlaylist playlist =
|
||||
(HlsMediaPlaylist)
|
||||
new HlsPlaylistParser()
|
||||
.parse(playlistUri, new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)));
|
||||
|
||||
assertThat(playlist.interstitials.get(0).snapTypes).containsExactly(SNAP_TYPE_OUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleExtXKeysForSingleSegment() throws Exception {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
|
@ -0,0 +1,47 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXT-X-BITRATE:4612
|
||||
#EXT-X-PROGRAM-DATE-TIME:2024-11-11T04:35:19.353-08:00
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence0.ts
|
||||
#EXT-X-BITRATE:4609
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence1.ts
|
||||
#EXT-X-BITRATE:4609
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence2.ts
|
||||
#EXT-X-BITRATE:4611
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence3.ts
|
||||
#EXT-X-BITRATE:4611
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence4.ts
|
||||
#EXT-X-BITRATE:4609
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence5.ts
|
||||
#EXT-X-BITRATE:4610
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence6.ts
|
||||
#EXT-X-BITRATE:4610
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence7.ts
|
||||
#EXT-X-BITRATE:4611
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence8.ts
|
||||
#EXT-X-BITRATE:4609
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence9.ts
|
||||
#EXT-X-BITRATE:4609
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence10.ts
|
||||
#EXT-X-BITRATE:4611
|
||||
#EXTINF:6,
|
||||
https://example.com/fileSequence11.ts
|
||||
#EXT-X-DATERANGE:ID="preroll",START-DATE="2024-11-11T04:35:19.353-08:00",CLASS="com.apple.hls.interstitial",CUE="PRE",X-ASSET-URI="https://example.com/0.m3u8?parse=1",X-RESUME-OFFSET=0,X-RESTRICT="JUMP,SKIP"
|
||||
#EXT-X-DATERANGE:ID="midroll-1",START-DATE="2024-11-11T04:35:34.353-08:00",CLASS="com.apple.hls.interstitial",X-ASSET-URI="https://example.com/1.m3u8?parse=1",X-RESUME-OFFSET=0,X-RESTRICT="JUMP,SKIP"
|
||||
#EXT-X-DATERANGE:ID="postroll",START-DATE="2024-11-11T04:36:31.353-08:00",CLASS="com.apple.hls.interstitial",CUE="POST",X-ASSET-URI="https://example.com/2.m3u8?parse=1",X-RESUME-OFFSET=0,X-RESTRICT="JUMP,SKIP"
|
||||
#EXT-X-ENDLIST
|
Loading…
x
Reference in New Issue
Block a user