More faithfully parse content of HLS master playlists

- Split HlsUrl into Rendition and Variant
- Add Rendition/Variant specific information to the new types

Issue: #5596
PiperOrigin-RevId: 240419763
This commit is contained in:
olly 2019-03-26 20:49:36 +00:00 committed by Toni
parent ae5e5f7efc
commit e3a8429ee2
7 changed files with 170 additions and 91 deletions

View file

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
@ -442,8 +443,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
: Collections.emptyMap();
boolean hasVariants = !masterPlaylist.variants.isEmpty();
List<HlsUrl> audioRenditions = masterPlaylist.audios;
List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles;
List<Rendition> audioRenditions = masterPlaylist.audios;
List<Rendition> subtitleRenditions = masterPlaylist.subtitles;
pendingPrepareCount = 0;
ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();
@ -644,7 +645,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private void buildAndPrepareAudioSampleStreamWrappers(
long positionUs,
List<HlsUrl> audioRenditions,
List<Rendition> audioRenditions,
List<HlsSampleStreamWrapper> sampleStreamWrappers,
List<int[]> manifestUrlsIndicesPerWrapper,
Map<String, DrmInitData> overridingDrmInitData) {

View file

@ -119,7 +119,8 @@ public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
return segments;
}
private void addMediaPlaylistDataSpecs(String baseUri, List<HlsUrl> urls, List<DataSpec> out) {
private void addMediaPlaylistDataSpecs(
String baseUri, List<? extends HlsUrl> urls, List<DataSpec> out) {
for (int i = 0; i < urls.size(); i++) {
Uri playlistUri = UriUtil.resolveToUri(baseUri, urls.get(i).url);
out.add(SegmentDownloader.getCompressibleDataSpec(playlistUri));

View file

@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
@ -298,7 +299,7 @@ public final class DefaultHlsPlaylistTracker
// Internal methods.
private boolean maybeSelectNewPrimaryUrl() {
List<HlsUrl> variants = masterPlaylist.variants;
List<Variant> variants = masterPlaylist.variants;
int variantsSize = variants.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < variantsSize; i++) {

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.hls.playlist;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.offline.StreamKey;
@ -45,10 +46,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
public static final int GROUP_INDEX_AUDIO = 1;
public static final int GROUP_INDEX_SUBTITLE = 2;
/**
* Represents a url in an HLS master playlist.
*/
public static final class HlsUrl {
/** Represents a url in an HLS master playlist. */
public abstract static class HlsUrl {
/**
* The http url from which the media playlist can be obtained.
@ -58,19 +57,61 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
* Format information associated with the HLS url.
*/
public final Format format;
/**
* Value of the NAME attribute as defined by the #EXT-X-MEDIA tag, or empty if the HLS url is
* not associated with any name.
*/
public final String name;
/**
* Creates an HLS url from a given http url.
*
* @param url The url.
* @return An HLS url.
* @param url See {@link #url}.
* @param format See {@link #format}.
*/
public static HlsUrl createMediaPlaylistHlsUrl(String url) {
public HlsUrl(String url, Format format) {
this.url = url;
this.format = format;
}
}
/** A variant in a master playlist. */
public static final class Variant extends HlsUrl {
/** 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(
String url,
Format format,
@Nullable String videoGroupId,
@Nullable String audioGroupId,
@Nullable String subtitleGroupId,
@Nullable String captionGroupId) {
super(url, 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(String url) {
Format format =
Format.createContainerFormat(
"0",
@ -82,34 +123,45 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
/* selectionFlags= */ 0,
/* roleFlags= */ 0,
/* language= */ null);
return new HlsUrl(url, format, /* name= */ "");
return new Variant(
url,
format,
/* videoGroupId= */ null,
/* audioGroupId= */ null,
/* subtitleGroupId= */ null,
/* captionGroupId= */ null);
}
}
/** A rendition in a master playlist. */
public static final class Rendition extends HlsUrl {
/** 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 HlsUrl(String url, Format format, String name) {
this.url = url;
this.format = format;
public Rendition(String url, Format format, String groupId, String name) {
super(url, format);
this.groupId = groupId;
this.name = name;
}
}
/**
* The list of variants declared by the playlist.
*/
public final List<HlsUrl> variants;
/**
* The list of demuxed audios declared by the playlist.
*/
public final List<HlsUrl> audios;
/**
* The list of subtitles declared by the playlist.
*/
public final List<HlsUrl> subtitles;
/** The list of variants declared by the playlist. */
public final List<Variant> variants;
/** The list of demuxed audios declared by the playlist. */
public final List<Rendition> audios;
/** The list of subtitles declared by the playlist. */
public final List<Rendition> subtitles;
/**
* The format of the audio muxed in the variants. May be null if the playlist does not declare any
@ -142,9 +194,9 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
public HlsMasterPlaylist(
String baseUri,
List<String> tags,
List<HlsUrl> variants,
List<HlsUrl> audios,
List<HlsUrl> subtitles,
List<Variant> variants,
List<Rendition> audios,
List<Rendition> subtitles,
Format muxedAudioFormat,
List<Format> muxedCaptionFormats,
boolean hasIndependentSegments,
@ -183,14 +235,14 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
* @return A master playlist with a single variant for the provided url.
*/
public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) {
List<HlsUrl> variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl));
List<HlsUrl> emptyList = Collections.emptyList();
List<Variant> variant =
Collections.singletonList(Variant.createMediaPlaylistVariantUrl(variantUrl));
return new HlsMasterPlaylist(
null,
Collections.emptyList(),
variant,
emptyList,
emptyList,
Collections.emptyList(),
Collections.emptyList(),
/* muxedAudioFormat= */ null,
/* muxedCaptionFormats= */ null,
/* hasIndependentSegments= */ false,
@ -198,11 +250,11 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
/* sessionKeyDrmInitData= */ Collections.emptyList());
}
private static List<HlsUrl> copyRenditionsList(
List<HlsUrl> renditions, int groupIndex, List<StreamKey> streamKeys) {
List<HlsUrl> copiedRenditions = new ArrayList<>(streamKeys.size());
private static <T extends HlsUrl> List<T> copyRenditionsList(
List<T> renditions, int groupIndex, List<StreamKey> streamKeys) {
List<T> copiedRenditions = new ArrayList<>(streamKeys.size());
for (int i = 0; i < renditions.size(); i++) {
HlsUrl rendition = renditions.get(i);
T rendition = renditions.get(i);
for (int j = 0; j < streamKeys.size(); j++) {
StreamKey streamKey = streamKeys.get(j);
if (streamKey.groupIndex == groupIndex && streamKey.trackIndex == i) {

View file

@ -26,6 +26,8 @@ import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.MimeTypes;
@ -100,7 +102,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_AVERAGE_BANDWIDTH =
Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\"");
private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\"");
private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\"");
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\"");
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
@ -249,9 +254,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
HashSet<String> variantUrls = new HashSet<>();
HashMap<String, String> audioGroupIdToCodecs = new HashMap<>();
HashMap<String, String> variableDefinitions = new HashMap<>();
ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
ArrayList<Variant> variants = new ArrayList<>();
ArrayList<Rendition> audios = new ArrayList<>();
ArrayList<Rendition> subtitles = new ArrayList<>();
ArrayList<String> mediaTags = new ArrayList<>();
ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();
ArrayList<String> tags = new ArrayList<>();
@ -321,7 +326,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (frameRateString != null) {
frameRate = Float.parseFloat(frameRateString);
}
String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions);
String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions);
String subtitlesGroupId =
parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);
String closedCaptionsGroupId =
parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);
if (audioGroupId != null && codecs != null) {
audioGroupIdToCodecs.put(audioGroupId, Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO));
}
@ -343,20 +353,27 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* initializationData= */ null,
/* selectionFlags= */ 0,
/* roleFlags= */ 0);
variants.add(new HlsMasterPlaylist.HlsUrl(line, format, /* name= */ ""));
variants.add(
new Variant(
line,
format,
videoGroupId,
audioGroupId,
subtitlesGroupId,
closedCaptionsGroupId));
}
}
}
for (int i = 0; i < mediaTags.size(); i++) {
line = mediaTags.get(i);
String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions);
String name = parseStringAttr(line, REGEX_NAME, variableDefinitions);
String uri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions);
String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions);
@C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
@C.RoleFlags int roleFlags = parseRoleFlags(line, variableDefinitions);
String uri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions);
String name = parseStringAttr(line, REGEX_NAME, variableDefinitions);
String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions);
String groupId = parseOptionalStringAttr(line, REGEX_GROUP_ID, variableDefinitions);
String id = groupId + ":" + name;
String formatId = groupId + ":" + name;
Format format;
switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
case TYPE_AUDIO:
@ -365,7 +382,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;
format =
Format.createAudioContainerFormat(
/* id= */ id,
/* id= */ formatId,
/* label= */ name,
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
sampleMimeType,
@ -380,13 +397,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (uri == null) {
muxedAudioFormat = format;
} else {
audios.add(new HlsMasterPlaylist.HlsUrl(uri, format, name));
audios.add(new Rendition(uri, format, groupId, name));
}
break;
case TYPE_SUBTITLES:
format =
Format.createTextContainerFormat(
/* id= */ id,
/* id= */ formatId,
/* label= */ name,
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
/* sampleMimeType= */ MimeTypes.TEXT_VTT,
@ -395,7 +412,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
selectionFlags,
roleFlags,
language);
subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format, name));
subtitles.add(new Rendition(uri, format, groupId, name));
break;
case TYPE_CLOSED_CAPTIONS:
String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);
@ -413,7 +430,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
muxedCaptionFormats.add(
Format.createTextContainerFormat(
/* id= */ id,
/* id= */ formatId,
/* label= */ name,
/* containerMimeType= */ null,
/* sampleMimeType= */ mimeType,

View file

@ -25,7 +25,8 @@ import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
import com.google.android.exoplayer2.testutil.MediaPeriodAsserts;
@ -53,19 +54,19 @@ public final class HlsMediaPeriodTest {
HlsMasterPlaylist testMasterPlaylist =
createMasterPlaylist(
/* variants= */ Arrays.asList(
createAudioOnlyVariantHlsUrl(/* bitrate= */ 10000),
createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 200000),
createAudioOnlyVariantHlsUrl(/* bitrate= */ 300000),
createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 400000),
createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 600000)),
createAudioOnlyVariant(/* bitrate= */ 10000),
createMuxedVideoAudioVariant(/* bitrate= */ 200000),
createAudioOnlyVariant(/* bitrate= */ 300000),
createMuxedVideoAudioVariant(/* bitrate= */ 400000),
createMuxedVideoAudioVariant(/* bitrate= */ 600000)),
/* audios= */ Arrays.asList(
createAudioHlsUrl(/* language= */ "spa"),
createAudioHlsUrl(/* language= */ "ger"),
createAudioHlsUrl(/* language= */ "tur")),
createAudioRendition(/* language= */ "spa"),
createAudioRendition(/* language= */ "ger"),
createAudioRendition(/* language= */ "tur")),
/* subtitles= */ Arrays.asList(
createSubtitleHlsUrl(/* language= */ "spa"),
createSubtitleHlsUrl(/* language= */ "ger"),
createSubtitleHlsUrl(/* language= */ "tur")),
createSubtitleRendition(/* language= */ "spa"),
createSubtitleRendition(/* language= */ "ger"),
createSubtitleRendition(/* language= */ "tur")),
/* muxedAudioFormat= */ createAudioFormat("eng"),
/* muxedCaptionFormats= */ Arrays.asList(
createSubtitleFormat("eng"), createSubtitleFormat("gsw")));
@ -97,9 +98,9 @@ public final class HlsMediaPeriodTest {
}
private static HlsMasterPlaylist createMasterPlaylist(
List<HlsUrl> variants,
List<HlsUrl> audios,
List<HlsUrl> subtitles,
List<Variant> variants,
List<Rendition> audios,
List<Rendition> subtitles,
Format muxedAudioFormat,
List<Format> muxedCaptionFormats) {
return new HlsMasterPlaylist(
@ -115,8 +116,8 @@ public final class HlsMediaPeriodTest {
/* sessionKeyDrmInitData= */ Collections.emptyList());
}
private static HlsUrl createMuxedVideoAudioVariantHlsUrl(int bitrate) {
return new HlsUrl(
private static Variant createMuxedVideoAudioVariant(int bitrate) {
return createVariant(
"http://url",
Format.createVideoContainerFormat(
/* id= */ null,
@ -130,12 +131,11 @@ public final class HlsMediaPeriodTest {
/* frameRate= */ Format.NO_VALUE,
/* initializationData= */ null,
/* selectionFlags= */ 0,
/* roleFlags= */ 0),
/* name= */ "");
/* roleFlags= */ 0));
}
private static HlsUrl createAudioOnlyVariantHlsUrl(int bitrate) {
return new HlsUrl(
private static Variant createAudioOnlyVariant(int bitrate) {
return createVariant(
"http://url",
Format.createVideoContainerFormat(
/* id= */ null,
@ -149,16 +149,23 @@ public final class HlsMediaPeriodTest {
/* frameRate= */ Format.NO_VALUE,
/* initializationData= */ null,
/* selectionFlags= */ 0,
/* roleFlags= */ 0),
/* name= */ "");
/* roleFlags= */ 0));
}
private static HlsUrl createAudioHlsUrl(String language) {
return new HlsUrl("http://url", createAudioFormat(language), /* name= */ "");
private static Rendition createAudioRendition(String language) {
return createRendition("http://url", createAudioFormat(language), "", "");
}
private static HlsUrl createSubtitleHlsUrl(String language) {
return new HlsUrl("http://url", createSubtitleFormat(language), /* name= */ "");
private static Rendition createSubtitleRendition(String language) {
return createRendition("http://url", createSubtitleFormat(language), "", "");
}
private static Variant createVariant(String url, Format format) {
return new Variant(url, format, null, null, null, null);
}
private static Rendition createRendition(String url, Format format, String groupId, String name) {
return new Rendition(url, format, groupId, name);
}
private static Format createAudioFormat(String language) {

View file

@ -95,7 +95,7 @@ public class HlsMasterPlaylistParserTest {
private static final String PLAYLIST_WITHOUT_CC =
" #EXTM3U \n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc1\","
+ "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,"
+ "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,"
@ -150,7 +150,7 @@ public class HlsMasterPlaylistParserTest {
public void testParseMasterPlaylist() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
List<HlsMasterPlaylist.Variant> variants = masterPlaylist.variants;
assertThat(variants).hasSize(5);
assertThat(masterPlaylist.muxedCaptionFormats).isNull();
@ -191,7 +191,7 @@ public class HlsMasterPlaylistParserTest {
HlsMasterPlaylist masterPlaylist =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
List<HlsMasterPlaylist.Variant> variants = masterPlaylist.variants;
assertThat(variants.get(0).format.bitrate).isEqualTo(1280000);
assertThat(variants.get(1).format.bitrate).isEqualTo(1270000);
@ -221,7 +221,7 @@ public class HlsMasterPlaylistParserTest {
public void testPlaylistWithChannelsAttribute() throws IOException {
HlsMasterPlaylist playlist =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE);
List<HlsMasterPlaylist.HlsUrl> audios = playlist.audios;
List<HlsMasterPlaylist.Rendition> audios = playlist.audios;
assertThat(audios).hasSize(3);
assertThat(audios.get(0).format.channelCount).isEqualTo(6);
assertThat(audios.get(1).format.channelCount).isEqualTo(2);