From 65bab95f72bfbe2c36d44c4f4cb8f66e4432de82 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 27 Mar 2019 20:01:06 +0000 Subject: [PATCH] Add video and closed caption renditions to HlsMasterPlaylist Still skipping any renditions that have a null URL, which means that closedCaptions in particular is always empty. This restriction will be removed in a subsequent change, which will also remove muxedAudioFormat and muxedAudioCaptions from HlsMasterPlaylist and instead derive them from the Variant and Rendition information in HlsMediaPeriod. Issue: #5596 Issue: #2600 PiperOrigin-RevId: 240623500 --- .../exoplayer2/source/hls/HlsMediaPeriod.java | 3 + .../hls/playlist/HlsMasterPlaylist.java | 63 +++++++--- .../hls/playlist/HlsPlaylistParser.java | 119 +++++++++++++----- .../source/hls/HlsMediaPeriodTest.java | 2 + .../playlist/HlsMediaPlaylistParserTest.java | 4 + 5 files changed, 144 insertions(+), 47 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 e76cf26db7..406f4ed9fc 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 @@ -157,6 +157,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper return trackGroups; } + // TODO: When the master playlist does not de-duplicate variants by URL and allows Renditions with + // null URLs, this method must be updated to calculate stream keys that are compatible with those + // that may already be persisted for offline. @Override public List getStreamKeys(List trackSelections) { // See HlsMasterPlaylist.copy for interpretation of StreamKeys. 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 f485778bb5..a37ebe6054 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 @@ -34,14 +34,17 @@ public final class HlsMasterPlaylist extends HlsPlaylist { /* 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; @@ -156,12 +159,16 @@ public final class HlsMasterPlaylist extends HlsPlaylist { } - /** The list of variants declared by the playlist. */ + /** The variants declared by the playlist. */ public final List variants; - /** The list of demuxed audios declared by the playlist. */ + /** The video renditions declared by the playlist. */ + public final List videos; + /** The audio renditions declared by the playlist. */ public final List audios; - /** The list of subtitles declared by the playlist. */ + /** The subtitle renditions declared by the playlist. */ public final List subtitles; + /** The closed caption renditions declared by the playlist. */ + public final List closedCaptions; /** * The format of the audio muxed in the variants. May be null if the playlist does not declare any @@ -183,8 +190,10 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * @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}. @@ -195,8 +204,10 @@ public final class HlsMasterPlaylist extends HlsPlaylist { String baseUri, List tags, List variants, + List videos, List audios, List subtitles, + List closedCaptions, Format muxedAudioFormat, List muxedCaptionFormats, boolean hasIndependentSegments, @@ -204,8 +215,10 @@ public final class HlsMasterPlaylist extends HlsPlaylist { List sessionKeyDrmInitData) { super(baseUri, tags, hasIndependentSegments); 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; @@ -218,9 +231,13 @@ public final class HlsMasterPlaylist extends HlsPlaylist { return new HlsMasterPlaylist( baseUri, tags, - copyRenditionsList(variants, GROUP_INDEX_VARIANT, streamKeys), - copyRenditionsList(audios, GROUP_INDEX_AUDIO, streamKeys), - copyRenditionsList(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), + 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, @@ -238,11 +255,13 @@ public final class HlsMasterPlaylist extends HlsPlaylist { List variant = Collections.singletonList(Variant.createMediaPlaylistVariantUrl(variantUrl)); return new HlsMasterPlaylist( - null, - Collections.emptyList(), + /* baseUri= */ null, + /* tags= */ Collections.emptyList(), variant, - Collections.emptyList(), - Collections.emptyList(), + /* videos= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* closedCaptions= */ Collections.emptyList(), /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ null, /* hasIndependentSegments= */ false, @@ -250,20 +269,30 @@ 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()); - for (int i = 0; i < renditions.size(); i++) { - T rendition = renditions.get(i); + private static List copyStreams( + List streams, int groupIndex, List streamKeys) { + List 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.trackIndex == i) { - copiedRenditions.add(rendition); + copiedStreams.add(stream); break; } } } - return copiedRenditions; + return copiedStreams; } } 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 f0ce24f19c..1a22dd1636 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 @@ -252,11 +252,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variantUrls = new HashSet<>(); - HashMap audioGroupIdToCodecs = new HashMap<>(); HashMap variableDefinitions = new HashMap<>(); ArrayList variants = new ArrayList<>(); + ArrayList deduplicatedVariants = new ArrayList<>(); + ArrayList videos = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); + ArrayList closedCaptions = new ArrayList<>(); ArrayList mediaTags = new ArrayList<>(); ArrayList sessionKeyDrmInitData = new ArrayList<>(); ArrayList tags = new ArrayList<>(); @@ -332,35 +334,30 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants, String groupId) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (groupId.equals(variant.audioGroupId)) { + return variant; + } + } + return null; + } + + private static Variant getVariantWithVideoGroup(ArrayList variants, String groupId) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (groupId.equals(variant.videoGroupId)) { + return variant; + } + } + return null; + } + private static HlsMediaPlaylist parseMediaPlaylist( HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index c3e311be89..dcf44bc2ae 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -107,8 +107,10 @@ public final class HlsMediaPeriodTest { "http://baseUri", /* tags= */ Collections.emptyList(), variants, + /* videos= */ Collections.emptyList(), audios, subtitles, + /* closedCaptions= */ Collections.emptyList(), muxedAudioFormat, muxedCaptionFormats, /* hasIndependentSegments= */ true, diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 384408a0af..80cc6b23cd 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -451,8 +451,10 @@ public class HlsMediaPlaylistParserTest { /* baseUri= */ "https://example.com/", /* tags= */ Collections.emptyList(), /* variants= */ Collections.emptyList(), + /* videos= */ Collections.emptyList(), /* audios= */ Collections.emptyList(), /* subtitles= */ Collections.emptyList(), + /* closedCaptions= */ Collections.emptyList(), /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ null, /* hasIndependentSegments= */ true, @@ -511,8 +513,10 @@ public class HlsMediaPlaylistParserTest { /* 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,