Rename HLS master playlist to multivariant playlist

The spec renamed this type of playlist in the latest revision
to use a more inclusive technical term (see
https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-10)

PiperOrigin-RevId: 417560274
This commit is contained in:
tonihei 2021-12-21 08:50:44 +00:00
parent 691f9c50cf
commit 40d9838315
28 changed files with 676 additions and 556 deletions

View File

@ -237,11 +237,11 @@
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
}, },
{ {
"name": "Apple master playlist advanced (TS)", "name": "Apple multivariant playlist advanced (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
}, },
{ {
"name": "Apple master playlist advanced (FMP4)", "name": "Apple multivariant playlist advanced (FMP4)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
}, },
{ {

View File

@ -629,7 +629,8 @@ public final class Format implements Bundleable {
* <ul> * <ul>
* <li>DASH representations: Always {@link Format#NO_VALUE}. * <li>DASH representations: Always {@link Format#NO_VALUE}.
* <li>HLS variants: The {@code AVERAGE-BANDWIDTH} attribute defined on the corresponding {@code * <li>HLS variants: The {@code AVERAGE-BANDWIDTH} attribute defined on the corresponding {@code
* EXT-X-STREAM-INF} tag in the master playlist, or {@link Format#NO_VALUE} if not present. * EXT-X-STREAM-INF} tag in the multivariant playlist, or {@link Format#NO_VALUE} if not
* present.
* <li>SmoothStreaming track elements: The {@code Bitrate} attribute defined on the * <li>SmoothStreaming track elements: The {@code Bitrate} attribute defined on the
* corresponding {@code TrackElement} in the manifest, or {@link Format#NO_VALUE} if not * corresponding {@code TrackElement} in the manifest, or {@link Format#NO_VALUE} if not
* present. * present.

View File

@ -42,20 +42,20 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
private static final PositionHolder POSITION_HOLDER = new PositionHolder(); private static final PositionHolder POSITION_HOLDER = new PositionHolder();
@VisibleForTesting /* package */ final Extractor extractor; @VisibleForTesting /* package */ final Extractor extractor;
private final Format masterPlaylistFormat; private final Format multivariantPlaylistFormat;
private final TimestampAdjuster timestampAdjuster; private final TimestampAdjuster timestampAdjuster;
/** /**
* Creates a new instance. * Creates a new instance.
* *
* @param extractor The underlying {@link Extractor}. * @param extractor The underlying {@link Extractor}.
* @param masterPlaylistFormat The {@link Format} obtained from the master playlist. * @param multivariantPlaylistFormat The {@link Format} obtained from the multivariant playlist.
* @param timestampAdjuster A {@link TimestampAdjuster} to adjust sample timestamps. * @param timestampAdjuster A {@link TimestampAdjuster} to adjust sample timestamps.
*/ */
public BundledHlsMediaChunkExtractor( public BundledHlsMediaChunkExtractor(
Extractor extractor, Format masterPlaylistFormat, TimestampAdjuster timestampAdjuster) { Extractor extractor, Format multivariantPlaylistFormat, TimestampAdjuster timestampAdjuster) {
this.extractor = extractor; this.extractor = extractor;
this.masterPlaylistFormat = masterPlaylistFormat; this.multivariantPlaylistFormat = multivariantPlaylistFormat;
this.timestampAdjuster = timestampAdjuster; this.timestampAdjuster = timestampAdjuster;
} }
@ -87,7 +87,8 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
Assertions.checkState(!isReusable()); Assertions.checkState(!isReusable());
Extractor newExtractorInstance; Extractor newExtractorInstance;
if (extractor instanceof WebvttExtractor) { if (extractor instanceof WebvttExtractor) {
newExtractorInstance = new WebvttExtractor(masterPlaylistFormat.language, timestampAdjuster); newExtractorInstance =
new WebvttExtractor(multivariantPlaylistFormat.language, timestampAdjuster);
} else if (extractor instanceof AdtsExtractor) { } else if (extractor instanceof AdtsExtractor) {
newExtractorInstance = new AdtsExtractor(); newExtractorInstance = new AdtsExtractor();
} else if (extractor instanceof Ac3Extractor) { } else if (extractor instanceof Ac3Extractor) {
@ -101,7 +102,7 @@ public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtract
"Unexpected extractor type for recreation: " + extractor.getClass().getSimpleName()); "Unexpected extractor type for recreation: " + extractor.getClass().getSimpleName());
} }
return new BundledHlsMediaChunkExtractor( return new BundledHlsMediaChunkExtractor(
newExtractorInstance, masterPlaylistFormat, timestampAdjuster); newExtractorInstance, multivariantPlaylistFormat, timestampAdjuster);
} }
@Override @Override

View File

@ -81,8 +81,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
* DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code * DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code
* payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}. * payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}.
* @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should * @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should
* expose a CEA-608 track should the master playlist contain no Closed Captions declarations. * expose a CEA-608 track should the multivariant playlist contain no Closed Captions
* If the master playlist contains any Closed Captions declarations, this flag is ignored. * declarations. If the multivariant playlist contains any Closed Captions declarations, this
* flag is ignored.
*/ */
public DefaultHlsExtractorFactory( public DefaultHlsExtractorFactory(
int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) { int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) {

View File

@ -154,7 +154,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* {@link HlsChunkSource}s are used for a single playback, they should all share the same * {@link HlsChunkSource}s are used for a single playback, they should all share the same
* provider. * provider.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist. * information is available in the multivariant playlist.
*/ */
public HlsChunkSource( public HlsChunkSource(
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
@ -877,8 +877,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public InitializationTrackSelection(TrackGroup group, int[] tracks) { public InitializationTrackSelection(TrackGroup group, int[] tracks) {
super(group, tracks); super(group, tracks);
// The initially selected index corresponds to the first EXT-X-STREAMINF tag in the master // The initially selected index corresponds to the first EXT-X-STREAMINF tag in the
// playlist. // multivariant playlist.
selectedIndex = indexOf(group.getFormat(tracks[0])); selectedIndex = indexOf(group.getFormat(tracks[0]));
} }

View File

@ -40,7 +40,7 @@ public interface HlsExtractorFactory {
* @param uri The URI of the media chunk. * @param uri The URI of the media chunk.
* @param format A {@link Format} associated with the chunk to extract. * @param format A {@link Format} associated with the chunk to extract.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist. * information is available in the multivariant playlist.
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
* @param responseHeaders The HTTP response headers associated with the media segment or * @param responseHeaders The HTTP response headers associated with the media segment or
* initialization section to extract. * initialization section to extract.

View File

@ -18,22 +18,43 @@ package androidx.media3.exoplayer.hls;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist;
/** Holds a master playlist along with a snapshot of one of its media playlists. */ /** Holds a multivariant playlist along with a snapshot of one of its media playlists. */
@UnstableApi @UnstableApi
public final class HlsManifest { public final class HlsManifest {
/** The master playlist of an HLS stream. */ /** @deprecated Use {@link #multivariantPlaylist} instead. */
@Deprecated
@SuppressWarnings("deprecation") // Keeping deprecated field with deprecated class.
public final HlsMasterPlaylist masterPlaylist; public final HlsMasterPlaylist masterPlaylist;
/** A snapshot of a media playlist referred to by {@link #masterPlaylist}. */ /** The multivariant playlist of an HLS stream. */
public final HlsMultivariantPlaylist multivariantPlaylist;
/** A snapshot of a media playlist referred to by {@link #multivariantPlaylist}. */
public final HlsMediaPlaylist mediaPlaylist; public final HlsMediaPlaylist mediaPlaylist;
/** /**
* @param masterPlaylist The master playlist. * @param multivariantPlaylist The multivariant playlist.
* @param mediaPlaylist The media playlist. * @param mediaPlaylist The media playlist.
*/ */
HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) { @SuppressWarnings("deprecation") // Intentionally creating deprecated hlsMasterPlaylist field.
this.masterPlaylist = masterPlaylist; /* package */ HlsManifest(
HlsMultivariantPlaylist multivariantPlaylist, HlsMediaPlaylist mediaPlaylist) {
this.multivariantPlaylist = multivariantPlaylist;
this.mediaPlaylist = mediaPlaylist; this.mediaPlaylist = mediaPlaylist;
this.masterPlaylist =
new HlsMasterPlaylist(
multivariantPlaylist.baseUri,
multivariantPlaylist.tags,
multivariantPlaylist.variants,
multivariantPlaylist.videos,
multivariantPlaylist.audios,
multivariantPlaylist.subtitles,
multivariantPlaylist.closedCaptions,
multivariantPlaylist.muxedAudioFormat,
multivariantPlaylist.muxedCaptionFormats,
multivariantPlaylist.hasIndependentSegments,
multivariantPlaylist.variableDefinitions,
multivariantPlaylist.sessionKeyDrmInitData);
} }
} }

View File

@ -64,7 +64,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param segmentBaseHolder The segment holder. * @param segmentBaseHolder The segment holder.
* @param playlistUrl The url of the playlist from which this chunk was obtained. * @param playlistUrl The url of the playlist from which this chunk was obtained.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the master playlist. * information is available in the multivariant playlist.
* @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionReason See {@link #trackSelectionReason}.
* @param trackSelectionData See {@link #trackSelectionData}. * @param trackSelectionData See {@link #trackSelectionData}.
* @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.

View File

@ -36,9 +36,9 @@ import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Rendition; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Rendition;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Variant; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker; import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker;
import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory; import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
@ -180,17 +180,18 @@ public final class HlsMediaPeriod
return Assertions.checkNotNull(trackGroups); return Assertions.checkNotNull(trackGroups);
} }
// TODO: When the master playlist does not de-duplicate variants by URL and allows Renditions with // TODO: When the multivariant playlist does not de-duplicate variants by URL and allows
// null URLs, this method must be updated to calculate stream keys that are compatible with those // Renditions with null URLs, this method must be updated to calculate stream keys that are
// that may already be persisted for offline. // compatible with those that may already be persisted for offline.
@Override @Override
public List<StreamKey> getStreamKeys(List<ExoTrackSelection> trackSelections) { public List<StreamKey> getStreamKeys(List<ExoTrackSelection> trackSelections) {
// See HlsMasterPlaylist.copy for interpretation of StreamKeys. // See HlsMultivariantPlaylist.copy for interpretation of StreamKeys.
HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); HlsMultivariantPlaylist multivariantPlaylist =
boolean hasVariants = !masterPlaylist.variants.isEmpty(); Assertions.checkNotNull(playlistTracker.getMultivariantPlaylist());
boolean hasVariants = !multivariantPlaylist.variants.isEmpty();
int audioWrapperOffset = hasVariants ? 1 : 0; int audioWrapperOffset = hasVariants ? 1 : 0;
// Subtitle sample stream wrappers are held last. // Subtitle sample stream wrappers are held last.
int subtitleWrapperOffset = sampleStreamWrappers.length - masterPlaylist.subtitles.size(); int subtitleWrapperOffset = sampleStreamWrappers.length - multivariantPlaylist.subtitles.size();
TrackGroupArray mainWrapperTrackGroups; TrackGroupArray mainWrapperTrackGroups;
int mainWrapperPrimaryGroupIndex; int mainWrapperPrimaryGroupIndex;
@ -218,7 +219,8 @@ public final class HlsMediaPeriod
hasPrimaryTrackGroupSelection = true; hasPrimaryTrackGroupSelection = true;
for (int i = 0; i < trackSelection.length(); i++) { for (int i = 0; i < trackSelection.length(); i++) {
int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)]; int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)];
streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); streamKeys.add(
new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, variantIndex));
} }
} else { } else {
// Embedded group in main wrapper. // Embedded group in main wrapper.
@ -232,8 +234,8 @@ public final class HlsMediaPeriod
if (selectedTrackGroupIndex != C.INDEX_UNSET) { if (selectedTrackGroupIndex != C.INDEX_UNSET) {
int groupIndexType = int groupIndexType =
i < subtitleWrapperOffset i < subtitleWrapperOffset
? HlsMasterPlaylist.GROUP_INDEX_AUDIO ? HlsMultivariantPlaylist.GROUP_INDEX_AUDIO
: HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; : HlsMultivariantPlaylist.GROUP_INDEX_SUBTITLE;
int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i]; int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i];
for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) { for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) {
int renditionIndex = int renditionIndex =
@ -249,16 +251,18 @@ public final class HlsMediaPeriod
// A track selection includes a variant-embedded track, but no variant is added yet. We use // A track selection includes a variant-embedded track, but no variant is added yet. We use
// the valid variant with the lowest bitrate to reduce overhead. // the valid variant with the lowest bitrate to reduce overhead.
int lowestBitrateIndex = mainWrapperVariantIndices[0]; int lowestBitrateIndex = mainWrapperVariantIndices[0];
int lowestBitrate = masterPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate; int lowestBitrate =
multivariantPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate;
for (int i = 1; i < mainWrapperVariantIndices.length; i++) { for (int i = 1; i < mainWrapperVariantIndices.length; i++) {
int variantBitrate = int variantBitrate =
masterPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate; multivariantPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate;
if (variantBitrate < lowestBitrate) { if (variantBitrate < lowestBitrate) {
lowestBitrate = variantBitrate; lowestBitrate = variantBitrate;
lowestBitrateIndex = mainWrapperVariantIndices[i]; lowestBitrateIndex = mainWrapperVariantIndices[i];
} }
} }
streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex)); streamKeys.add(
new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex));
} }
return streamKeys; return streamKeys;
} }
@ -491,15 +495,16 @@ public final class HlsMediaPeriod
// Internal methods. // Internal methods.
private void buildAndPrepareSampleStreamWrappers(long positionUs) { private void buildAndPrepareSampleStreamWrappers(long positionUs) {
HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); HlsMultivariantPlaylist multivariantPlaylist =
Assertions.checkNotNull(playlistTracker.getMultivariantPlaylist());
Map<String, DrmInitData> overridingDrmInitData = Map<String, DrmInitData> overridingDrmInitData =
useSessionKeys useSessionKeys
? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData) ? deriveOverridingDrmInitData(multivariantPlaylist.sessionKeyDrmInitData)
: Collections.emptyMap(); : Collections.emptyMap();
boolean hasVariants = !masterPlaylist.variants.isEmpty(); boolean hasVariants = !multivariantPlaylist.variants.isEmpty();
List<Rendition> audioRenditions = masterPlaylist.audios; List<Rendition> audioRenditions = multivariantPlaylist.audios;
List<Rendition> subtitleRenditions = masterPlaylist.subtitles; List<Rendition> subtitleRenditions = multivariantPlaylist.subtitles;
pendingPrepareCount = 0; pendingPrepareCount = 0;
ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>(); ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();
@ -507,7 +512,7 @@ public final class HlsMediaPeriod
if (hasVariants) { if (hasVariants) {
buildAndPrepareMainSampleStreamWrapper( buildAndPrepareMainSampleStreamWrapper(
masterPlaylist, multivariantPlaylist,
positionUs, positionUs,
sampleStreamWrappers, sampleStreamWrappers,
manifestUrlIndicesPerWrapper, manifestUrlIndicesPerWrapper,
@ -525,7 +530,8 @@ public final class HlsMediaPeriod
audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size(); audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size();
// Subtitle stream wrappers. We can always use master playlist information to prepare these. // Subtitle stream wrappers. We can always use multivariant playlist information to prepare
// these.
for (int i = 0; i < subtitleRenditions.size(); i++) { for (int i = 0; i < subtitleRenditions.size(); i++) {
Rendition subtitleRendition = subtitleRenditions.get(i); Rendition subtitleRendition = subtitleRenditions.get(i);
String sampleStreamWrapperUid = "subtitle:" + i + ":" + subtitleRendition.name; String sampleStreamWrapperUid = "subtitle:" + i + ":" + subtitleRendition.name;
@ -541,7 +547,7 @@ public final class HlsMediaPeriod
positionUs); positionUs);
manifestUrlIndicesPerWrapper.add(new int[] {i}); manifestUrlIndicesPerWrapper.add(new int[] {i});
sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrappers.add(sampleStreamWrapper);
sampleStreamWrapper.prepareWithMasterPlaylistInfo( sampleStreamWrapper.prepareWithMultivariantPlaylistInfo(
new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, subtitleRendition.format)}, new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, subtitleRendition.format)},
/* primaryTrackGroupIndex= */ 0); /* primaryTrackGroupIndex= */ 0);
} }
@ -562,8 +568,8 @@ public final class HlsMediaPeriod
* This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}. * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}.
* *
* <p>The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It * <p>The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It
* provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive * provides {@link SampleStream}s for the variant urls in the multivariant playlist. It may be
* and may contain multiple muxed tracks. * adaptive and may contain multiple muxed tracks.
* *
* <p>If chunkless preparation is allowed, the media period will try preparation without segment * <p>If chunkless preparation is allowed, the media period will try preparation without segment
* downloads. This is only possible if variants contain the CODECS attribute. If not, traditional * downloads. This is only possible if variants contain the CODECS attribute. If not, traditional
@ -572,13 +578,13 @@ public final class HlsMediaPeriod
* *
* <ul> * <ul>
* <li>A muxed audio track will be exposed if the codecs list contain an audio entry and the * <li>A muxed audio track will be exposed if the codecs list contain an audio entry and the
* master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not * multivariant playlist either contains an EXT-X-MEDIA tag without the URI attribute or
* contain any EXT-X-MEDIA tag. * does not contain any EXT-X-MEDIA tag.
* <li>Closed captions will only be exposed if they are declared by the master playlist. * <li>Closed captions will only be exposed if they are declared by the multivariant playlist.
* <li>An ID3 track is exposed preemptively, in case the segments contain an ID3 track. * <li>An ID3 track is exposed preemptively, in case the segments contain an ID3 track.
* </ul> * </ul>
* *
* @param masterPlaylist The HLS master playlist. * @param multivariantPlaylist The HLS multivariant playlist.
* @param positionUs If preparation requires any chunk downloads, the position in microseconds at * @param positionUs If preparation requires any chunk downloads, the position in microseconds at
* which downloading should start. Ignored otherwise. * which downloading should start. Ignored otherwise.
* @param sampleStreamWrappers List to which the built main sample stream wrapper should be added. * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added.
@ -587,16 +593,16 @@ public final class HlsMediaPeriod
* (i.e. {@link DrmInitData#schemeType}). * (i.e. {@link DrmInitData#schemeType}).
*/ */
private void buildAndPrepareMainSampleStreamWrapper( private void buildAndPrepareMainSampleStreamWrapper(
HlsMasterPlaylist masterPlaylist, HlsMultivariantPlaylist multivariantPlaylist,
long positionUs, long positionUs,
List<HlsSampleStreamWrapper> sampleStreamWrappers, List<HlsSampleStreamWrapper> sampleStreamWrappers,
List<int[]> manifestUrlIndicesPerWrapper, List<int[]> manifestUrlIndicesPerWrapper,
Map<String, DrmInitData> overridingDrmInitData) { Map<String, DrmInitData> overridingDrmInitData) {
int[] variantTypes = new int[masterPlaylist.variants.size()]; int[] variantTypes = new int[multivariantPlaylist.variants.size()];
int videoVariantCount = 0; int videoVariantCount = 0;
int audioVariantCount = 0; int audioVariantCount = 0;
for (int i = 0; i < masterPlaylist.variants.size(); i++) { for (int i = 0; i < multivariantPlaylist.variants.size(); i++) {
Variant variant = masterPlaylist.variants.get(i); Variant variant = multivariantPlaylist.variants.get(i);
Format format = variant.format; Format format = variant.format;
if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) { if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) {
variantTypes[i] = C.TRACK_TYPE_VIDEO; variantTypes[i] = C.TRACK_TYPE_VIDEO;
@ -613,8 +619,8 @@ public final class HlsMediaPeriod
int selectedVariantsCount = variantTypes.length; int selectedVariantsCount = variantTypes.length;
if (videoVariantCount > 0) { if (videoVariantCount > 0) {
// We've identified some variants as definitely containing video. Assume variants within the // We've identified some variants as definitely containing video. Assume variants within the
// master playlist are marked consistently, and hence that we have the full set. Filter out // multivariant playlist are marked consistently, and hence that we have the full set. Filter
// any other variants, which are likely to be audio only. // out any other variants, which are likely to be audio only.
useVideoVariantsOnly = true; useVideoVariantsOnly = true;
selectedVariantsCount = videoVariantCount; selectedVariantsCount = videoVariantCount;
} else if (audioVariantCount < variantTypes.length) { } else if (audioVariantCount < variantTypes.length) {
@ -627,10 +633,10 @@ public final class HlsMediaPeriod
Format[] selectedPlaylistFormats = new Format[selectedVariantsCount]; Format[] selectedPlaylistFormats = new Format[selectedVariantsCount];
int[] selectedVariantIndices = new int[selectedVariantsCount]; int[] selectedVariantIndices = new int[selectedVariantsCount];
int outIndex = 0; int outIndex = 0;
for (int i = 0; i < masterPlaylist.variants.size(); i++) { for (int i = 0; i < multivariantPlaylist.variants.size(); i++) {
if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO)
&& (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) { && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) {
Variant variant = masterPlaylist.variants.get(i); Variant variant = multivariantPlaylist.variants.get(i);
selectedPlaylistUrls[outIndex] = variant.url; selectedPlaylistUrls[outIndex] = variant.url;
selectedPlaylistFormats[outIndex] = variant.format; selectedPlaylistFormats[outIndex] = variant.format;
selectedVariantIndices[outIndex++] = i; selectedVariantIndices[outIndex++] = i;
@ -655,8 +661,8 @@ public final class HlsMediaPeriod
trackType, trackType,
selectedPlaylistUrls, selectedPlaylistUrls,
selectedPlaylistFormats, selectedPlaylistFormats,
masterPlaylist.muxedAudioFormat, multivariantPlaylist.muxedAudioFormat,
masterPlaylist.muxedCaptionFormats, multivariantPlaylist.muxedCaptionFormats,
overridingDrmInitData, overridingDrmInitData,
positionUs); positionUs);
sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrappers.add(sampleStreamWrapper);
@ -671,16 +677,17 @@ public final class HlsMediaPeriod
muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, videoFormats)); muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, videoFormats));
if (numberOfAudioCodecs > 0 if (numberOfAudioCodecs > 0
&& (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { && (multivariantPlaylist.muxedAudioFormat != null
|| multivariantPlaylist.audios.isEmpty())) {
muxedTrackGroups.add( muxedTrackGroups.add(
new TrackGroup( new TrackGroup(
/* id= */ sampleStreamWrapperUid + ":audio", /* id= */ sampleStreamWrapperUid + ":audio",
deriveAudioFormat( deriveAudioFormat(
selectedPlaylistFormats[0], selectedPlaylistFormats[0],
masterPlaylist.muxedAudioFormat, multivariantPlaylist.muxedAudioFormat,
/* isPrimaryTrackInVariant= */ false))); /* isPrimaryTrackInVariant= */ false)));
} }
List<Format> ccFormats = masterPlaylist.muxedCaptionFormats; List<Format> ccFormats = multivariantPlaylist.muxedCaptionFormats;
if (ccFormats != null) { if (ccFormats != null) {
for (int i = 0; i < ccFormats.size(); i++) { for (int i = 0; i < ccFormats.size(); i++) {
String ccId = sampleStreamWrapperUid + ":cc:" + i; String ccId = sampleStreamWrapperUid + ":cc:" + i;
@ -694,7 +701,7 @@ public final class HlsMediaPeriod
audioFormats[i] = audioFormats[i] =
deriveAudioFormat( deriveAudioFormat(
/* variantFormat= */ selectedPlaylistFormats[i], /* variantFormat= */ selectedPlaylistFormats[i],
masterPlaylist.muxedAudioFormat, multivariantPlaylist.muxedAudioFormat,
/* isPrimaryTrackInVariant= */ true); /* isPrimaryTrackInVariant= */ true);
} }
muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, audioFormats)); muxedTrackGroups.add(new TrackGroup(sampleStreamWrapperUid, audioFormats));
@ -709,7 +716,7 @@ public final class HlsMediaPeriod
.build()); .build());
muxedTrackGroups.add(id3TrackGroup); muxedTrackGroups.add(id3TrackGroup);
sampleStreamWrapper.prepareWithMasterPlaylistInfo( sampleStreamWrapper.prepareWithMultivariantPlaylistInfo(
muxedTrackGroups.toArray(new TrackGroup[0]), muxedTrackGroups.toArray(new TrackGroup[0]),
/* primaryTrackGroupIndex= */ 0, /* primaryTrackGroupIndex= */ 0,
/* optionalTrackGroupsIndices...= */ muxedTrackGroups.indexOf(id3TrackGroup)); /* optionalTrackGroupsIndices...= */ muxedTrackGroups.indexOf(id3TrackGroup));
@ -770,7 +777,7 @@ public final class HlsMediaPeriod
if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) { if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) {
Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]); Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]);
sampleStreamWrapper.prepareWithMasterPlaylistInfo( sampleStreamWrapper.prepareWithMultivariantPlaylistInfo(
new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, renditionFormats)}, new TrackGroup[] {new TrackGroup(sampleStreamWrapperUid, renditionFormats)},
/* primaryTrackGroupIndex= */ 0); /* primaryTrackGroupIndex= */ 0);
} }

View File

@ -240,7 +240,8 @@ public final class HlsMediaSource extends BaseMediaSource
/** /**
* Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads * Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads
* will be enabled for streams that provide sufficient information in their master playlist. * will be enabled for streams that provide sufficient information in their multivariant
* playlist.
* *
* @param allowChunklessPreparation Whether chunkless preparation is allowed. * @param allowChunklessPreparation Whether chunkless preparation is allowed.
* @return This factory, for convenience. * @return This factory, for convenience.
@ -275,10 +276,10 @@ public final class HlsMediaSource extends BaseMediaSource
} }
/** /**
* Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's * Sets whether to use #EXT-X-SESSION-KEY tags provided in the multivariant playlist. If
* assumed that any single session key declared in the master playlist can be used to obtain all * enabled, it's assumed that any single session key declared in the multivariant playlist can
* of the keys required for playback. For media where this is not true, this option should not * be used to obtain all of the keys required for playback. For media where this is not true,
* be enabled. * this option should not be enabled.
* *
* @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags. * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.
* @return This factory, for convenience. * @return This factory, for convenience.
@ -526,9 +527,9 @@ public final class HlsMediaSource extends BaseMediaSource
|| mediaPlaylist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD || mediaPlaylist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
? windowStartTimeMs ? windowStartTimeMs
: C.TIME_UNSET; : C.TIME_UNSET;
// The master playlist is non-null because the first playlist has been fetched by now. // The multivariant playlist is non-null because the first playlist has been fetched by now.
HlsManifest manifest = HlsManifest manifest =
new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), mediaPlaylist); new HlsManifest(checkNotNull(playlistTracker.getMultivariantPlaylist()), mediaPlaylist);
SinglePeriodTimeline timeline = SinglePeriodTimeline timeline =
playlistTracker.isLive() playlistTracker.isLive()
? createTimelineForLive( ? createTimelineForLive(

View File

@ -103,7 +103,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* Called when the wrapper has been prepared. * Called when the wrapper has been prepared.
* *
* <p>Note: This method will be called on a later handler loop than the one on which either * <p>Note: This method will be called on a later handler loop than the one on which either
* {@link #prepareWithMasterPlaylistInfo} or {@link #continuePreparing} are invoked. * {@link #prepareWithMultivariantPlaylistInfo} or {@link #continuePreparing} are invoked.
*/ */
void onPrepared(); void onPrepared();
@ -197,7 +197,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* stream's {@link DrmInitData} will be overridden. * stream's {@link DrmInitData} will be overridden.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param positionUs The position from which to start loading media. * @param positionUs The position from which to start loading media.
* @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the multivariant
* playlist.
* @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession
* DrmSessions} with. * DrmSessions} with.
* @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events. * @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events.
@ -261,7 +262,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
/** /**
* Prepares the sample stream wrapper with master playlist information. * Prepares the sample stream wrapper with multivariant playlist information.
* *
* @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link
* #getTrackGroups()}. * #getTrackGroups()}.
@ -269,7 +270,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not
* trigger a failure if not found in the media playlist's segments. * trigger a failure if not found in the media playlist's segments.
*/ */
public void prepareWithMasterPlaylistInfo( public void prepareWithMultivariantPlaylistInfo(
TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) {
this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups);
optionalTrackGroups = new HashSet<>(); optionalTrackGroups = new HashSet<>();
@ -1298,8 +1299,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
} }
if (trackGroups != null) { if (trackGroups != null) {
// The track groups were created with master playlist information. They only need to be mapped // The track groups were created with multivariant playlist information. They only need to be
// to a sample queue. // mapped to a sample queue.
mapSampleQueuesToMatchTrackGroups(); mapSampleQueuesToMatchTrackGroups();
} else { } else {
// Tracks are created using media segment information. // Tracks are created using media segment information.
@ -1334,18 +1335,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as * Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as
* internal data-structures required for operation. * internal data-structures required for operation.
* *
* <p>Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each * <p>Tracks in HLS are complicated. A HLS multivariant playlist contains a number of "variants".
* variant stream typically contains muxed video, audio and (possibly) additional audio, metadata * Each variant stream typically contains muxed video, audio and (possibly) additional audio,
* and caption tracks. We wish to allow the user to select between an adaptive track that spans * metadata and caption tracks. We wish to allow the user to select between an adaptive track that
* all variants, as well as each individual variant. If multiple audio tracks are present within * spans all variants, as well as each individual variant. If multiple audio tracks are present
* each variant then we wish to allow the user to select between those also. * within each variant then we wish to allow the user to select between those also.
* *
* <p>To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) * <p>To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1)
* tracks, where N is the number of variants defined in the HLS master playlist. These consist of * tracks, where N is the number of variants defined in the HLS multivariant playlist. These
* one adaptive track defined to span all variants and a track for each individual variant. The * consist of one adaptive track defined to span all variants and a track for each individual
* adaptive track is initially selected. The extractor is then prepared to discover the tracks * variant. The adaptive track is initially selected. The extractor is then prepared to discover
* inside of each variant stream. The two sets of tracks are then combined by this method to * the tracks inside of each variant stream. The two sets of tracks are then combined by this
* create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}: * method to create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}:
* *
* <ul> * <ul>
* <li>The extractor tracks are inspected to infer a "primary" track type. If a video track is * <li>The extractor tracks are inspected to infer a "primary" track type. If a video track is
@ -1518,14 +1519,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
/** /**
* Derives a track sample format from the corresponding format in the master playlist, and a * Derives a track sample format from the corresponding format in the multivariant playlist, and a
* sample format that may have been obtained from a chunk belonging to a different track in the * sample format that may have been obtained from a chunk belonging to a different track in the
* same track group. * same track group.
* *
* <p>Note: Since the sample format may have been obtained from a chunk belonging to a different * <p>Note: Since the sample format may have been obtained from a chunk belonging to a different
* track, it should not be used as a source for data that may vary between tracks. * track, it should not be used as a source for data that may vary between tracks.
* *
* @param playlistFormat The format information obtained from the master playlist. * @param playlistFormat The format information obtained from the multivariant playlist.
* @param sampleFormat The format information obtained from samples within a chunk. The chunk may * @param sampleFormat The format information obtained from samples within a chunk. The chunk may
* belong to a different track in the same track group. * belong to a different track in the same track group.
* @param propagateBitrates Whether the bitrates from the playlist format should be included in * @param propagateBitrates Whether the bitrates from the playlist format should be included in

View File

@ -23,8 +23,8 @@ import androidx.media3.common.util.UriUtil;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser; import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
import androidx.media3.exoplayer.offline.SegmentDownloader; import androidx.media3.exoplayer.offline.SegmentDownloader;
@ -46,14 +46,14 @@ import java.util.concurrent.Executor;
* new CacheDataSource.Factory() * new CacheDataSource.Factory()
* .setCache(cache) * .setCache(cache)
* .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory()); * .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
* // Create a downloader for the first variant in a master playlist. * // Create a downloader for the first variant in a multivariant playlist.
* HlsDownloader hlsDownloader = * HlsDownloader hlsDownloader =
* new HlsDownloader( * new HlsDownloader(
* new MediaItem.Builder() * new MediaItem.Builder()
* .setUri(playlistUri) * .setUri(playlistUri)
* .setStreamKeys( * .setStreamKeys(
* Collections.singletonList( * Collections.singletonList(
* new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0))) * new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, 0)))
* .build(), * .build(),
* Collections.singletonList(); * Collections.singletonList();
* // Perform the download. * // Perform the download.
@ -115,9 +115,9 @@ public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
protected List<Segment> getSegments(DataSource dataSource, HlsPlaylist playlist, boolean removing) protected List<Segment> getSegments(DataSource dataSource, HlsPlaylist playlist, boolean removing)
throws IOException, InterruptedException { throws IOException, InterruptedException {
ArrayList<DataSpec> mediaPlaylistDataSpecs = new ArrayList<>(); ArrayList<DataSpec> mediaPlaylistDataSpecs = new ArrayList<>();
if (playlist instanceof HlsMasterPlaylist) { if (playlist instanceof HlsMultivariantPlaylist) {
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; HlsMultivariantPlaylist multivariantPlaylist = (HlsMultivariantPlaylist) playlist;
addMediaPlaylistDataSpecs(masterPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs); addMediaPlaylistDataSpecs(multivariantPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs);
} else { } else {
mediaPlaylistDataSpecs.add( mediaPlaylistDataSpecs.add(
SegmentDownloader.getCompressibleDataSpec(Uri.parse(playlist.baseUri))); SegmentDownloader.getCompressibleDataSpec(Uri.parse(playlist.baseUri)));

View File

@ -30,7 +30,8 @@ public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserF
@Override @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser( public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { HlsMultivariantPlaylist multivariantPlaylist,
return new HlsPlaylistParser(masterPlaylist, previousMediaPlaylist); @Nullable HlsMediaPlaylist previousMediaPlaylist) {
return new HlsPlaylistParser(multivariantPlaylist, previousMediaPlaylist);
} }
} }

View File

@ -31,10 +31,10 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.HttpDataSource; import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.hls.HlsDataSourceFactory; import androidx.media3.exoplayer.hls.HlsDataSourceFactory;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Variant;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.RenditionReport; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.RenditionReport;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment;
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant;
import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher;
@ -74,7 +74,7 @@ public final class DefaultHlsPlaylistTracker
@Nullable private Loader initialPlaylistLoader; @Nullable private Loader initialPlaylistLoader;
@Nullable private Handler playlistRefreshHandler; @Nullable private Handler playlistRefreshHandler;
@Nullable private PrimaryPlaylistListener primaryPlaylistListener; @Nullable private PrimaryPlaylistListener primaryPlaylistListener;
@Nullable private HlsMasterPlaylist masterPlaylist; @Nullable private HlsMultivariantPlaylist multivariantPlaylist;
@Nullable private Uri primaryMediaPlaylistUrl; @Nullable private Uri primaryMediaPlaylistUrl;
@Nullable private HlsMediaPlaylist primaryMediaPlaylistSnapshot; @Nullable private HlsMediaPlaylist primaryMediaPlaylistSnapshot;
private boolean isLive; private boolean isLive;
@ -133,30 +133,33 @@ public final class DefaultHlsPlaylistTracker
this.playlistRefreshHandler = Util.createHandlerForCurrentLooper(); this.playlistRefreshHandler = Util.createHandlerForCurrentLooper();
this.eventDispatcher = eventDispatcher; this.eventDispatcher = eventDispatcher;
this.primaryPlaylistListener = primaryPlaylistListener; this.primaryPlaylistListener = primaryPlaylistListener;
ParsingLoadable<HlsPlaylist> masterPlaylistLoadable = ParsingLoadable<HlsPlaylist> multivariantPlaylistLoadable =
new ParsingLoadable<>( new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
initialPlaylistUri, initialPlaylistUri,
C.DATA_TYPE_MANIFEST, C.DATA_TYPE_MANIFEST,
playlistParserFactory.createPlaylistParser()); playlistParserFactory.createPlaylistParser());
Assertions.checkState(initialPlaylistLoader == null); Assertions.checkState(initialPlaylistLoader == null);
initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MultivariantPlaylist");
long elapsedRealtime = long elapsedRealtime =
initialPlaylistLoader.startLoading( initialPlaylistLoader.startLoading(
masterPlaylistLoadable, multivariantPlaylistLoadable,
this, this,
loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type)); loadErrorHandlingPolicy.getMinimumLoadableRetryCount(
multivariantPlaylistLoadable.type));
eventDispatcher.loadStarted( eventDispatcher.loadStarted(
new LoadEventInfo( new LoadEventInfo(
masterPlaylistLoadable.loadTaskId, masterPlaylistLoadable.dataSpec, elapsedRealtime), multivariantPlaylistLoadable.loadTaskId,
masterPlaylistLoadable.type); multivariantPlaylistLoadable.dataSpec,
elapsedRealtime),
multivariantPlaylistLoadable.type);
} }
@Override @Override
public void stop() { public void stop() {
primaryMediaPlaylistUrl = null; primaryMediaPlaylistUrl = null;
primaryMediaPlaylistSnapshot = null; primaryMediaPlaylistSnapshot = null;
masterPlaylist = null; multivariantPlaylist = null;
initialStartTimeUs = C.TIME_UNSET; initialStartTimeUs = C.TIME_UNSET;
initialPlaylistLoader.release(); initialPlaylistLoader.release();
initialPlaylistLoader = null; initialPlaylistLoader = null;
@ -181,8 +184,8 @@ public final class DefaultHlsPlaylistTracker
@Override @Override
@Nullable @Nullable
public HlsMasterPlaylist getMasterPlaylist() { public HlsMultivariantPlaylist getMultivariantPlaylist() {
return masterPlaylist; return multivariantPlaylist;
} }
@Override @Override
@ -245,18 +248,19 @@ public final class DefaultHlsPlaylistTracker
public void onLoadCompleted( public void onLoadCompleted(
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) { ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
HlsPlaylist result = loadable.getResult(); HlsPlaylist result = loadable.getResult();
HlsMasterPlaylist masterPlaylist; HlsMultivariantPlaylist multivariantPlaylist;
boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;
if (isMediaPlaylist) { if (isMediaPlaylist) {
masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); multivariantPlaylist =
} else /* result instanceof HlsMasterPlaylist */ { HlsMultivariantPlaylist.createSingleVariantMultivariantPlaylist(result.baseUri);
masterPlaylist = (HlsMasterPlaylist) result; } else /* result instanceof HlsMultivariantPlaylist */ {
multivariantPlaylist = (HlsMultivariantPlaylist) result;
} }
this.masterPlaylist = masterPlaylist; this.multivariantPlaylist = multivariantPlaylist;
primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url; primaryMediaPlaylistUrl = multivariantPlaylist.variants.get(0).url;
// Add a temporary playlist listener for loading the first primary playlist. // Add a temporary playlist listener for loading the first primary playlist.
listeners.add(new FirstPrimaryMediaPlaylistListener()); listeners.add(new FirstPrimaryMediaPlaylistListener());
createBundles(masterPlaylist.mediaPlaylistUrls); createBundles(multivariantPlaylist.mediaPlaylistUrls);
LoadEventInfo loadEventInfo = LoadEventInfo loadEventInfo =
new LoadEventInfo( new LoadEventInfo(
loadable.loadTaskId, loadable.loadTaskId,
@ -329,7 +333,7 @@ public final class DefaultHlsPlaylistTracker
// Internal methods. // Internal methods.
private boolean maybeSelectNewPrimaryUrl() { private boolean maybeSelectNewPrimaryUrl() {
List<Variant> variants = masterPlaylist.variants; List<Variant> variants = multivariantPlaylist.variants;
int variantsSize = variants.size(); int variantsSize = variants.size();
long currentTimeMs = SystemClock.elapsedRealtime(); long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < variantsSize; i++) { for (int i = 0; i < variantsSize; i++) {
@ -384,9 +388,12 @@ public final class DefaultHlsPlaylistTracker
return newPrimaryPlaylistUri; return newPrimaryPlaylistUri;
} }
/** Returns whether any of the variants in the master playlist have the specified playlist URL. */ /**
* Returns whether any of the variants in the multivariant playlist have the specified playlist
* URL.
*/
private boolean isVariantUrl(Uri playlistUrl) { private boolean isVariantUrl(Uri playlistUrl) {
List<Variant> variants = masterPlaylist.variants; List<Variant> variants = multivariantPlaylist.variants;
for (int i = 0; i < variants.size(); i++) { for (int i = 0; i < variants.size(); i++) {
if (playlistUrl.equals(variants.get(i).url)) { if (playlistUrl.equals(variants.get(i).url)) {
return true; return true;
@ -689,7 +696,7 @@ public final class DefaultHlsPlaylistTracker
private void loadPlaylistImmediately(Uri playlistRequestUri) { private void loadPlaylistImmediately(Uri playlistRequestUri) {
ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser = ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser =
playlistParserFactory.createPlaylistParser(masterPlaylist, playlistSnapshot); playlistParserFactory.createPlaylistParser(multivariantPlaylist, playlistSnapshot);
ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable = ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable =
new ParsingLoadable<>( new ParsingLoadable<>(
mediaPlaylistDataSource, mediaPlaylistDataSource,
@ -825,7 +832,7 @@ public final class DefaultHlsPlaylistTracker
if (primaryMediaPlaylistSnapshot == null) { if (primaryMediaPlaylistSnapshot == null) {
long nowMs = SystemClock.elapsedRealtime(); long nowMs = SystemClock.elapsedRealtime();
int variantExclusionCounter = 0; int variantExclusionCounter = 0;
List<Variant> variants = castNonNull(masterPlaylist).variants; List<Variant> variants = castNonNull(multivariantPlaylist).variants;
for (int i = 0; i < variants.size(); i++) { for (int i = 0; i < variants.size(); i++) {
@Nullable @Nullable
MediaPlaylistBundle mediaPlaylistBundle = playlistBundles.get(variants.get(i).url); MediaPlaylistBundle mediaPlaylistBundle = playlistBundles.get(variants.get(i).url);
@ -837,7 +844,7 @@ public final class DefaultHlsPlaylistTracker
new LoadErrorHandlingPolicy.FallbackOptions( new LoadErrorHandlingPolicy.FallbackOptions(
/* numberOfLocations= */ 1, /* numberOfLocations= */ 1,
/* numberOfExcludedLocations= */ 0, /* numberOfExcludedLocations= */ 0,
/* numberOfTracks= */ masterPlaylist.variants.size(), /* numberOfTracks= */ multivariantPlaylist.variants.size(),
/* numberOfExcludedTracks= */ variantExclusionCounter); /* numberOfExcludedTracks= */ variantExclusionCounter);
@Nullable @Nullable
LoadErrorHandlingPolicy.FallbackSelection fallbackSelection = LoadErrorHandlingPolicy.FallbackSelection fallbackSelection =

View File

@ -51,9 +51,10 @@ public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParse
@Override @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser( public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { HlsMultivariantPlaylist multivariantPlaylist,
@Nullable HlsMediaPlaylist previousMediaPlaylist) {
return new FilteringManifestParser<>( return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist, previousMediaPlaylist), hlsPlaylistParserFactory.createPlaylistParser(multivariantPlaylist, previousMediaPlaylist),
streamKeys); streamKeys);
} }
} }

View File

@ -15,183 +15,24 @@
*/ */
package androidx.media3.exoplayer.hls.playlist; package androidx.media3.exoplayer.hls.playlist;
import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** Represents an HLS master playlist. */ /** @deprecated Use {@link HlsMultivariantPlaylist} instead. */
@UnstableApi @UnstableApi
public final class HlsMasterPlaylist extends HlsPlaylist { @Deprecated
public final class HlsMasterPlaylist extends HlsMultivariantPlaylist {
/** Represents an empty master playlist, from which no attributes can be inherited. */
public static final HlsMasterPlaylist EMPTY =
new HlsMasterPlaylist(
/* baseUri= */ "",
/* tags= */ Collections.emptyList(),
/* variants= */ Collections.emptyList(),
/* videos= */ Collections.emptyList(),
/* audios= */ Collections.emptyList(),
/* subtitles= */ Collections.emptyList(),
/* closedCaptions= */ Collections.emptyList(),
/* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ Collections.emptyList(),
/* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
// These constants must not be changed because they are persisted in offline stream keys.
public static final int GROUP_INDEX_VARIANT = 0;
public static final int GROUP_INDEX_AUDIO = 1;
public static final int GROUP_INDEX_SUBTITLE = 2;
/** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */
public static final class Variant {
/** The variant's url. */
public final Uri url;
/** Format information associated with this variant. */
public final Format format;
/** The video rendition group referenced by this variant, or {@code null}. */
@Nullable public final String videoGroupId;
/** The audio rendition group referenced by this variant, or {@code null}. */
@Nullable public final String audioGroupId;
/** The subtitle rendition group referenced by this variant, or {@code null}. */
@Nullable public final String subtitleGroupId;
/** The caption rendition group referenced by this variant, or {@code null}. */
@Nullable public final String captionGroupId;
/** /**
* @param url See {@link #url}. * Creates an HLS multivariant playlist.
* @param format See {@link #format}.
* @param videoGroupId See {@link #videoGroupId}.
* @param audioGroupId See {@link #audioGroupId}.
* @param subtitleGroupId See {@link #subtitleGroupId}.
* @param captionGroupId See {@link #captionGroupId}.
*/
public Variant(
Uri url,
Format format,
@Nullable String videoGroupId,
@Nullable String audioGroupId,
@Nullable String subtitleGroupId,
@Nullable String captionGroupId) {
this.url = url;
this.format = format;
this.videoGroupId = videoGroupId;
this.audioGroupId = audioGroupId;
this.subtitleGroupId = subtitleGroupId;
this.captionGroupId = captionGroupId;
}
/**
* Creates a variant for a given media playlist url.
* *
* @param url The media playlist url. * @deprecated Use {@link HlsMultivariantPlaylist#HlsMultivariantPlaylist} instead.
* @return The variant instance.
*/
public static Variant createMediaPlaylistVariantUrl(Uri url) {
Format format =
new Format.Builder().setId("0").setContainerMimeType(MimeTypes.APPLICATION_M3U8).build();
return new Variant(
url,
format,
/* videoGroupId= */ null,
/* audioGroupId= */ null,
/* subtitleGroupId= */ null,
/* captionGroupId= */ null);
}
/** Returns a copy of this instance with the given {@link Format}. */
public Variant copyWithFormat(Format format) {
return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId);
}
}
/** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */
public static final class Rendition {
/** The rendition's url, or null if the tag does not have a URI attribute. */
@Nullable public final Uri url;
/** Format information associated with this rendition. */
public final Format format;
/** The group to which this rendition belongs. */
public final String groupId;
/** The name of the rendition. */
public final String name;
/**
* @param url See {@link #url}.
* @param format See {@link #format}.
* @param groupId See {@link #groupId}.
* @param name See {@link #name}.
*/
public Rendition(@Nullable Uri url, Format format, String groupId, String name) {
this.url = url;
this.format = format;
this.groupId = groupId;
this.name = name;
}
}
/** All of the media playlist URLs referenced by the playlist. */
public final List<Uri> mediaPlaylistUrls;
/** The variants declared by the playlist. */
public final List<Variant> variants;
/** The video renditions declared by the playlist. */
public final List<Rendition> videos;
/** The audio renditions declared by the playlist. */
public final List<Rendition> audios;
/** The subtitle renditions declared by the playlist. */
public final List<Rendition> subtitles;
/** The closed caption renditions declared by the playlist. */
public final List<Rendition> closedCaptions;
/**
* The format of the audio muxed in the variants. May be null if the playlist does not declare any
* muxed audio.
*/
@Nullable public final Format muxedAudioFormat;
/**
* The format of the closed captions declared by the playlist. May be empty if the playlist
* explicitly declares no captions are available, or null if the playlist does not declare any
* captions information.
*/
@Nullable public final List<Format> muxedCaptionFormats;
/** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
public final Map<String, String> variableDefinitions;
/** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */
public final List<DrmInitData> sessionKeyDrmInitData;
/**
* @param baseUri See {@link #baseUri}.
* @param tags See {@link #tags}.
* @param variants See {@link #variants}.
* @param videos See {@link #videos}.
* @param audios See {@link #audios}.
* @param subtitles See {@link #subtitles}.
* @param closedCaptions See {@link #closedCaptions}.
* @param muxedAudioFormat See {@link #muxedAudioFormat}.
* @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
* @param hasIndependentSegments See {@link #hasIndependentSegments}.
* @param variableDefinitions See {@link #variableDefinitions}.
* @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}.
*/ */
@Deprecated
public HlsMasterPlaylist( public HlsMasterPlaylist(
String baseUri, String baseUri,
List<String> tags, List<String> tags,
@ -205,117 +46,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
boolean hasIndependentSegments, boolean hasIndependentSegments,
Map<String, String> variableDefinitions, Map<String, String> variableDefinitions,
List<DrmInitData> sessionKeyDrmInitData) { List<DrmInitData> sessionKeyDrmInitData) {
super(baseUri, tags, hasIndependentSegments); super(
this.mediaPlaylistUrls =
Collections.unmodifiableList(
getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions));
this.variants = Collections.unmodifiableList(variants);
this.videos = Collections.unmodifiableList(videos);
this.audios = Collections.unmodifiableList(audios);
this.subtitles = Collections.unmodifiableList(subtitles);
this.closedCaptions = Collections.unmodifiableList(closedCaptions);
this.muxedAudioFormat = muxedAudioFormat;
this.muxedCaptionFormats =
muxedCaptionFormats != null ? Collections.unmodifiableList(muxedCaptionFormats) : null;
this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);
}
@Override
public HlsMasterPlaylist copy(List<StreamKey> streamKeys) {
return new HlsMasterPlaylist(
baseUri, baseUri,
tags, tags,
copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys), variants,
// TODO: Allow stream keys to specify video renditions to be retained. videos,
/* videos= */ Collections.emptyList(), audios,
copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys), subtitles,
copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), closedCaptions,
// TODO: Update to retain all closed captions.
/* closedCaptions= */ Collections.emptyList(),
muxedAudioFormat, muxedAudioFormat,
muxedCaptionFormats, muxedCaptionFormats,
hasIndependentSegments, hasIndependentSegments,
variableDefinitions, variableDefinitions,
sessionKeyDrmInitData); sessionKeyDrmInitData);
} }
/**
* Creates a playlist with a single variant.
*
* @param variantUrl The url of the single variant.
* @return A master playlist with a single variant for the provided url.
*/
public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) {
List<Variant> variant =
Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl)));
return new HlsMasterPlaylist(
/* baseUri= */ "",
/* tags= */ Collections.emptyList(),
variant,
/* videos= */ Collections.emptyList(),
/* audios= */ Collections.emptyList(),
/* subtitles= */ Collections.emptyList(),
/* closedCaptions= */ Collections.emptyList(),
/* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ null,
/* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
}
private static List<Uri> getMediaPlaylistUrls(
List<Variant> variants,
List<Rendition> videos,
List<Rendition> audios,
List<Rendition> subtitles,
List<Rendition> closedCaptions) {
ArrayList<Uri> mediaPlaylistUrls = new ArrayList<>();
for (int i = 0; i < variants.size(); i++) {
Uri uri = variants.get(i).url;
if (!mediaPlaylistUrls.contains(uri)) {
mediaPlaylistUrls.add(uri);
}
}
addMediaPlaylistUrls(videos, mediaPlaylistUrls);
addMediaPlaylistUrls(audios, mediaPlaylistUrls);
addMediaPlaylistUrls(subtitles, mediaPlaylistUrls);
addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls);
return mediaPlaylistUrls;
}
private static void addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out) {
for (int i = 0; i < renditions.size(); i++) {
Uri uri = renditions.get(i).url;
if (uri != null && !out.contains(uri)) {
out.add(uri);
}
}
}
private static <T> List<T> copyStreams(
List<T> streams, int groupIndex, List<StreamKey> streamKeys) {
List<T> copiedStreams = new ArrayList<>(streamKeys.size());
// TODO:
// 1. When variants with the same URL are not de-duplicated, duplicates must not increment
// trackIndex so as to avoid breaking stream keys that have been persisted for offline. All
// duplicates should be copied if the first variant is copied, or discarded otherwise.
// 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to
// avoid breaking stream keys that have been persisted for offline. All renitions with null
// URLs should be copied. They may become unreachable if all variants that reference them are
// removed, but this is OK.
// 3. Renditions with URLs matching copied variants should always themselves be copied, even if
// the corresponding stream key is omitted. Else we're throwing away information for no gain.
for (int i = 0; i < streams.size(); i++) {
T stream = streams.get(i);
for (int j = 0; j < streamKeys.size(); j++) {
StreamKey streamKey = streamKeys.get(j);
if (streamKey.groupIndex == groupIndex && streamKey.streamIndex == i) {
copiedStreams.add(stream);
break;
}
}
}
return copiedStreams;
}
} }

View File

@ -0,0 +1,322 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.hls.playlist;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.util.UnstableApi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** Represents an HLS multivariant playlist. */
// TODO(b/211458101): Make non-final once HlsMasterPlaylist is removed.
@UnstableApi
public class HlsMultivariantPlaylist extends HlsPlaylist {
/** Represents an empty multivariant playlist, from which no attributes can be inherited. */
public static final HlsMultivariantPlaylist EMPTY =
new HlsMultivariantPlaylist(
/* baseUri= */ "",
/* tags= */ Collections.emptyList(),
/* variants= */ Collections.emptyList(),
/* videos= */ Collections.emptyList(),
/* audios= */ Collections.emptyList(),
/* subtitles= */ Collections.emptyList(),
/* closedCaptions= */ Collections.emptyList(),
/* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ Collections.emptyList(),
/* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
// These constants must not be changed because they are persisted in offline stream keys.
public static final int GROUP_INDEX_VARIANT = 0;
public static final int GROUP_INDEX_AUDIO = 1;
public static final int GROUP_INDEX_SUBTITLE = 2;
/** A variant (i.e. an #EXT-X-STREAM-INF tag) in a multivariant playlist. */
public static final class Variant {
/** The variant's url. */
public final Uri url;
/** Format information associated with this variant. */
public final Format format;
/** The video rendition group referenced by this variant, or {@code null}. */
@Nullable public final String videoGroupId;
/** The audio rendition group referenced by this variant, or {@code null}. */
@Nullable public final String audioGroupId;
/** The subtitle rendition group referenced by this variant, or {@code null}. */
@Nullable public final String subtitleGroupId;
/** The caption rendition group referenced by this variant, or {@code null}. */
@Nullable public final String captionGroupId;
/**
* @param url See {@link #url}.
* @param format See {@link #format}.
* @param videoGroupId See {@link #videoGroupId}.
* @param audioGroupId See {@link #audioGroupId}.
* @param subtitleGroupId See {@link #subtitleGroupId}.
* @param captionGroupId See {@link #captionGroupId}.
*/
public Variant(
Uri url,
Format format,
@Nullable String videoGroupId,
@Nullable String audioGroupId,
@Nullable String subtitleGroupId,
@Nullable String captionGroupId) {
this.url = url;
this.format = format;
this.videoGroupId = videoGroupId;
this.audioGroupId = audioGroupId;
this.subtitleGroupId = subtitleGroupId;
this.captionGroupId = captionGroupId;
}
/**
* Creates a variant for a given media playlist url.
*
* @param url The media playlist url.
* @return The variant instance.
*/
public static Variant createMediaPlaylistVariantUrl(Uri url) {
Format format =
new Format.Builder().setId("0").setContainerMimeType(MimeTypes.APPLICATION_M3U8).build();
return new Variant(
url,
format,
/* videoGroupId= */ null,
/* audioGroupId= */ null,
/* subtitleGroupId= */ null,
/* captionGroupId= */ null);
}
/** Returns a copy of this instance with the given {@link Format}. */
public Variant copyWithFormat(Format format) {
return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId);
}
}
/** A rendition (i.e. an #EXT-X-MEDIA tag) in a multivariant playlist. */
public static final class Rendition {
/** The rendition's url, or null if the tag does not have a URI attribute. */
@Nullable public final Uri url;
/** Format information associated with this rendition. */
public final Format format;
/** The group to which this rendition belongs. */
public final String groupId;
/** The name of the rendition. */
public final String name;
/**
* @param url See {@link #url}.
* @param format See {@link #format}.
* @param groupId See {@link #groupId}.
* @param name See {@link #name}.
*/
public Rendition(@Nullable Uri url, Format format, String groupId, String name) {
this.url = url;
this.format = format;
this.groupId = groupId;
this.name = name;
}
}
/** All of the media playlist URLs referenced by the playlist. */
public final List<Uri> mediaPlaylistUrls;
/** The variants declared by the playlist. */
public final List<Variant> variants;
/** The video renditions declared by the playlist. */
public final List<Rendition> videos;
/** The audio renditions declared by the playlist. */
public final List<Rendition> audios;
/** The subtitle renditions declared by the playlist. */
public final List<Rendition> subtitles;
/** The closed caption renditions declared by the playlist. */
public final List<Rendition> closedCaptions;
/**
* The format of the audio muxed in the variants. May be null if the playlist does not declare any
* muxed audio.
*/
@Nullable public final Format muxedAudioFormat;
/**
* The format of the closed captions declared by the playlist. May be empty if the playlist
* explicitly declares no captions are available, or null if the playlist does not declare any
* captions information.
*/
@Nullable public final List<Format> muxedCaptionFormats;
/** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
public final Map<String, String> variableDefinitions;
/** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */
public final List<DrmInitData> sessionKeyDrmInitData;
/**
* @param baseUri See {@link #baseUri}.
* @param tags See {@link #tags}.
* @param variants See {@link #variants}.
* @param videos See {@link #videos}.
* @param audios See {@link #audios}.
* @param subtitles See {@link #subtitles}.
* @param closedCaptions See {@link #closedCaptions}.
* @param muxedAudioFormat See {@link #muxedAudioFormat}.
* @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
* @param hasIndependentSegments See {@link #hasIndependentSegments}.
* @param variableDefinitions See {@link #variableDefinitions}.
* @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}.
*/
public HlsMultivariantPlaylist(
String baseUri,
List<String> tags,
List<Variant> variants,
List<Rendition> videos,
List<Rendition> audios,
List<Rendition> subtitles,
List<Rendition> closedCaptions,
@Nullable Format muxedAudioFormat,
@Nullable List<Format> muxedCaptionFormats,
boolean hasIndependentSegments,
Map<String, String> variableDefinitions,
List<DrmInitData> sessionKeyDrmInitData) {
super(baseUri, tags, hasIndependentSegments);
this.mediaPlaylistUrls =
Collections.unmodifiableList(
getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions));
this.variants = Collections.unmodifiableList(variants);
this.videos = Collections.unmodifiableList(videos);
this.audios = Collections.unmodifiableList(audios);
this.subtitles = Collections.unmodifiableList(subtitles);
this.closedCaptions = Collections.unmodifiableList(closedCaptions);
this.muxedAudioFormat = muxedAudioFormat;
this.muxedCaptionFormats =
muxedCaptionFormats != null ? Collections.unmodifiableList(muxedCaptionFormats) : null;
this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);
}
@Override
public HlsMultivariantPlaylist copy(List<StreamKey> streamKeys) {
return new HlsMultivariantPlaylist(
baseUri,
tags,
copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys),
// TODO: Allow stream keys to specify video renditions to be retained.
/* videos= */ Collections.emptyList(),
copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys),
copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys),
// TODO: Update to retain all closed captions.
/* closedCaptions= */ Collections.emptyList(),
muxedAudioFormat,
muxedCaptionFormats,
hasIndependentSegments,
variableDefinitions,
sessionKeyDrmInitData);
}
/**
* Creates a playlist with a single variant.
*
* @param variantUrl The url of the single variant.
* @return A multivariant playlist with a single variant for the provided url.
*/
public static HlsMultivariantPlaylist createSingleVariantMultivariantPlaylist(String variantUrl) {
List<Variant> variant =
Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl)));
return new HlsMultivariantPlaylist(
/* baseUri= */ "",
/* tags= */ Collections.emptyList(),
variant,
/* videos= */ Collections.emptyList(),
/* audios= */ Collections.emptyList(),
/* subtitles= */ Collections.emptyList(),
/* closedCaptions= */ Collections.emptyList(),
/* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ null,
/* hasIndependentSegments= */ false,
/* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList());
}
private static List<Uri> getMediaPlaylistUrls(
List<Variant> variants,
List<Rendition> videos,
List<Rendition> audios,
List<Rendition> subtitles,
List<Rendition> closedCaptions) {
ArrayList<Uri> mediaPlaylistUrls = new ArrayList<>();
for (int i = 0; i < variants.size(); i++) {
Uri uri = variants.get(i).url;
if (!mediaPlaylistUrls.contains(uri)) {
mediaPlaylistUrls.add(uri);
}
}
addMediaPlaylistUrls(videos, mediaPlaylistUrls);
addMediaPlaylistUrls(audios, mediaPlaylistUrls);
addMediaPlaylistUrls(subtitles, mediaPlaylistUrls);
addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls);
return mediaPlaylistUrls;
}
private static void addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out) {
for (int i = 0; i < renditions.size(); i++) {
Uri uri = renditions.get(i).url;
if (uri != null && !out.contains(uri)) {
out.add(uri);
}
}
}
private static <T> List<T> copyStreams(
List<T> streams, int groupIndex, List<StreamKey> streamKeys) {
List<T> copiedStreams = new ArrayList<>(streamKeys.size());
// TODO:
// 1. When variants with the same URL are not de-duplicated, duplicates must not increment
// trackIndex so as to avoid breaking stream keys that have been persisted for offline. All
// duplicates should be copied if the first variant is copied, or discarded otherwise.
// 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to
// avoid breaking stream keys that have been persisted for offline. All renitions with null
// URLs should be copied. They may become unreachable if all variants that reference them are
// removed, but this is OK.
// 3. Renditions with URLs matching copied variants should always themselves be copied, even if
// the corresponding stream key is omitted. Else we're throwing away information for no gain.
for (int i = 0; i < streams.size(); i++) {
T stream = streams.get(i);
for (int j = 0; j < streamKeys.size(); j++) {
StreamKey streamKey = streamKeys.get(j);
if (streamKey.groupIndex == groupIndex && streamKey.streamIndex == i) {
copiedStreams.add(stream);
break;
}
}
}
return copiedStreams;
}
}

View File

@ -37,11 +37,11 @@ import androidx.media3.common.util.UriUtil;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry; import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry;
import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry.VariantInfo; import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry.VariantInfo;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Rendition;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Variant;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Part;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.RenditionReport; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.RenditionReport;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment;
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.exoplayer.upstream.ParsingLoadable;
import androidx.media3.extractor.mp4.PsshAtomUtil; import androidx.media3.extractor.mp4.PsshAtomUtil;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -225,28 +225,30 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_VARIABLE_REFERENCE = private static final Pattern REGEX_VARIABLE_REFERENCE =
Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}"); Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}");
private final HlsMasterPlaylist masterPlaylist; private final HlsMultivariantPlaylist multivariantPlaylist;
@Nullable private final HlsMediaPlaylist previousMediaPlaylist; @Nullable private final HlsMediaPlaylist previousMediaPlaylist;
/** /**
* Creates an instance where media playlists are parsed without inheriting attributes from a * Creates an instance where media playlists are parsed without inheriting attributes from a
* master playlist. * multivariant playlist.
*/ */
public HlsPlaylistParser() { public HlsPlaylistParser() {
this(HlsMasterPlaylist.EMPTY, /* previousMediaPlaylist= */ null); this(HlsMultivariantPlaylist.EMPTY, /* previousMediaPlaylist= */ null);
} }
/** /**
* Creates an instance where parsed media playlists inherit attributes from the given master * Creates an instance where parsed media playlists inherit attributes from the given master
* playlist. * playlist.
* *
* @param masterPlaylist The master playlist from which media playlists will inherit attributes. * @param multivariantPlaylist The multivariant playlist from which media playlists will inherit
* attributes.
* @param previousMediaPlaylist The previous media playlist from which the new media playlist may * @param previousMediaPlaylist The previous media playlist from which the new media playlist may
* inherit skipped segments. * inherit skipped segments.
*/ */
public HlsPlaylistParser( public HlsPlaylistParser(
HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { HlsMultivariantPlaylist multivariantPlaylist,
this.masterPlaylist = masterPlaylist; @Nullable HlsMediaPlaylist previousMediaPlaylist) {
this.multivariantPlaylist = multivariantPlaylist;
this.previousMediaPlaylist = previousMediaPlaylist; this.previousMediaPlaylist = previousMediaPlaylist;
} }
@ -266,7 +268,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
// Do nothing. // Do nothing.
} else if (line.startsWith(TAG_STREAM_INF)) { } else if (line.startsWith(TAG_STREAM_INF)) {
extraLines.add(line); extraLines.add(line);
return parseMasterPlaylist(new LineIterator(extraLines, reader), uri.toString()); return parseMultivariantPlaylist(new LineIterator(extraLines, reader), uri.toString());
} else if (line.startsWith(TAG_TARGET_DURATION) } else if (line.startsWith(TAG_TARGET_DURATION)
|| line.startsWith(TAG_MEDIA_SEQUENCE) || line.startsWith(TAG_MEDIA_SEQUENCE)
|| line.startsWith(TAG_MEDIA_DURATION) || line.startsWith(TAG_MEDIA_DURATION)
@ -277,7 +279,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|| line.equals(TAG_ENDLIST)) { || line.equals(TAG_ENDLIST)) {
extraLines.add(line); extraLines.add(line);
return parseMediaPlaylist( return parseMediaPlaylist(
masterPlaylist, multivariantPlaylist,
previousMediaPlaylist, previousMediaPlaylist,
new LineIterator(extraLines, reader), new LineIterator(extraLines, reader),
uri.toString()); uri.toString());
@ -321,8 +323,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return c; return c;
} }
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri) private static HlsMultivariantPlaylist parseMultivariantPlaylist(
throws IOException { LineIterator iterator, String baseUri) throws IOException {
HashMap<Uri, ArrayList<VariantInfo>> urlToVariantInfos = new HashMap<>(); HashMap<Uri, ArrayList<VariantInfo>> urlToVariantInfos = new HashMap<>();
HashMap<String, String> variableDefinitions = new HashMap<>(); HashMap<String, String> variableDefinitions = new HashMap<>();
ArrayList<Variant> variants = new ArrayList<>(); ArrayList<Variant> variants = new ArrayList<>();
@ -580,7 +582,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
muxedCaptionFormats = Collections.emptyList(); muxedCaptionFormats = Collections.emptyList();
} }
return new HlsMasterPlaylist( return new HlsMultivariantPlaylist(
baseUri, baseUri,
tags, tags,
deduplicatedVariants, deduplicatedVariants,
@ -629,7 +631,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
private static HlsMediaPlaylist parseMediaPlaylist( private static HlsMediaPlaylist parseMediaPlaylist(
HlsMasterPlaylist masterPlaylist, HlsMultivariantPlaylist multivariantPlaylist,
@Nullable HlsMediaPlaylist previousMediaPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist,
LineIterator iterator, LineIterator iterator,
String baseUri) String baseUri)
@ -640,7 +642,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int version = 1; // Default version == 1. int version = 1; // Default version == 1.
long targetDurationUs = C.TIME_UNSET; long targetDurationUs = C.TIME_UNSET;
long partTargetDurationUs = C.TIME_UNSET; long partTargetDurationUs = C.TIME_UNSET;
boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments; boolean hasIndependentSegmentsTag = multivariantPlaylist.hasIndependentSegments;
boolean hasEndTag = false; boolean hasEndTag = false;
@Nullable Segment initializationSegment = null; @Nullable Segment initializationSegment = null;
HashMap<String, String> variableDefinitions = new HashMap<>(); HashMap<String, String> variableDefinitions = new HashMap<>();
@ -750,11 +752,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} else if (line.startsWith(TAG_DEFINE)) { } else if (line.startsWith(TAG_DEFINE)) {
String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions); String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);
if (importName != null) { if (importName != null) {
String value = masterPlaylist.variableDefinitions.get(importName); String value = multivariantPlaylist.variableDefinitions.get(importName);
if (value != null) { if (value != null) {
variableDefinitions.put(importName, value); variableDefinitions.put(importName, value);
} else { } else {
// The master playlist does not declare the imported variable. Ignore. // The multivariant playlist does not declare the imported variable. Ignore.
} }
} else { } else {
variableDefinitions.put( variableDefinitions.put(

View File

@ -31,14 +31,16 @@ public interface HlsPlaylistParserFactory {
/** /**
* Returns a playlist parser for playlists that were referenced by the given {@link * Returns a playlist parser for playlists that were referenced by the given {@link
* HlsMasterPlaylist}. Returned {@link HlsMediaPlaylist} instances may inherit attributes from * HlsMultivariantPlaylist}. Returned {@link HlsMediaPlaylist} instances may inherit attributes
* {@code masterPlaylist}. * from {@code multivariantPlaylist}.
* *
* @param masterPlaylist The master playlist that referenced any parsed media playlists. * @param multivariantPlaylist The multivariant playlist that referenced any parsed media
* playlists.
* @param previousMediaPlaylist The previous media playlist or null if there is no previous media * @param previousMediaPlaylist The previous media playlist or null if there is no previous media
* playlist. * playlist.
* @return A parser for HLS playlists. * @return A parser for HLS playlists.
*/ */
ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser( ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist); HlsMultivariantPlaylist multivariantPlaylist,
@Nullable HlsMediaPlaylist previousMediaPlaylist);
} }

View File

@ -30,8 +30,8 @@ import java.io.IOException;
* <p>The playlist tracker is responsible for exposing the seeking window, which is defined by the * <p>The playlist tracker is responsible for exposing the seeking window, which is defined by the
* segments that one of the playlists exposes. This playlist is called primary and needs to be * segments that one of the playlists exposes. This playlist is called primary and needs to be
* periodically refreshed in the case of live streams. Note that the primary playlist is one of the * periodically refreshed in the case of live streams. Note that the primary playlist is one of the
* media playlists while the master playlist is an optional kind of playlist defined by the HLS * media playlists while the multivariant playlist is an optional kind of playlist defined by the
* specification (RFC 8216). * HLS specification (RFC 8216).
* *
* <p>Playlist loads might encounter errors. The tracker may choose to exclude them to ensure a * <p>Playlist loads might encounter errors. The tracker may choose to exclude them to ensure a
* primary playlist is always available. * primary playlist is always available.
@ -122,8 +122,8 @@ public interface HlsPlaylistTracker {
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()} * <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}
* call. * call.
* *
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a
* playlist. * multivariant playlist.
* @param eventDispatcher A dispatcher to notify of events. * @param eventDispatcher A dispatcher to notify of events.
* @param primaryPlaylistListener A callback for the primary playlist change events. * @param primaryPlaylistListener A callback for the primary playlist change events.
*/ */
@ -154,15 +154,15 @@ public interface HlsPlaylistTracker {
void removeListener(PlaylistEventListener listener); void removeListener(PlaylistEventListener listener);
/** /**
* Returns the master playlist. * Returns the multivariant playlist.
* *
* <p>If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist} * <p>If the uri passed to {@link #start} points to a media playlist, an {@link
* with a single variant for said media playlist is returned. * HlsMultivariantPlaylist} with a single variant for said media playlist is returned.
* *
* @return The master playlist. Null if the initial playlist has yet to be loaded. * @return The multivariant playlist. Null if the initial playlist has yet to be loaded.
*/ */
@Nullable @Nullable
HlsMasterPlaylist getMasterPlaylist(); HlsMultivariantPlaylist getMultivariantPlaylist();
/** /**
* Returns the most recent snapshot available of the playlist referenced by the provided {@link * Returns the most recent snapshot available of the playlist referenced by the provided {@link
@ -194,8 +194,8 @@ public interface HlsPlaylistTracker {
boolean isSnapshotValid(Uri url); boolean isSnapshotValid(Uri url);
/** /**
* If the tracker is having trouble refreshing the master playlist or the primary playlist, this * If the tracker is having trouble refreshing the multivariant playlist or the primary playlist,
* method throws the underlying error. Otherwise, does nothing. * this method throws the underlying error. Otherwise, does nothing.
* *
* @throws IOException The underlying error. * @throws IOException The underlying error.
*/ */

View File

@ -27,9 +27,9 @@ import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Rendition; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Rendition;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Variant; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker; import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker;
import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory; import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory;
@ -51,9 +51,9 @@ import org.junit.runner.RunWith;
public final class HlsMediaPeriodTest { public final class HlsMediaPeriodTest {
@Test @Test
public void getSteamKeys_isCompatibleWithHlsMasterPlaylistFilter() { public void getSteamKeys_isCompatibleWithHlsMultivariantPlaylistFilter() {
HlsMasterPlaylist testMasterPlaylist = HlsMultivariantPlaylist testMultivariantPlaylist =
createMasterPlaylist( createMultivariantPlaylist(
/* variants= */ Arrays.asList( /* variants= */ Arrays.asList(
createAudioOnlyVariant(/* peakBitrate= */ 10000), createAudioOnlyVariant(/* peakBitrate= */ 10000),
createMuxedVideoAudioVariant(/* peakBitrate= */ 200000), createMuxedVideoAudioVariant(/* peakBitrate= */ 200000),
@ -76,7 +76,8 @@ public final class HlsMediaPeriodTest {
HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class); HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class);
when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class)); when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class));
HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class); HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class);
when(mockPlaylistTracker.getMasterPlaylist()).thenReturn((HlsMasterPlaylist) playlist); when(mockPlaylistTracker.getMultivariantPlaylist())
.thenReturn((HlsMultivariantPlaylist) playlist);
MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object()); MediaPeriodId mediaPeriodId = new MediaPeriodId(/* periodUid= */ new Object());
return new HlsMediaPeriod( return new HlsMediaPeriod(
mock(HlsExtractorFactory.class), mock(HlsExtractorFactory.class),
@ -98,16 +99,16 @@ public final class HlsMediaPeriodTest {
}; };
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
mediaPeriodFactory, testMasterPlaylist); mediaPeriodFactory, testMultivariantPlaylist);
} }
private static HlsMasterPlaylist createMasterPlaylist( private static HlsMultivariantPlaylist createMultivariantPlaylist(
List<Variant> variants, List<Variant> variants,
List<Rendition> audios, List<Rendition> audios,
List<Rendition> subtitles, List<Rendition> subtitles,
Format muxedAudioFormat, Format muxedAudioFormat,
List<Format> muxedCaptionFormats) { List<Format> muxedCaptionFormats) {
return new HlsMasterPlaylist( return new HlsMultivariantPlaylist(
"http://baseUri", "http://baseUri",
/* tags= */ Collections.emptyList(), /* tags= */ Collections.emptyList(),
variants, variants,

View File

@ -20,11 +20,11 @@ import com.google.common.base.Charsets;
/** Data for HLS downloading tests. */ /** Data for HLS downloading tests. */
/* package */ interface HlsDownloadTestData { /* package */ interface HlsDownloadTestData {
String MASTER_PLAYLIST_URI = "test.m3u8"; String MULTIVARIANT_PLAYLIST_URI = "test.m3u8";
int MASTER_MEDIA_PLAYLIST_1_INDEX = 0; int MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX = 0;
int MASTER_MEDIA_PLAYLIST_2_INDEX = 1; int MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX = 1;
int MASTER_MEDIA_PLAYLIST_3_INDEX = 2; int MULTIVARIANT_MEDIA_PLAYLIST_3_INDEX = 2;
int MASTER_MEDIA_PLAYLIST_0_INDEX = 3; int MULTIVARIANT_MEDIA_PLAYLIST_0_INDEX = 3;
String MEDIA_PLAYLIST_0_DIR = "gear0/"; String MEDIA_PLAYLIST_0_DIR = "gear0/";
String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8"; String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8";
@ -35,7 +35,7 @@ import com.google.common.base.Charsets;
String MEDIA_PLAYLIST_3_DIR = "gear3/"; String MEDIA_PLAYLIST_3_DIR = "gear3/";
String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8"; String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8";
byte[] MASTER_PLAYLIST_DATA = byte[] MULTIVARIANT_PLAYLIST_DATA =
("#EXTM3U\n" ("#EXTM3U\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n"
+ MEDIA_PLAYLIST_1_URI + MEDIA_PLAYLIST_1_URI

View File

@ -17,10 +17,6 @@ package androidx.media3.exoplayer.hls.offline;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_1_INDEX;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_2_INDEX;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_DATA;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_URI;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_URI; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_URI;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_DIR; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_DIR;
@ -30,6 +26,10 @@ import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PL
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_DIR; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_DIR;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_URI; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_URI;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_DATA; import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_DATA;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MULTIVARIANT_PLAYLIST_DATA;
import static androidx.media3.exoplayer.hls.offline.HlsDownloadTestData.MULTIVARIANT_PLAYLIST_URI;
import static androidx.media3.test.utils.CacheAsserts.assertCacheEmpty; import static androidx.media3.test.utils.CacheAsserts.assertCacheEmpty;
import static androidx.media3.test.utils.CacheAsserts.assertCachedData; import static androidx.media3.test.utils.CacheAsserts.assertCachedData;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
@ -44,7 +44,7 @@ import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.NoOpCacheEvictor; import androidx.media3.datasource.cache.NoOpCacheEvictor;
import androidx.media3.datasource.cache.SimpleCache; import androidx.media3.datasource.cache.SimpleCache;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist;
import androidx.media3.exoplayer.offline.DefaultDownloaderFactory; import androidx.media3.exoplayer.offline.DefaultDownloaderFactory;
import androidx.media3.exoplayer.offline.DownloadRequest; import androidx.media3.exoplayer.offline.DownloadRequest;
import androidx.media3.exoplayer.offline.Downloader; import androidx.media3.exoplayer.offline.Downloader;
@ -83,7 +83,7 @@ public class HlsDownloaderTest {
progressListener = new ProgressListener(); progressListener = new ProgressListener();
fakeDataSet = fakeDataSet =
new FakeDataSet() new FakeDataSet()
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) .setData(MULTIVARIANT_PLAYLIST_URI, MULTIVARIANT_PLAYLIST_DATA)
.setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA) .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10) .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10)
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11) .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11)
@ -122,7 +122,7 @@ public class HlsDownloaderTest {
@Test @Test
public void counterMethods() throws Exception { public void counterMethods() throws Exception {
HlsDownloader downloader = HlsDownloader downloader =
getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); getHlsDownloader(MULTIVARIANT_PLAYLIST_URI, getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX));
downloader.download(progressListener); downloader.download(progressListener);
progressListener.assertBytesDownloaded(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); progressListener.assertBytesDownloaded(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
@ -131,14 +131,14 @@ public class HlsDownloaderTest {
@Test @Test
public void downloadRepresentation() throws Exception { public void downloadRepresentation() throws Exception {
HlsDownloader downloader = HlsDownloader downloader =
getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); getHlsDownloader(MULTIVARIANT_PLAYLIST_URI, getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX));
downloader.download(progressListener); downloader.download(progressListener);
assertCachedData( assertCachedData(
cache, cache,
new CacheAsserts.RequestSet(fakeDataSet) new CacheAsserts.RequestSet(fakeDataSet)
.subset( .subset(
MASTER_PLAYLIST_URI, MULTIVARIANT_PLAYLIST_URI,
MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_1_URI,
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
@ -149,8 +149,8 @@ public class HlsDownloaderTest {
public void downloadMultipleRepresentations() throws Exception { public void downloadMultipleRepresentations() throws Exception {
HlsDownloader downloader = HlsDownloader downloader =
getHlsDownloader( getHlsDownloader(
MASTER_PLAYLIST_URI, MULTIVARIANT_PLAYLIST_URI,
getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX, MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX));
downloader.download(progressListener); downloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
@ -169,7 +169,7 @@ public class HlsDownloaderTest {
.setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence1.ts", 14) .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence1.ts", 14)
.setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15); .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15);
HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys()); HlsDownloader downloader = getHlsDownloader(MULTIVARIANT_PLAYLIST_URI, getKeys());
downloader.download(progressListener); downloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
@ -179,8 +179,8 @@ public class HlsDownloaderTest {
public void remove() throws Exception { public void remove() throws Exception {
HlsDownloader downloader = HlsDownloader downloader =
getHlsDownloader( getHlsDownloader(
MASTER_PLAYLIST_URI, MULTIVARIANT_PLAYLIST_URI,
getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); getKeys(MULTIVARIANT_MEDIA_PLAYLIST_1_INDEX, MULTIVARIANT_MEDIA_PLAYLIST_2_INDEX));
downloader.download(progressListener); downloader.download(progressListener);
downloader.remove(); downloader.remove();
@ -231,7 +231,7 @@ public class HlsDownloaderTest {
private static ArrayList<StreamKey> getKeys(int... variantIndices) { private static ArrayList<StreamKey> getKeys(int... variantIndices) {
ArrayList<StreamKey> streamKeys = new ArrayList<>(); ArrayList<StreamKey> streamKeys = new ArrayList<>();
for (int variantIndex : variantIndices) { for (int variantIndex : variantIndices) {
streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); streamKeys.add(new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, variantIndex));
} }
return streamKeys; return streamKeys;
} }

View File

@ -44,9 +44,10 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class DefaultHlsPlaylistTrackerTest { public class DefaultHlsPlaylistTrackerTest {
private static final String SAMPLE_M3U8_LIVE_MASTER = "media/m3u8/live_low_latency_master"; private static final String SAMPLE_M3U8_LIVE_MULTIVARIANT =
private static final String SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM = "media/m3u8/live_low_latency_multivariant";
"media/m3u8/live_low_latency_master_media_uri_with_param"; private static final String SAMPLE_M3U8_LIVE_MULTIVARIANT_MEDIA_URI_WITH_PARAM =
"media/m3u8/live_low_latency_multivariant_media_uri_with_param";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL = private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL =
"media/m3u8/live_low_latency_media_can_skip_until"; "media/m3u8/live_low_latency_media_can_skip_until";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR = private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR =
@ -110,15 +111,15 @@ public class DefaultHlsPlaylistTrackerTest {
throws IOException, TimeoutException, InterruptedException { throws IOException, TimeoutException, InterruptedException {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] {"master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8"}, new String[] {"multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8"},
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -141,16 +142,16 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES" "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -175,12 +176,12 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/multivariant.m3u8",
"/media0/playlist.m3u8", "/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_skip=YES", "/media0/playlist.m3u8?_HLS_skip=YES",
"/media0/playlist.m3u8" "/media0/playlist.m3u8"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR));
@ -188,7 +189,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -206,16 +207,16 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=v2" "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=v2"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -230,18 +231,18 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/multivariant.m3u8",
"/media0/playlist.m3u8?param1=1&param2=2", "/media0/playlist.m3u8?param1=1&param2=2",
"/media0/playlist.m3u8?param1=1&param2=2&_HLS_skip=YES" "/media0/playlist.m3u8?param1=1&param2=2&_HLS_skip=YES"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT_MEDIA_URI_WITH_PARAM),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -257,16 +258,16 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14" "/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_NEXT)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_NEXT));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -281,18 +282,18 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/multivariant.m3u8",
"/media0/playlist.m3u8", "/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=1" "/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=1"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_NEXT)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_NEXT));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -310,18 +311,18 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/multivariant.m3u8",
"/media0/playlist.m3u8", "/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0" "/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_NEXT)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_NEXT));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -339,11 +340,11 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/multivariant.m3u8",
"/media0/playlist.m3u8", "/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0" "/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse( getMockResponse(
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD), SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD),
getMockResponse( getMockResponse(
@ -352,7 +353,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSource.Factory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -370,13 +371,13 @@ public class DefaultHlsPlaylistTrackerTest {
List<HttpUrl> httpUrls = List<HttpUrl> httpUrls =
enqueueWebServerResponses( enqueueWebServerResponses(
new String[] { new String[] {
"/master.m3u8", "/multivariant.m3u8",
"/media0/playlist.m3u8", "/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=16&_HLS_skip=YES", "/media0/playlist.m3u8?_HLS_msn=16&_HLS_skip=YES",
"/media0/playlist.m3u8", "/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=17&_HLS_skip=YES" "/media0/playlist.m3u8?_HLS_msn=17&_HLS_skip=YES"
}, },
getMockResponse(SAMPLE_M3U8_LIVE_MASTER), getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD),
new MockResponse().setResponseCode(400), new MockResponse().setResponseCode(400),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT), getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT),
@ -385,7 +386,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
/* dataSourceFactory= */ new DefaultHttpDataSource.Factory(), /* dataSourceFactory= */ new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 3); /* awaitedMediaPlaylistCount= */ 3);
assertRequestUrlsCalled(httpUrls); assertRequestUrlsCalled(httpUrls);
@ -415,7 +416,9 @@ public class DefaultHlsPlaylistTrackerTest {
} }
private static List<HlsMediaPlaylist> runPlaylistTrackerAndCollectMediaPlaylists( private static List<HlsMediaPlaylist> runPlaylistTrackerAndCollectMediaPlaylists(
DataSource.Factory dataSourceFactory, Uri masterPlaylistUri, int awaitedMediaPlaylistCount) DataSource.Factory dataSourceFactory,
Uri multivariantPlaylistUri,
int awaitedMediaPlaylistCount)
throws TimeoutException { throws TimeoutException {
DefaultHlsPlaylistTracker defaultHlsPlaylistTracker = DefaultHlsPlaylistTracker defaultHlsPlaylistTracker =
@ -427,7 +430,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = new ArrayList<>(); List<HlsMediaPlaylist> mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger(); AtomicInteger playlistCounter = new AtomicInteger();
defaultHlsPlaylistTracker.start( defaultHlsPlaylistTracker.start(
masterPlaylistUri, multivariantPlaylistUri,
new MediaSourceEventListener.EventDispatcher(), new MediaSourceEventListener.EventDispatcher(),
mediaPlaylist -> { mediaPlaylist -> {
mediaPlaylists.add(mediaPlaylist); mediaPlaylists.add(mediaPlaylist);

View File

@ -392,7 +392,7 @@ public class HlsMediaPlaylistParserTest {
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) (HlsMediaPlaylist)
new HlsPlaylistParser(HlsMasterPlaylist.EMPTY, previousPlaylist) new HlsPlaylistParser(HlsMultivariantPlaylist.EMPTY, previousPlaylist)
.parse(playlistUri, inputStream); .parse(playlistUri, inputStream);
assertThat(playlist.segments).hasSize(3); assertThat(playlist.segments).hasSize(3);
@ -446,7 +446,7 @@ public class HlsMediaPlaylistParserTest {
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) (HlsMediaPlaylist)
new HlsPlaylistParser(HlsMasterPlaylist.EMPTY, previousPlaylist) new HlsPlaylistParser(HlsMultivariantPlaylist.EMPTY, previousPlaylist)
.parse(playlistUri, inputStream); .parse(playlistUri, inputStream);
assertThat(playlist.segments).hasSize(2); assertThat(playlist.segments).hasSize(2);
@ -1363,7 +1363,7 @@ public class HlsMediaPlaylistParserTest {
} }
@Test @Test
public void masterPlaylistAttributeInheritance() throws IOException { public void multivariantPlaylistAttributeInheritance() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test3.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
String playlistString = String playlistString =
"#EXTM3U\n" "#EXTM3U\n"
@ -1386,8 +1386,8 @@ public class HlsMediaPlaylistParserTest {
assertThat(standalonePlaylist.hasIndependentSegments).isFalse(); assertThat(standalonePlaylist.hasIndependentSegments).isFalse();
inputStream.reset(); inputStream.reset();
HlsMasterPlaylist masterPlaylist = HlsMultivariantPlaylist multivariantPlaylist =
new HlsMasterPlaylist( new HlsMultivariantPlaylist(
/* baseUri= */ "https://example.com/", /* baseUri= */ "https://example.com/",
/* tags= */ Collections.emptyList(), /* tags= */ Collections.emptyList(),
/* variants= */ Collections.emptyList(), /* variants= */ Collections.emptyList(),
@ -1402,7 +1402,7 @@ public class HlsMediaPlaylistParserTest {
/* sessionKeyDrmInitData= */ Collections.emptyList()); /* sessionKeyDrmInitData= */ Collections.emptyList());
HlsMediaPlaylist playlistWithInheritance = HlsMediaPlaylist playlistWithInheritance =
(HlsMediaPlaylist) (HlsMediaPlaylist)
new HlsPlaylistParser(masterPlaylist, /* previousMediaPlaylist= */ null) new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null)
.parse(playlistUri, inputStream); .parse(playlistUri, inputStream);
assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); assertThat(playlistWithInheritance.hasIndependentSegments).isTrue();
} }
@ -1450,8 +1450,8 @@ public class HlsMediaPlaylistParserTest {
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HashMap<String, String> variableDefinitions = new HashMap<>(); HashMap<String, String> variableDefinitions = new HashMap<>();
variableDefinitions.put("imported_base", "long_path"); variableDefinitions.put("imported_base", "long_path");
HlsMasterPlaylist masterPlaylist = HlsMultivariantPlaylist multivariantPlaylist =
new HlsMasterPlaylist( new HlsMultivariantPlaylist(
/* baseUri= */ "", /* baseUri= */ "",
/* tags= */ Collections.emptyList(), /* tags= */ Collections.emptyList(),
/* variants= */ Collections.emptyList(), /* variants= */ Collections.emptyList(),
@ -1466,7 +1466,7 @@ public class HlsMediaPlaylistParserTest {
/* sessionKeyDrmInitData= */ Collections.emptyList()); /* sessionKeyDrmInitData= */ Collections.emptyList());
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) (HlsMediaPlaylist)
new HlsPlaylistParser(masterPlaylist, /* previousMediaPlaylist= */ null) new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null)
.parse(playlistUri, inputStream); .parse(playlistUri, inputStream);
for (int i = 1; i <= 4; i++) { for (int i = 1; i <= 4; i++) {
assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts"); assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts");

View File

@ -25,7 +25,7 @@ import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry; import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry;
import androidx.media3.exoplayer.hls.playlist.HlsMasterPlaylist.Variant; import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -36,9 +36,9 @@ import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Test for {@link HlsMasterPlaylist}. */ /** Test for {@link HlsMultivariantPlaylist}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class HlsMasterPlaylistParserTest { public class HlsMultivariantPlaylistParserTest {
private static final String PLAYLIST_URI = "https://example.com/test.m3u8"; private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
@ -233,12 +233,13 @@ public class HlsMasterPlaylistParserTest {
+ "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,RESOLUTION=1920x1080,CODECS=\"avc1.640028\",URI=\"iframe_1313400/index.m3u8\"\n"; + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,RESOLUTION=1920x1080,CODECS=\"avc1.640028\",URI=\"iframe_1313400/index.m3u8\"\n";
@Test @Test
public void parseMasterPlaylist_withSimple_success() throws IOException { public void parseMultivariantPlaylist_withSimple_success() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); HlsMultivariantPlaylist multivariantPlaylist =
parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
List<HlsMasterPlaylist.Variant> variants = masterPlaylist.variants; List<HlsMultivariantPlaylist.Variant> variants = multivariantPlaylist.variants;
assertThat(variants).hasSize(5); assertThat(variants).hasSize(5);
assertThat(masterPlaylist.muxedCaptionFormats).isNull(); assertThat(multivariantPlaylist.muxedCaptionFormats).isNull();
assertThat(variants.get(0).format.bitrate).isEqualTo(1280000); assertThat(variants.get(0).format.bitrate).isEqualTo(1280000);
assertThat(variants.get(0).format.codecs).isEqualTo("mp4a.40.2,avc1.66.30"); assertThat(variants.get(0).format.codecs).isEqualTo("mp4a.40.2,avc1.66.30");
@ -274,20 +275,20 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_withAverageBandwidth_success() throws IOException { public void parseMultivariantPlaylist_withAverageBandwidth_success() throws IOException {
HlsMasterPlaylist masterPlaylist = HlsMultivariantPlaylist multivariantPlaylist =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH);
List<HlsMasterPlaylist.Variant> variants = masterPlaylist.variants; List<HlsMultivariantPlaylist.Variant> variants = multivariantPlaylist.variants;
assertThat(variants.get(0).format.bitrate).isEqualTo(1280000); assertThat(variants.get(0).format.bitrate).isEqualTo(1280000);
assertThat(variants.get(1).format.bitrate).isEqualTo(1280000); assertThat(variants.get(1).format.bitrate).isEqualTo(1280000);
} }
@Test @Test
public void parseMasterPlaylist_withInvalidHeader_throwsException() throws IOException { public void parseMultivariantPlaylist_withInvalidHeader_throwsException() throws IOException {
try { try {
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
fail("Expected exception not thrown."); fail("Expected exception not thrown.");
} catch (ParserException e) { } catch (ParserException e) {
// Expected due to invalid header. // Expected due to invalid header.
@ -295,8 +296,8 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_withClosedCaption_success() throws IOException { public void parseMultivariantPlaylist_withClosedCaption_success() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); HlsMultivariantPlaylist playlist = parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC);
assertThat(playlist.muxedCaptionFormats).hasSize(1); assertThat(playlist.muxedCaptionFormats).hasSize(1);
Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0);
assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708); assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708);
@ -305,10 +306,10 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_withChannelsAttribute_success() throws IOException { public void parseMultivariantPlaylist_withChannelsAttribute_success() throws IOException {
HlsMasterPlaylist playlist = HlsMultivariantPlaylist playlist =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE);
List<HlsMasterPlaylist.Rendition> audios = playlist.audios; List<HlsMultivariantPlaylist.Rendition> audios = playlist.audios;
assertThat(audios).hasSize(3); assertThat(audios).hasSize(3);
assertThat(audios.get(0).format.channelCount).isEqualTo(6); assertThat(audios.get(0).format.channelCount).isEqualTo(6);
assertThat(audios.get(1).format.channelCount).isEqualTo(2); assertThat(audios.get(1).format.channelCount).isEqualTo(2);
@ -316,14 +317,15 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_withoutClosedCaption_success() throws IOException { public void parseMultivariantPlaylist_withoutClosedCaption_success() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); HlsMultivariantPlaylist playlist = parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC);
assertThat(playlist.muxedCaptionFormats).isEmpty(); assertThat(playlist.muxedCaptionFormats).isEmpty();
} }
@Test @Test
public void parseMasterPlaylist_withAudio_codecPropagated() throws IOException { public void parseMultivariantPlaylist_withAudio_codecPropagated() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); HlsMultivariantPlaylist playlist =
parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG);
Format firstAudioFormat = playlist.audios.get(0).format; Format firstAudioFormat = playlist.audios.get(0).format;
assertThat(firstAudioFormat.codecs).isEqualTo("mp4a.40.2"); assertThat(firstAudioFormat.codecs).isEqualTo("mp4a.40.2");
@ -335,8 +337,9 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_withAudio_audioIdPropagated() throws IOException { public void parseMultivariantPlaylist_withAudio_audioIdPropagated() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); HlsMultivariantPlaylist playlist =
parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG);
Format firstAudioFormat = playlist.audios.get(0).format; Format firstAudioFormat = playlist.audios.get(0).format;
assertThat(firstAudioFormat.id).isEqualTo("aud1:English"); assertThat(firstAudioFormat.id).isEqualTo("aud1:English");
@ -346,16 +349,17 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_withCc_cCIdPropagated() throws IOException { public void parseMultivariantPlaylist_withCc_cCIdPropagated() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); HlsMultivariantPlaylist playlist = parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC);
Format firstTextFormat = playlist.muxedCaptionFormats.get(0); Format firstTextFormat = playlist.muxedCaptionFormats.get(0);
assertThat(firstTextFormat.id).isEqualTo("cc1:Eng"); assertThat(firstTextFormat.id).isEqualTo("cc1:Eng");
} }
@Test @Test
public void parseMasterPlaylist_withSubtitles_subtitlesIdPropagated() throws IOException { public void parseMultivariantPlaylist_withSubtitles_subtitlesIdPropagated() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES); HlsMultivariantPlaylist playlist =
parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES);
Format firstTextFormat = playlist.subtitles.get(0).format; Format firstTextFormat = playlist.subtitles.get(0).format;
assertThat(firstTextFormat.id).isEqualTo("sub1:Eng"); assertThat(firstTextFormat.id).isEqualTo("sub1:Eng");
@ -363,39 +367,40 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_subtitlesWithoutUri_skipsSubtitles() throws IOException { public void parseMultivariantPlaylist_subtitlesWithoutUri_skipsSubtitles() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES_NO_URI); HlsMultivariantPlaylist playlist =
parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES_NO_URI);
assertThat(playlist.subtitles).isEmpty(); assertThat(playlist.subtitles).isEmpty();
} }
@Test @Test
public void parseMasterPlaylist_withIndependentSegments_hasNoIndenpendentSegments() public void parseMultivariantPlaylist_withIndependentSegments_hasNoIndenpendentSegments()
throws IOException { throws IOException {
HlsMasterPlaylist playlistWithIndependentSegments = HlsMultivariantPlaylist playlistWithIndependentSegments =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INDEPENDENT_SEGMENTS); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INDEPENDENT_SEGMENTS);
assertThat(playlistWithIndependentSegments.hasIndependentSegments).isTrue(); assertThat(playlistWithIndependentSegments.hasIndependentSegments).isTrue();
HlsMasterPlaylist playlistWithoutIndependentSegments = HlsMultivariantPlaylist playlistWithoutIndependentSegments =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse(); assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse();
} }
@Test @Test
public void parseMasterPlaylist_withVariableSubstitution_success() throws IOException { public void parseMultivariantPlaylist_withVariableSubstitution_success() throws IOException {
HlsMasterPlaylist playlistWithSubstitutions = HlsMultivariantPlaylist playlistWithSubstitutions =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION);
HlsMasterPlaylist.Variant variant = playlistWithSubstitutions.variants.get(0); HlsMultivariantPlaylist.Variant variant = playlistWithSubstitutions.variants.get(0);
assertThat(variant.format.codecs).isEqualTo("mp4a.40.5"); assertThat(variant.format.codecs).isEqualTo("mp4a.40.5");
assertThat(variant.url) assertThat(variant.url)
.isEqualTo(Uri.parse("http://example.com/This/{$nested}/reference/shouldnt/work")); .isEqualTo(Uri.parse("http://example.com/This/{$nested}/reference/shouldnt/work"));
} }
@Test @Test
public void parseMasterPlaylist_withTtmlSubtitle() throws IOException { public void parseMultivariantPlaylist_withTtmlSubtitle() throws IOException {
HlsMasterPlaylist playlistWithTtmlSubtitle = HlsMultivariantPlaylist playlistWithTtmlSubtitle =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_TTML_SUBTITLE); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_TTML_SUBTITLE);
HlsMasterPlaylist.Variant variant = playlistWithTtmlSubtitle.variants.get(0); HlsMultivariantPlaylist.Variant variant = playlistWithTtmlSubtitle.variants.get(0);
Format firstTextFormat = playlistWithTtmlSubtitle.subtitles.get(0).format; Format firstTextFormat = playlistWithTtmlSubtitle.subtitles.get(0).format;
assertThat(firstTextFormat.id).isEqualTo("sub1:English"); assertThat(firstTextFormat.id).isEqualTo("sub1:English");
assertThat(firstTextFormat.containerMimeType).isEqualTo(MimeTypes.APPLICATION_M3U8); assertThat(firstTextFormat.containerMimeType).isEqualTo(MimeTypes.APPLICATION_M3U8);
@ -404,9 +409,9 @@ public class HlsMasterPlaylistParserTest {
} }
@Test @Test
public void parseMasterPlaylist_withMatchingStreamInfUrls_success() throws IOException { public void parseMultivariantPlaylist_withMatchingStreamInfUrls_success() throws IOException {
HlsMasterPlaylist playlist = HlsMultivariantPlaylist playlist =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS); parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS);
assertThat(playlist.variants).hasSize(4); assertThat(playlist.variants).hasSize(4);
assertThat(playlist.variants.get(0).format.metadata) assertThat(playlist.variants.get(0).format.metadata)
.isEqualTo( .isEqualTo(
@ -441,7 +446,8 @@ public class HlsMasterPlaylistParserTest {
@Test @Test
public void testIFrameVariant() throws IOException { public void testIFrameVariant() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANTS); HlsMultivariantPlaylist playlist =
parseMultivariantPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANTS);
assertThat(playlist.variants).hasSize(5); assertThat(playlist.variants).hasSize(5);
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
assertThat(playlist.variants.get(i).format.roleFlags).isEqualTo(0); assertThat(playlist.variants.get(i).format.roleFlags).isEqualTo(0);
@ -472,11 +478,11 @@ public class HlsMasterPlaylistParserTest {
/* captionGroupId= */ "cc1"); /* captionGroupId= */ "cc1");
} }
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) private static HlsMultivariantPlaylist parseMultivariantPlaylist(
throws IOException { String uri, String playlistString) throws IOException {
Uri playlistUri = Uri.parse(uri); Uri playlistUri = Uri.parse(uri);
ByteArrayInputStream inputStream = ByteArrayInputStream inputStream =
new ByteArrayInputStream(playlistString.getBytes(Charsets.UTF_8)); new ByteArrayInputStream(playlistString.getBytes(Charsets.UTF_8));
return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); return (HlsMultivariantPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
} }
} }