From e3a8429ee299c2aea275e587c6286b0bdf894745 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 26 Mar 2019 20:49:36 +0000 Subject: [PATCH] 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 --- .../exoplayer2/source/hls/HlsMediaPeriod.java | 7 +- .../source/hls/offline/HlsDownloader.java | 3 +- .../playlist/DefaultHlsPlaylistTracker.java | 3 +- .../hls/playlist/HlsMasterPlaylist.java | 134 ++++++++++++------ .../hls/playlist/HlsPlaylistParser.java | 45 ++++-- .../source/hls/HlsMediaPeriodTest.java | 61 ++++---- .../playlist/HlsMasterPlaylistParserTest.java | 8 +- 7 files changed, 170 insertions(+), 91 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 78551e6079..e76cf26db7 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -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 audioRenditions = masterPlaylist.audios; - List subtitleRenditions = masterPlaylist.subtitles; + List audioRenditions = masterPlaylist.audios; + List subtitleRenditions = masterPlaylist.subtitles; pendingPrepareCount = 0; ArrayList sampleStreamWrappers = new ArrayList<>(); @@ -644,7 +645,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private void buildAndPrepareAudioSampleStreamWrappers( long positionUs, - List audioRenditions, + List audioRenditions, List sampleStreamWrappers, List manifestUrlsIndicesPerWrapper, Map overridingDrmInitData) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index a0f64f298e..803f914686 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -119,7 +119,8 @@ public final class HlsDownloader extends SegmentDownloader { return segments; } - private void addMediaPlaylistDataSpecs(String baseUri, List urls, List out) { + private void addMediaPlaylistDataSpecs( + String baseUri, List urls, List out) { for (int i = 0; i < urls.size(); i++) { Uri playlistUri = UriUtil.resolveToUri(baseUri, urls.get(i).url); out.add(SegmentDownloader.getCompressibleDataSpec(playlistUri)); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index f0032ef475..93f617003d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -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 variants = masterPlaylist.variants; + List variants = masterPlaylist.variants; int variantsSize = variants.size(); long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < variantsSize; i++) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 5ebfc48e4d..f485778bb5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -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 variants; - /** - * The list of demuxed audios declared by the playlist. - */ - public final List audios; - /** - * The list of subtitles declared by the playlist. - */ - public final List subtitles; + /** The list of variants declared by the playlist. */ + public final List variants; + /** The list of demuxed audios declared by the playlist. */ + public final List audios; + /** The list of subtitles declared by the playlist. */ + public final List 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 tags, - List variants, - List audios, - List subtitles, + List variants, + List audios, + List subtitles, Format muxedAudioFormat, List 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 variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl)); - List emptyList = Collections.emptyList(); + List 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 copyRenditionsList( - List renditions, int groupIndex, List streamKeys) { - List copiedRenditions = new ArrayList<>(streamKeys.size()); + private static List copyRenditionsList( + List renditions, int groupIndex, List streamKeys) { + List 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) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 8d6c76e544..f0ce24f19c 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -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 variantUrls = new HashSet<>(); HashMap audioGroupIdToCodecs = new HashMap<>(); HashMap variableDefinitions = new HashMap<>(); - ArrayList variants = new ArrayList<>(); - ArrayList audios = new ArrayList<>(); - ArrayList subtitles = new ArrayList<>(); + ArrayList variants = new ArrayList<>(); + ArrayList audios = new ArrayList<>(); + ArrayList subtitles = new ArrayList<>(); ArrayList mediaTags = new ArrayList<>(); ArrayList sessionKeyDrmInitData = new ArrayList<>(); ArrayList tags = new ArrayList<>(); @@ -321,7 +326,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants, - List audios, - List subtitles, + List variants, + List audios, + List subtitles, Format muxedAudioFormat, List 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) { diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index dadf396190..92f578951b 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -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 variants = masterPlaylist.variants; + List 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 variants = masterPlaylist.variants; + List 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 audios = playlist.audios; + List audios = playlist.audios; assertThat(audios).hasSize(3); assertThat(audios.get(0).format.channelCount).isEqualTo(6); assertThat(audios.get(1).format.channelCount).isEqualTo(2);