Group audio renditions by name

Allows the player to adapt between audio renditions with different
names.

PiperOrigin-RevId: 233052105
This commit is contained in:
aquilescanta 2019-02-08 14:50:26 +00:00 committed by Andrew Lewis
parent 891ec2230e
commit 36da5fb511
6 changed files with 118 additions and 52 deletions

View File

@ -2,6 +2,8 @@
### dev-v2 (not yet released) ### ### dev-v2 (not yet released) ###
* HLS:
* Form an adaptive track group out of audio renditions with matching name.
* `ExtractorMediaSource` renamed to `ProgressiveMediaSource`. * `ExtractorMediaSource` renamed to `ProgressiveMediaSource`.
* Support for playing spherical videos on Daydream. * Support for playing spherical videos on Daydream.
* Improve decoder re-use between playbacks. TODO: Write and link a blog post * Improve decoder re-use between playbacks. TODO: Write and link a blog post

View File

@ -43,6 +43,7 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.List; import java.util.List;
@ -70,7 +71,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] sampleStreamWrappers;
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
// Maps sample stream wrappers to variant/rendition index by matching array positions. // Maps sample stream wrappers to variant/rendition index by matching array positions.
private int[][] manifestUrlsIndicesPerWrapper; private int[][] manifestUrlIndicesPerWrapper;
private SequenceableLoader compositeSequenceableLoader; private SequenceableLoader compositeSequenceableLoader;
private boolean notifiedReadingStarted; private boolean notifiedReadingStarted;
@ -115,7 +116,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
timestampAdjusterProvider = new TimestampAdjusterProvider(); timestampAdjusterProvider = new TimestampAdjusterProvider();
sampleStreamWrappers = new HlsSampleStreamWrapper[0]; sampleStreamWrappers = new HlsSampleStreamWrapper[0];
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];
manifestUrlsIndicesPerWrapper = new int[0][]; manifestUrlIndicesPerWrapper = new int[0][];
eventDispatcher.mediaPeriodCreated(); eventDispatcher.mediaPeriodCreated();
} }
@ -153,14 +154,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());
boolean hasVariants = !masterPlaylist.variants.isEmpty(); boolean hasVariants = !masterPlaylist.variants.isEmpty();
int audioWrapperOffset = hasVariants ? 1 : 0; 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; TrackGroupArray mainWrapperTrackGroups;
int mainWrapperPrimaryGroupIndex; int mainWrapperPrimaryGroupIndex;
int[] mainWrapperVariantIndices; int[] mainWrapperVariantIndices;
if (hasVariants) { if (hasVariants) {
HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0]; HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0];
mainWrapperVariantIndices = manifestUrlsIndicesPerWrapper[0]; mainWrapperVariantIndices = manifestUrlIndicesPerWrapper[0];
mainWrapperTrackGroups = mainWrapper.getTrackGroups(); mainWrapperTrackGroups = mainWrapper.getTrackGroups();
mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex(); mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex();
} else { } else {
@ -191,12 +193,18 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
// Audio or subtitle group. // Audio or subtitle group.
for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) { for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) {
TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups(); TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups();
if (wrapperTrackGroups.indexOf(trackSelectionGroup) != C.INDEX_UNSET) { int selectedTrackGroupIndex = wrapperTrackGroups.indexOf(trackSelectionGroup);
if (selectedTrackGroupIndex != C.INDEX_UNSET) {
int groupIndexType = int groupIndexType =
i < subtitleWrapperOffset i < subtitleWrapperOffset
? HlsMasterPlaylist.GROUP_INDEX_AUDIO ? HlsMasterPlaylist.GROUP_INDEX_AUDIO
: HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; : 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; break;
} }
} }
@ -424,37 +432,19 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
List<HlsUrl> audioRenditions = masterPlaylist.audios; List<HlsUrl> audioRenditions = masterPlaylist.audios;
List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles; List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles;
int wrapperCount = (hasVariants ? 1 : 0) + audioRenditions.size() + subtitleRenditions.size(); pendingPrepareCount = 0;
sampleStreamWrappers = new HlsSampleStreamWrapper[wrapperCount]; ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();
manifestUrlsIndicesPerWrapper = new int[wrapperCount][]; ArrayList<int[]> manifestUrlIndicesPerWrapper = new ArrayList<>();
pendingPrepareCount = wrapperCount;
int currentWrapperIndex = 0;
if (hasVariants) { if (hasVariants) {
buildAndPrepareMainSampleStreamWrapper(masterPlaylist, positionUs); buildAndPrepareMainSampleStreamWrapper(
currentWrapperIndex++; masterPlaylist, positionUs, sampleStreamWrappers, manifestUrlIndicesPerWrapper);
} }
// TODO: Build video stream wrappers here. // TODO: Build video stream wrappers here.
// Audio sample stream wrappers. buildAndPrepareAudioSampleStreamWrappers(
for (int i = 0; i < audioRenditions.size(); i++) { positionUs, audioRenditions, sampleStreamWrappers, manifestUrlIndicesPerWrapper);
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);
}
}
// Subtitle stream wrappers. We can always use master playlist information to prepare these. // Subtitle stream wrappers. We can always use master playlist information to prepare these.
for (int i = 0; i < subtitleRenditions.size(); i++) { for (int i = 0; i < subtitleRenditions.size(); i++) {
@ -462,20 +452,22 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
HlsSampleStreamWrapper sampleStreamWrapper = HlsSampleStreamWrapper sampleStreamWrapper =
buildSampleStreamWrapper( buildSampleStreamWrapper(
C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, Collections.emptyList(), positionUs); C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, Collections.emptyList(), positionUs);
manifestUrlsIndicesPerWrapper[currentWrapperIndex] = new int[] {i}; manifestUrlIndicesPerWrapper.add(new int[] {i});
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrappers.add(sampleStreamWrapper);
sampleStreamWrapper.prepareWithMasterPlaylistInfo( sampleStreamWrapper.prepareWithMasterPlaylistInfo(
new TrackGroupArray(new TrackGroup(url.format)), 0, TrackGroupArray.EMPTY); 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) // Set timestamp master and trigger preparation (if not already prepared)
sampleStreamWrappers[0].setIsTimestampMaster(true); this.sampleStreamWrappers[0].setIsTimestampMaster(true);
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) {
sampleStreamWrapper.continuePreparing(); sampleStreamWrapper.continuePreparing();
} }
// All wrappers are enabled during preparation. // 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 masterPlaylist The HLS master 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 manifestUrlIndicesPerWrapper List to which the selected variant indices should be added.
*/ */
private void buildAndPrepareMainSampleStreamWrapper( private void buildAndPrepareMainSampleStreamWrapper(
HlsMasterPlaylist masterPlaylist, long positionUs) { HlsMasterPlaylist masterPlaylist,
long positionUs,
List<HlsSampleStreamWrapper> sampleStreamWrappers,
List<int[]> manifestUrlIndicesPerWrapper) {
int[] variantTypes = new int[masterPlaylist.variants.size()]; int[] variantTypes = new int[masterPlaylist.variants.size()];
int videoVariantCount = 0; int videoVariantCount = 0;
int audioVariantCount = 0; int audioVariantCount = 0;
@ -536,13 +533,13 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
selectedVariantsCount = variantTypes.length - audioVariantCount; selectedVariantsCount = variantTypes.length - audioVariantCount;
} }
HlsUrl[] selectedVariants = new HlsUrl[selectedVariantsCount]; HlsUrl[] selectedVariants = new HlsUrl[selectedVariantsCount];
manifestUrlsIndicesPerWrapper[0] = 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 < masterPlaylist.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)) {
selectedVariants[outIndex] = masterPlaylist.variants.get(i); selectedVariants[outIndex] = masterPlaylist.variants.get(i);
manifestUrlsIndicesPerWrapper[0][outIndex++] = i; selectedVariantIndices[outIndex++] = i;
} }
} }
String codecs = selectedVariants[0].format.codecs; String codecs = selectedVariants[0].format.codecs;
@ -553,7 +550,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
masterPlaylist.muxedAudioFormat, masterPlaylist.muxedAudioFormat,
masterPlaylist.muxedCaptionFormats, masterPlaylist.muxedCaptionFormats,
positionUs); positionUs);
sampleStreamWrappers[0] = sampleStreamWrapper; sampleStreamWrappers.add(sampleStreamWrapper);
manifestUrlIndicesPerWrapper.add(selectedVariantIndices);
if (allowChunklessPreparation && codecs != null) { if (allowChunklessPreparation && codecs != null) {
boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null; boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;
boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != 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<HlsUrl> audioRenditions,
List<HlsSampleStreamWrapper> sampleStreamWrappers,
List<int[]> manifestUrlsIndicesPerWrapper) {
ArrayList<HlsUrl> scratchRenditionList =
new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
ArrayList<Integer> scratchIndicesList =
new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
HashSet<String> 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, private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants,
Format muxedAudioFormat, List<Format> muxedCaptionFormats, long positionUs) { Format muxedAudioFormat, List<Format> muxedCaptionFormats, long positionUs) {
HlsChunkSource defaultChunkSource = HlsChunkSource defaultChunkSource =

View File

@ -67,6 +67,9 @@ import java.util.List;
/** /**
* 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
* {@link #prepareWithMasterPlaylistInfo} or {@link #continuePreparing} are invoked.
*/ */
void onPrepared(); void onPrepared();
@ -203,7 +206,7 @@ import java.util.List;
this.trackGroups = trackGroups; this.trackGroups = trackGroups;
this.optionalTrackGroups = optionalTrackGroups; this.optionalTrackGroups = optionalTrackGroups;
this.primaryTrackGroupIndex = primaryTrackGroupIndex; this.primaryTrackGroupIndex = primaryTrackGroupIndex;
callback.onPrepared(); handler.post(callback::onPrepared);
} }
public void maybeThrowPrepareError() throws IOException { public void maybeThrowPrepareError() throws IOException {

View File

@ -56,6 +56,11 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
* Format information associated with the HLS url. * Format information associated with the HLS url.
*/ */
public final Format format; 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. * Creates an HLS url from a given http url.
@ -74,16 +79,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
/* bitrate= */ Format.NO_VALUE, /* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* language= */ null); /* language= */ null);
return new HlsUrl(url, format); return new HlsUrl(url, format, /* name= */ "");
} }
/** /**
* @param url See {@link #url}. * @param url See {@link #url}.
* @param format See {@link #format}. * @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.url = url;
this.format = format; this.format = format;
this.name = name;
} }
} }

View File

@ -330,7 +330,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
frameRate, frameRate,
/* initializationData= */ null, /* initializationData= */ null,
/* selectionFlags= */ 0); /* selectionFlags= */ 0);
variants.add(new HlsMasterPlaylist.HlsUrl(line, format)); variants.add(new HlsMasterPlaylist.HlsUrl(line, format, /* name= */ ""));
} }
} }
} }
@ -365,7 +365,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (isMediaTagMuxed(variants, uri)) { if (isMediaTagMuxed(variants, uri)) {
muxedAudioFormat = format; muxedAudioFormat = format;
} else { } else {
audios.add(new HlsMasterPlaylist.HlsUrl(uri, format)); audios.add(new HlsMasterPlaylist.HlsUrl(uri, format, name));
} }
break; break;
case TYPE_SUBTITLES: case TYPE_SUBTITLES:
@ -379,7 +379,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* bitrate= */ Format.NO_VALUE, /* bitrate= */ Format.NO_VALUE,
selectionFlags, selectionFlags,
language); language);
subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format)); subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format, name));
break; break;
case TYPE_CLOSED_CAPTIONS: case TYPE_CLOSED_CAPTIONS:
String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions); String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);

View File

@ -49,7 +49,7 @@ import org.robolectric.annotation.Config;
public final class HlsMediaPeriodTest { public final class HlsMediaPeriodTest {
@Test @Test
public void getSteamKeys_isCompatibleWithhHlsMasterPlaylistFilter() { public void getSteamKeys_isCompatibleWithHlsMasterPlaylistFilter() {
HlsMasterPlaylist testMasterPlaylist = HlsMasterPlaylist testMasterPlaylist =
createMasterPlaylist( createMasterPlaylist(
/* variants= */ Arrays.asList( /* variants= */ Arrays.asList(
@ -127,7 +127,8 @@ public final class HlsMediaPeriodTest {
/* height= */ Format.NO_VALUE, /* height= */ Format.NO_VALUE,
/* frameRate= */ Format.NO_VALUE, /* frameRate= */ Format.NO_VALUE,
/* initializationData= */ null, /* initializationData= */ null,
/* selectionFlags= */ 0)); /* selectionFlags= */ 0),
/* name= */ "");
} }
private static HlsUrl createAudioOnlyVariantHlsUrl(int bitrate) { private static HlsUrl createAudioOnlyVariantHlsUrl(int bitrate) {
@ -144,15 +145,16 @@ public final class HlsMediaPeriodTest {
/* height= */ Format.NO_VALUE, /* height= */ Format.NO_VALUE,
/* frameRate= */ Format.NO_VALUE, /* frameRate= */ Format.NO_VALUE,
/* initializationData= */ null, /* initializationData= */ null,
/* selectionFlags= */ 0)); /* selectionFlags= */ 0),
/* name= */ "");
} }
private static HlsUrl createAudioHlsUrl(String language) { private static HlsUrl createAudioHlsUrl(String language) {
return new HlsUrl("http://url", createAudioFormat(language)); return new HlsUrl("http://url", createAudioFormat(language), /* name= */ "");
} }
private static HlsUrl createSubtitleHlsUrl(String language) { private static HlsUrl createSubtitleHlsUrl(String language) {
return new HlsUrl("http://url", createSubtitleFormat(language)); return new HlsUrl("http://url", createSubtitleFormat(language), /* name= */ "");
} }
private static Format createAudioFormat(String language) { private static Format createAudioFormat(String language) {