Parse interstitials into HLS media playlist

PiperOrigin-RevId: 696454575
This commit is contained in:
bachinger 2024-11-14 03:05:47 -08:00 committed by Copybara-Service
parent 16a15b94ca
commit 6453054878
4 changed files with 982 additions and 7 deletions

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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");

View File

@ -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