From 36da5fb5115cc81ac9a66b46af8e5a96e14a8457 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 8 Feb 2019 14:50:26 +0000 Subject: [PATCH] Group audio renditions by name Allows the player to adapt between audio renditions with different names. PiperOrigin-RevId: 233052105 --- RELEASENOTES.md | 2 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 134 ++++++++++++------ .../source/hls/HlsSampleStreamWrapper.java | 5 +- .../hls/playlist/HlsMasterPlaylist.java | 11 +- .../hls/playlist/HlsPlaylistParser.java | 6 +- .../source/hls/HlsMediaPeriodTest.java | 12 +- 6 files changed, 118 insertions(+), 52 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1f7e6b74ce..90eddb7c08 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* HLS: + * Form an adaptive track group out of audio renditions with matching name. * `ExtractorMediaSource` renamed to `ProgressiveMediaSource`. * Support for playing spherical videos on Daydream. * Improve decoder re-use between playbacks. TODO: Write and link a blog post 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 0c4ebcb508..a31f040a32 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 @@ -43,6 +43,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; @@ -70,7 +71,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; // Maps sample stream wrappers to variant/rendition index by matching array positions. - private int[][] manifestUrlsIndicesPerWrapper; + private int[][] manifestUrlIndicesPerWrapper; private SequenceableLoader compositeSequenceableLoader; private boolean notifiedReadingStarted; @@ -115,7 +116,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper timestampAdjusterProvider = new TimestampAdjusterProvider(); sampleStreamWrappers = new HlsSampleStreamWrapper[0]; enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; - manifestUrlsIndicesPerWrapper = new int[0][]; + manifestUrlIndicesPerWrapper = new int[0][]; eventDispatcher.mediaPeriodCreated(); } @@ -153,14 +154,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); boolean hasVariants = !masterPlaylist.variants.isEmpty(); int audioWrapperOffset = hasVariants ? 1 : 0; - int subtitleWrapperOffset = audioWrapperOffset + masterPlaylist.audios.size(); + // Subtitle sample stream wrappers are held last. + int subtitleWrapperOffset = sampleStreamWrappers.length - masterPlaylist.subtitles.size(); TrackGroupArray mainWrapperTrackGroups; int mainWrapperPrimaryGroupIndex; int[] mainWrapperVariantIndices; if (hasVariants) { HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0]; - mainWrapperVariantIndices = manifestUrlsIndicesPerWrapper[0]; + mainWrapperVariantIndices = manifestUrlIndicesPerWrapper[0]; mainWrapperTrackGroups = mainWrapper.getTrackGroups(); mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex(); } else { @@ -191,12 +193,18 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Audio or subtitle group. for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) { TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups(); - if (wrapperTrackGroups.indexOf(trackSelectionGroup) != C.INDEX_UNSET) { + int selectedTrackGroupIndex = wrapperTrackGroups.indexOf(trackSelectionGroup); + if (selectedTrackGroupIndex != C.INDEX_UNSET) { int groupIndexType = i < subtitleWrapperOffset ? HlsMasterPlaylist.GROUP_INDEX_AUDIO : HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; - streamKeys.add(new StreamKey(groupIndexType, manifestUrlsIndicesPerWrapper[i][0])); + int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i]; + for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) { + int renditionIndex = + selectedWrapperUrlIndices[trackSelection.getIndexInTrackGroup(trackIndex)]; + streamKeys.add(new StreamKey(groupIndexType, renditionIndex)); + } break; } } @@ -424,37 +432,19 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper List audioRenditions = masterPlaylist.audios; List subtitleRenditions = masterPlaylist.subtitles; - int wrapperCount = (hasVariants ? 1 : 0) + audioRenditions.size() + subtitleRenditions.size(); - sampleStreamWrappers = new HlsSampleStreamWrapper[wrapperCount]; - manifestUrlsIndicesPerWrapper = new int[wrapperCount][]; - pendingPrepareCount = wrapperCount; + pendingPrepareCount = 0; + ArrayList sampleStreamWrappers = new ArrayList<>(); + ArrayList manifestUrlIndicesPerWrapper = new ArrayList<>(); - int currentWrapperIndex = 0; if (hasVariants) { - buildAndPrepareMainSampleStreamWrapper(masterPlaylist, positionUs); - currentWrapperIndex++; + buildAndPrepareMainSampleStreamWrapper( + masterPlaylist, positionUs, sampleStreamWrappers, manifestUrlIndicesPerWrapper); } // TODO: Build video stream wrappers here. - // Audio sample stream wrappers. - for (int i = 0; i < audioRenditions.size(); i++) { - HlsUrl audioRendition = audioRenditions.get(i); - HlsSampleStreamWrapper sampleStreamWrapper = - buildSampleStreamWrapper( - C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRendition}, - null, - Collections.emptyList(), - positionUs); - manifestUrlsIndicesPerWrapper[currentWrapperIndex] = new int[] {i}; - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; - Format renditionFormat = audioRendition.format; - if (allowChunklessPreparation && renditionFormat.codecs != null) { - sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(audioRendition.format)), 0, TrackGroupArray.EMPTY); - } - } + buildAndPrepareAudioSampleStreamWrappers( + positionUs, audioRenditions, sampleStreamWrappers, manifestUrlIndicesPerWrapper); // Subtitle stream wrappers. We can always use master playlist information to prepare these. for (int i = 0; i < subtitleRenditions.size(); i++) { @@ -462,20 +452,22 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper( C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, Collections.emptyList(), positionUs); - manifestUrlsIndicesPerWrapper[currentWrapperIndex] = new int[] {i}; - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + manifestUrlIndicesPerWrapper.add(new int[] {i}); + sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrapper.prepareWithMasterPlaylistInfo( new TrackGroupArray(new TrackGroup(url.format)), 0, TrackGroupArray.EMPTY); } + this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]); + this.manifestUrlIndicesPerWrapper = manifestUrlIndicesPerWrapper.toArray(new int[0][]); + pendingPrepareCount = this.sampleStreamWrappers.length; // Set timestamp master and trigger preparation (if not already prepared) - sampleStreamWrappers[0].setIsTimestampMaster(true); - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + this.sampleStreamWrappers[0].setIsTimestampMaster(true); + for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) { sampleStreamWrapper.continuePreparing(); } - // All wrappers are enabled during preparation. - enabledSampleStreamWrappers = sampleStreamWrappers; + enabledSampleStreamWrappers = this.sampleStreamWrappers; } /** @@ -501,9 +493,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper * @param masterPlaylist The HLS master playlist. * @param positionUs If preparation requires any chunk downloads, the position in microseconds at * which downloading should start. Ignored otherwise. + * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added. + * @param manifestUrlIndicesPerWrapper List to which the selected variant indices should be added. */ private void buildAndPrepareMainSampleStreamWrapper( - HlsMasterPlaylist masterPlaylist, long positionUs) { + HlsMasterPlaylist masterPlaylist, + long positionUs, + List sampleStreamWrappers, + List manifestUrlIndicesPerWrapper) { int[] variantTypes = new int[masterPlaylist.variants.size()]; int videoVariantCount = 0; int audioVariantCount = 0; @@ -536,13 +533,13 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper selectedVariantsCount = variantTypes.length - audioVariantCount; } HlsUrl[] selectedVariants = new HlsUrl[selectedVariantsCount]; - manifestUrlsIndicesPerWrapper[0] = new int[selectedVariantsCount]; + int[] selectedVariantIndices = new int[selectedVariantsCount]; int outIndex = 0; for (int i = 0; i < masterPlaylist.variants.size(); i++) { if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) { selectedVariants[outIndex] = masterPlaylist.variants.get(i); - manifestUrlsIndicesPerWrapper[0][outIndex++] = i; + selectedVariantIndices[outIndex++] = i; } } String codecs = selectedVariants[0].format.codecs; @@ -553,7 +550,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs); - sampleStreamWrappers[0] = sampleStreamWrapper; + sampleStreamWrappers.add(sampleStreamWrapper); + manifestUrlIndicesPerWrapper.add(selectedVariantIndices); if (allowChunklessPreparation && codecs != null) { boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null; boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null; @@ -614,6 +612,60 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } + private void buildAndPrepareAudioSampleStreamWrappers( + long positionUs, + List audioRenditions, + List sampleStreamWrappers, + List manifestUrlsIndicesPerWrapper) { + + ArrayList scratchRenditionList = + new ArrayList<>(/* initialCapacity= */ audioRenditions.size()); + ArrayList scratchIndicesList = + new ArrayList<>(/* initialCapacity= */ audioRenditions.size()); + HashSet alreadyGroupedNames = new HashSet<>(); + for (int renditionByNameIndex = 0; + renditionByNameIndex < audioRenditions.size(); + renditionByNameIndex++) { + String name = audioRenditions.get(renditionByNameIndex).name; + if (!alreadyGroupedNames.add(name)) { + // This name already has a corresponding group. + continue; + } + + boolean renditionsHaveCodecs = true; + scratchRenditionList.clear(); + scratchIndicesList.clear(); + // Group all renditions with matching name. + for (int renditionIndex = 0; renditionIndex < audioRenditions.size(); renditionIndex++) { + if (Util.areEqual(name, audioRenditions.get(renditionIndex).name)) { + HlsUrl rendition = audioRenditions.get(renditionIndex); + scratchIndicesList.add(renditionIndex); + scratchRenditionList.add(rendition); + renditionsHaveCodecs &= rendition.format.codecs != null; + } + } + + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_AUDIO, + scratchRenditionList.toArray(new HlsUrl[0]), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ Collections.emptyList(), + positionUs); + manifestUrlsIndicesPerWrapper.add(Util.toArray(scratchIndicesList)); + sampleStreamWrappers.add(sampleStreamWrapper); + + if (allowChunklessPreparation && renditionsHaveCodecs) { + Format[] renditionFormats = new Format[scratchRenditionList.size()]; + for (int i = 0; i < renditionFormats.length; i++) { + renditionFormats[i] = scratchRenditionList.get(i).format; + } + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroupArray(new TrackGroup(renditionFormats)), 0, TrackGroupArray.EMPTY); + } + } + } + private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, Format muxedAudioFormat, List muxedCaptionFormats, long positionUs) { HlsChunkSource defaultChunkSource = diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 4fd27ba2a0..df32b4ed84 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -67,6 +67,9 @@ import java.util.List; /** * Called when the wrapper has been prepared. + * + *

Note: This method will be called on a later handler loop than the one on which either + * {@link #prepareWithMasterPlaylistInfo} or {@link #continuePreparing} are invoked. */ void onPrepared(); @@ -203,7 +206,7 @@ import java.util.List; this.trackGroups = trackGroups; this.optionalTrackGroups = optionalTrackGroups; this.primaryTrackGroupIndex = primaryTrackGroupIndex; - callback.onPrepared(); + handler.post(callback::onPrepared); } public void maybeThrowPrepareError() throws IOException { 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 bb01ade28d..ac14ea6de5 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 @@ -56,6 +56,11 @@ 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. @@ -74,16 +79,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { /* bitrate= */ Format.NO_VALUE, /* selectionFlags= */ 0, /* language= */ null); - return new HlsUrl(url, format); + return new HlsUrl(url, format, /* name= */ ""); } /** * @param url See {@link #url}. * @param format See {@link #format}. + * @param name See {@link #name}. */ - public HlsUrl(String url, Format format) { + public HlsUrl(String url, Format format, String name) { this.url = url; this.format = format; + this.name = name; } } 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 9e13d6fa0f..26e1e1a9a7 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 @@ -330,7 +330,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser