blob: e1a441f36faa0c7b5fce434088bb6e408a0b4d80 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.dash;
import android.util.Pair;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.EmptySampleStream;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
import com.google.android.exoplayer2.source.dash.manifest.EventStream;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** A DASH {@link MediaPeriod}. */
/* package */ final class DashMediaPeriod
implements MediaPeriod,
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>>,
ChunkSampleStream.ReleaseCallback<DashChunkSource> {
private static final Pattern CEA608_SERVICE_DESCRIPTOR_REGEX = Pattern.compile("CC([1-4])=(.+)");
/* package */ final int id;
private final DashChunkSource.Factory chunkSourceFactory;
@Nullable private final TransferListener transferListener;
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final long elapsedRealtimeOffsetMs;
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final Allocator allocator;
private final TrackGroupArray trackGroups;
private final TrackGroupInfo[] trackGroupInfos;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final PlayerEmsgHandler playerEmsgHandler;
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
trackEmsgHandlerBySampleStream;
private final EventDispatcher eventDispatcher;
@Nullable private Callback callback;
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
private EventSampleStream[] eventSampleStreams;
private SequenceableLoader compositeSequenceableLoader;
private DashManifest manifest;
private int periodIndex;
private List<EventStream> eventStreams;
private boolean notifiedReadingStarted;
public DashMediaPeriod(
int id,
DashManifest manifest,
int periodIndex,
DashChunkSource.Factory chunkSourceFactory,
@Nullable TransferListener transferListener,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
EventDispatcher eventDispatcher,
long elapsedRealtimeOffsetMs,
LoaderErrorThrower manifestLoaderErrorThrower,
Allocator allocator,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
PlayerEmsgCallback playerEmsgCallback) {
this.id = id;
this.manifest = manifest;
this.periodIndex = periodIndex;
this.chunkSourceFactory = chunkSourceFactory;
this.transferListener = transferListener;
this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.eventDispatcher = eventDispatcher;
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator);
sampleStreams = newSampleStreamArray(0);
eventSampleStreams = new EventSampleStream[0];
trackEmsgHandlerBySampleStream = new IdentityHashMap<>();
compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
Period period = manifest.getPeriod(periodIndex);
eventStreams = period.eventStreams;
Pair<TrackGroupArray, TrackGroupInfo[]> result =
buildTrackGroups(drmSessionManager, period.adaptationSets, eventStreams);
trackGroups = result.first;
trackGroupInfos = result.second;
eventDispatcher.mediaPeriodCreated();
}
/**
* Updates the {@link DashManifest} and the index of this period in the manifest.
*
* @param manifest The updated manifest.
* @param periodIndex the new index of this period in the updated manifest.
*/
public void updateManifest(DashManifest manifest, int periodIndex) {
this.manifest = manifest;
this.periodIndex = periodIndex;
playerEmsgHandler.updateManifest(manifest);
if (sampleStreams != null) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.getChunkSource().updateManifest(manifest, periodIndex);
}
callback.onContinueLoadingRequested(this);
}
eventStreams = manifest.getPeriod(periodIndex).eventStreams;
for (EventSampleStream eventSampleStream : eventSampleStreams) {
for (EventStream eventStream : eventStreams) {
if (eventStream.id().equals(eventSampleStream.eventStreamId())) {
int lastPeriodIndex = manifest.getPeriodCount() - 1;
eventSampleStream.updateEventStream(
eventStream,
/* eventStreamAppendable= */ manifest.dynamic && periodIndex == lastPeriodIndex);
break;
}
}
}
}
public void release() {
playerEmsgHandler.release();
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.release(this);
}
callback = null;
eventDispatcher.mediaPeriodReleased();
}
// ChunkSampleStream.ReleaseCallback implementation.
@Override
public synchronized void onSampleStreamReleased(ChunkSampleStream<DashChunkSource> stream) {
PlayerTrackEmsgHandler trackEmsgHandler = trackEmsgHandlerBySampleStream.remove(stream);
if (trackEmsgHandler != null) {
trackEmsgHandler.release();
}
}
// MediaPeriod implementation.
@Override
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
callback.onPrepared(this);
}
@Override
public void maybeThrowPrepareError() throws IOException {
manifestLoaderErrorThrower.maybeThrowError();
}
@Override
public TrackGroupArray getTrackGroups() {
return trackGroups;
}
@Override
public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
List<StreamKey> streamKeys = new ArrayList<>();
for (TrackSelection trackSelection : trackSelections) {
int trackGroupIndex = trackGroups.indexOf(trackSelection.getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory != TrackGroupInfo.CATEGORY_PRIMARY) {
// Ignore non-primary tracks.
continue;
}
int[] adaptationSetIndices = trackGroupInfo.adaptationSetIndices;
int[] trackIndices = new int[trackSelection.length()];
for (int i = 0; i < trackSelection.length(); i++) {
trackIndices[i] = trackSelection.getIndexInTrackGroup(i);
}
Arrays.sort(trackIndices);
int currentAdaptationSetIndex = 0;
int totalTracksInPreviousAdaptationSets = 0;
int tracksInCurrentAdaptationSet =
manifestAdaptationSets.get(adaptationSetIndices[0]).representations.size();
for (int trackIndex : trackIndices) {
while (trackIndex >= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) {
currentAdaptationSetIndex++;
totalTracksInPreviousAdaptationSets += tracksInCurrentAdaptationSet;
tracksInCurrentAdaptationSet =
manifestAdaptationSets
.get(adaptationSetIndices[currentAdaptationSetIndex])
.representations
.size();
}
streamKeys.add(
new StreamKey(
periodIndex,
adaptationSetIndices[currentAdaptationSetIndex],
trackIndex - totalTracksInPreviousAdaptationSets));
}
}
return streamKeys;
}
@Override
public long selectTracks(
@NullableType TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs) {
int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections);
releaseDisabledStreams(selections, mayRetainStreamFlags, streams);
releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex);
selectNewStreams(
selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex);
ArrayList<ChunkSampleStream<DashChunkSource>> sampleStreamList = new ArrayList<>();
ArrayList<EventSampleStream> eventSampleStreamList = new ArrayList<>();
for (SampleStream sampleStream : streams) {
if (sampleStream instanceof ChunkSampleStream) {
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream =
(ChunkSampleStream<DashChunkSource>) sampleStream;
sampleStreamList.add(stream);
} else if (sampleStream instanceof EventSampleStream) {
eventSampleStreamList.add((EventSampleStream) sampleStream);
}
}
sampleStreams = newSampleStreamArray(sampleStreamList.size());
sampleStreamList.toArray(sampleStreams);
eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()];
eventSampleStreamList.toArray(eventSampleStreams);
compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
return positionUs;
}
@Override
public void discardBuffer(long positionUs, boolean toKeyframe) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.discardBuffer(positionUs, toKeyframe);
}
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);
}
@Override
public boolean isLoading() {
return compositeSequenceableLoader.isLoading();
}
@Override
public long getNextLoadPositionUs() {
return compositeSequenceableLoader.getNextLoadPositionUs();
}
@Override
public long readDiscontinuity() {
if (!notifiedReadingStarted) {
eventDispatcher.readingStarted();
notifiedReadingStarted = true;
}
return C.TIME_UNSET;
}
@Override
public long getBufferedPositionUs() {
return compositeSequenceableLoader.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.seekToUs(positionUs);
}
for (EventSampleStream sampleStream : eventSampleStreams) {
sampleStream.seekToUs(positionUs);
}
return positionUs;
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {
return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);
}
}
return positionUs;
}
// SequenceableLoader.Callback implementation.
@Override
public void onContinueLoadingRequested(ChunkSampleStream<DashChunkSource> sampleStream) {
callback.onContinueLoadingRequested(this);
}
// Internal methods.
private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) {
int[] streamIndexToTrackGroupIndex = new int[selections.length];
for (int i = 0; i < selections.length; i++) {
if (selections[i] != null) {
streamIndexToTrackGroupIndex[i] = trackGroups.indexOf(selections[i].getTrackGroup());
} else {
streamIndexToTrackGroupIndex[i] = C.INDEX_UNSET;
}
}
return streamIndexToTrackGroupIndex;
}
private void releaseDisabledStreams(
TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) {
for (int i = 0; i < selections.length; i++) {
if (selections[i] == null || !mayRetainStreamFlags[i]) {
if (streams[i] instanceof ChunkSampleStream) {
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream =
(ChunkSampleStream<DashChunkSource>) streams[i];
stream.release(this);
} else if (streams[i] instanceof EmbeddedSampleStream) {
((EmbeddedSampleStream) streams[i]).release();
}
streams[i] = null;
}
}
}
private void releaseOrphanEmbeddedStreams(
TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) {
for (int i = 0; i < selections.length; i++) {
if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) {
// We need to release an embedded stream if the corresponding primary stream is released.
int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);
boolean mayRetainStream;
if (primaryStreamIndex == C.INDEX_UNSET) {
// If the corresponding primary stream is not selected, we may retain an existing
// EmptySampleStream.
mayRetainStream = streams[i] instanceof EmptySampleStream;
} else {
// If the corresponding primary stream is selected, we may retain the embedded stream if
// the stream's parent still matches.
mayRetainStream =
(streams[i] instanceof EmbeddedSampleStream)
&& ((EmbeddedSampleStream) streams[i]).parent == streams[primaryStreamIndex];
}
if (!mayRetainStream) {
if (streams[i] instanceof EmbeddedSampleStream) {
((EmbeddedSampleStream) streams[i]).release();
}
streams[i] = null;
}
}
}
}
private void selectNewStreams(
TrackSelection[] selections,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs,
int[] streamIndexToTrackGroupIndex) {
// Create newly selected primary and event streams.
for (int i = 0; i < selections.length; i++) {
TrackSelection selection = selections[i];
if (selection == null) {
continue;
}
if (streams[i] == null) {
// Create new stream for selection.
streamResetFlags[i] = true;
int trackGroupIndex = streamIndexToTrackGroupIndex[i];
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) {
streams[i] = buildSampleStream(trackGroupInfo, selection, positionUs);
} else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) {
EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex);
Format format = selection.getTrackGroup().getFormat(0);
streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic);
}
} else if (streams[i] instanceof ChunkSampleStream) {
// Update selection in existing stream.
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];
stream.getChunkSource().updateTrackSelection(selection);
}
}
// Create newly selected embedded streams from the corresponding primary stream. Note that this
// second pass is needed because the primary stream may not have been created yet in a first
// pass if the index of the primary stream is greater than the index of the embedded stream.
for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) {
int trackGroupIndex = streamIndexToTrackGroupIndex[i];
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) {
int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);
if (primaryStreamIndex == C.INDEX_UNSET) {
// If an embedded track is selected without the corresponding primary track, create an
// empty sample stream instead.
streams[i] = new EmptySampleStream();
} else {
streams[i] =
((ChunkSampleStream) streams[primaryStreamIndex])
.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType);
}
}
}
}
}
private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTrackGroupIndex) {
int embeddedTrackGroupIndex = streamIndexToTrackGroupIndex[embeddedStreamIndex];
if (embeddedTrackGroupIndex == C.INDEX_UNSET) {
return C.INDEX_UNSET;
}
int primaryTrackGroupIndex = trackGroupInfos[embeddedTrackGroupIndex].primaryTrackGroupIndex;
for (int i = 0; i < streamIndexToTrackGroupIndex.length; i++) {
int trackGroupIndex = streamIndexToTrackGroupIndex[i];
if (trackGroupIndex == primaryTrackGroupIndex
&& trackGroupInfos[trackGroupIndex].trackGroupCategory
== TrackGroupInfo.CATEGORY_PRIMARY) {
return i;
}
}
return C.INDEX_UNSET;
}
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
DrmSessionManager drmSessionManager,
List<AdaptationSet> adaptationSets,
List<EventStream> eventStreams) {
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
int primaryGroupCount = groupedAdaptationSetIndices.length;
boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];
Format[][] primaryGroupCea608TrackFormats = new Format[primaryGroupCount][];
int totalEmbeddedTrackGroupCount =
identifyEmbeddedTracks(
primaryGroupCount,
adaptationSets,
groupedAdaptationSetIndices,
primaryGroupHasEventMessageTrackFlags,
primaryGroupCea608TrackFormats);
int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount + eventStreams.size();
TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];
TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];
int trackGroupCount =
buildPrimaryAndEmbeddedTrackGroupInfos(
drmSessionManager,
adaptationSets,
groupedAdaptationSetIndices,
primaryGroupCount,
primaryGroupHasEventMessageTrackFlags,
primaryGroupCea608TrackFormats,
trackGroups,
trackGroupInfos);
buildManifestEventTrackGroupInfos(eventStreams, trackGroups, trackGroupInfos, trackGroupCount);
return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);
}
/**
* Groups adaptation sets. Two adaptations sets belong to the same group if either:
*
* <ul>
* <li>One is a trick-play adaptation set and uses a {@code
* http://dashif.org/guidelines/trickmode} essential or supplemental property to indicate
* that the other is the main adaptation set to which it corresponds.
* <li>The two adaptation sets are marked as safe for switching using {@code
* urn:mpeg:dash:adaptation-set-switching:2016} supplemental properties.
* </ul>
*
* @param adaptationSets The adaptation sets to merge.
* @return An array of groups, where each group is an array of adaptation set indices.
*/
private static int[][] getGroupedAdaptationSetIndices(List<AdaptationSet> adaptationSets) {
int adaptationSetCount = adaptationSets.size();
SparseIntArray adaptationSetIdToIndex = new SparseIntArray(adaptationSetCount);
List<List<Integer>> adaptationSetGroupedIndices = new ArrayList<>(adaptationSetCount);
SparseArray<List<Integer>> adaptationSetIndexToGroupedIndices =
new SparseArray<>(adaptationSetCount);
// Initially make each adaptation set belong to its own group. Also build the
// adaptationSetIdToIndex map.
for (int i = 0; i < adaptationSetCount; i++) {
adaptationSetIdToIndex.put(adaptationSets.get(i).id, i);
List<Integer> initialGroup = new ArrayList<>();
initialGroup.add(i);
adaptationSetGroupedIndices.add(initialGroup);
adaptationSetIndexToGroupedIndices.put(i, initialGroup);
}
// Merge adaptation set groups.
for (int i = 0; i < adaptationSetCount; i++) {
int mergedGroupIndex = i;
AdaptationSet adaptationSet = adaptationSets.get(i);
// Trick-play adaptation sets are merged with their corresponding main adaptation sets.
@Nullable
Descriptor trickPlayProperty = findTrickPlayProperty(adaptationSet.essentialProperties);
if (trickPlayProperty == null) {
// Trick-play can also be specified using a supplemental property.
trickPlayProperty = findTrickPlayProperty(adaptationSet.supplementalProperties);
}
if (trickPlayProperty != null) {
int mainAdaptationSetId = Integer.parseInt(trickPlayProperty.value);
int mainAdaptationSetIndex =
adaptationSetIdToIndex.get(mainAdaptationSetId, /* valueIfKeyNotFound= */ -1);
if (mainAdaptationSetIndex != -1) {
mergedGroupIndex = mainAdaptationSetIndex;
}
}
// Adaptation sets that are safe for switching are merged, using the smallest index for the
// merged group.
if (mergedGroupIndex == i) {
@Nullable
Descriptor adaptationSetSwitchingProperty =
findAdaptationSetSwitchingProperty(adaptationSet.supplementalProperties);
if (adaptationSetSwitchingProperty != null) {
String[] otherAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ",");
for (String adaptationSetId : otherAdaptationSetIds) {
int otherAdaptationSetId =
adaptationSetIdToIndex.get(
Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1);
if (otherAdaptationSetId != -1) {
mergedGroupIndex = Math.min(mergedGroupIndex, otherAdaptationSetId);
}
}
}
}
// Merge the groups if necessary.
if (mergedGroupIndex != i) {
List<Integer> thisGroup = adaptationSetIndexToGroupedIndices.get(i);
List<Integer> mergedGroup = adaptationSetIndexToGroupedIndices.get(mergedGroupIndex);
mergedGroup.addAll(thisGroup);
adaptationSetIndexToGroupedIndices.put(i, mergedGroup);
adaptationSetGroupedIndices.remove(thisGroup);
}
}
int[][] groupedAdaptationSetIndices = new int[adaptationSetGroupedIndices.size()][];
for (int i = 0; i < groupedAdaptationSetIndices.length; i++) {
groupedAdaptationSetIndices[i] = Util.toArray(adaptationSetGroupedIndices.get(i));
// Restore the original adaptation set order within each group.
Arrays.sort(groupedAdaptationSetIndices[i]);
}
return groupedAdaptationSetIndices;
}
/**
* Iterates through list of primary track groups and identifies embedded tracks.
*
* @param primaryGroupCount The number of primary track groups.
* @param adaptationSets The list of {@link AdaptationSet} of the current DASH period.
* @param groupedAdaptationSetIndices The indices of {@link AdaptationSet} that belongs to the
* same primary group, grouped in primary track groups order.
* @param primaryGroupHasEventMessageTrackFlags An output array to be filled with flags indicating
* whether each of the primary track groups contains an embedded event message track.
* @param primaryGroupCea608TrackFormats An output array to be filled with track formats for
* CEA-608 tracks embedded in each of the primary track groups.
* @return Total number of embedded track groups.
*/
private static int identifyEmbeddedTracks(
int primaryGroupCount,
List<AdaptationSet> adaptationSets,
int[][] groupedAdaptationSetIndices,
boolean[] primaryGroupHasEventMessageTrackFlags,
Format[][] primaryGroupCea608TrackFormats) {
int numEmbeddedTrackGroups = 0;
for (int i = 0; i < primaryGroupCount; i++) {
if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {
primaryGroupHasEventMessageTrackFlags[i] = true;
numEmbeddedTrackGroups++;
}
primaryGroupCea608TrackFormats[i] =
getCea608TrackFormats(adaptationSets, groupedAdaptationSetIndices[i]);
if (primaryGroupCea608TrackFormats[i].length != 0) {
numEmbeddedTrackGroups++;
}
}
return numEmbeddedTrackGroups;
}
private static int buildPrimaryAndEmbeddedTrackGroupInfos(
DrmSessionManager drmSessionManager,
List<AdaptationSet> adaptationSets,
int[][] groupedAdaptationSetIndices,
int primaryGroupCount,
boolean[] primaryGroupHasEventMessageTrackFlags,
Format[][] primaryGroupCea608TrackFormats,
TrackGroup[] trackGroups,
TrackGroupInfo[] trackGroupInfos) {
int trackGroupCount = 0;
for (int i = 0; i < primaryGroupCount; i++) {
int[] adaptationSetIndices = groupedAdaptationSetIndices[i];
List<Representation> representations = new ArrayList<>();
for (int adaptationSetIndex : adaptationSetIndices) {
representations.addAll(adaptationSets.get(adaptationSetIndex).representations);
}
Format[] formats = new Format[representations.size()];
for (int j = 0; j < formats.length; j++) {
Format format = representations.get(j).format;
DrmInitData drmInitData = format.drmInitData;
if (drmInitData != null) {
format =
format.copyWithExoMediaCryptoType(
drmSessionManager.getExoMediaCryptoType(drmInitData));
}
formats[j] = format;
}
AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);
int primaryTrackGroupIndex = trackGroupCount++;
int eventMessageTrackGroupIndex =
primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET;
int cea608TrackGroupIndex =
primaryGroupCea608TrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET;
trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats);
trackGroupInfos[primaryTrackGroupIndex] =
TrackGroupInfo.primaryTrack(
firstAdaptationSet.type,
adaptationSetIndices,
primaryTrackGroupIndex,
eventMessageTrackGroupIndex,
cea608TrackGroupIndex);
if (eventMessageTrackGroupIndex != C.INDEX_UNSET) {
Format format =
new Format.Builder()
.setId(firstAdaptationSet.id + ":emsg")
.setSampleMimeType(MimeTypes.APPLICATION_EMSG)
.build();
trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format);
trackGroupInfos[eventMessageTrackGroupIndex] =
TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex);
}
if (cea608TrackGroupIndex != C.INDEX_UNSET) {
trackGroups[cea608TrackGroupIndex] = new TrackGroup(primaryGroupCea608TrackFormats[i]);
trackGroupInfos[cea608TrackGroupIndex] =
TrackGroupInfo.embeddedCea608Track(adaptationSetIndices, primaryTrackGroupIndex);
}
}
return trackGroupCount;
}
private static void buildManifestEventTrackGroupInfos(List<EventStream> eventStreams,
TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos, int existingTrackGroupCount) {
for (int i = 0; i < eventStreams.size(); i++) {
EventStream eventStream = eventStreams.get(i);
Format format =
new Format.Builder()
.setId(eventStream.id())
.setSampleMimeType(MimeTypes.APPLICATION_EMSG)
.build();
trackGroups[existingTrackGroupCount] = new TrackGroup(format);
trackGroupInfos[existingTrackGroupCount++] = TrackGroupInfo.mpdEventTrack(i);
}
}
private ChunkSampleStream<DashChunkSource> buildSampleStream(TrackGroupInfo trackGroupInfo,
TrackSelection selection, long positionUs) {
int embeddedTrackCount = 0;
boolean enableEventMessageTrack =
trackGroupInfo.embeddedEventMessageTrackGroupIndex != C.INDEX_UNSET;
TrackGroup embeddedEventMessageTrackGroup = null;
if (enableEventMessageTrack) {
embeddedEventMessageTrackGroup =
trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex);
embeddedTrackCount++;
}
boolean enableCea608Tracks = trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET;
TrackGroup embeddedCea608TrackGroup = null;
if (enableCea608Tracks) {
embeddedCea608TrackGroup = trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex);
embeddedTrackCount += embeddedCea608TrackGroup.length;
}
Format[] embeddedTrackFormats = new Format[embeddedTrackCount];
int[] embeddedTrackTypes = new int[embeddedTrackCount];
embeddedTrackCount = 0;
if (enableEventMessageTrack) {
embeddedTrackFormats[embeddedTrackCount] = embeddedEventMessageTrackGroup.getFormat(0);
embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_METADATA;
embeddedTrackCount++;
}
List<Format> embeddedCea608TrackFormats = new ArrayList<>();
if (enableCea608Tracks) {
for (int i = 0; i < embeddedCea608TrackGroup.length; i++) {
embeddedTrackFormats[embeddedTrackCount] = embeddedCea608TrackGroup.getFormat(i);
embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT;
embeddedCea608TrackFormats.add(embeddedTrackFormats[embeddedTrackCount]);
embeddedTrackCount++;
}
}
PlayerTrackEmsgHandler trackPlayerEmsgHandler =
manifest.dynamic && enableEventMessageTrack
? playerEmsgHandler.newPlayerTrackEmsgHandler()
: null;
DashChunkSource chunkSource =
chunkSourceFactory.createDashChunkSource(
manifestLoaderErrorThrower,
manifest,
periodIndex,
trackGroupInfo.adaptationSetIndices,
selection,
trackGroupInfo.trackType,
elapsedRealtimeOffsetMs,
enableEventMessageTrack,
embeddedCea608TrackFormats,
trackPlayerEmsgHandler,
transferListener);
ChunkSampleStream<DashChunkSource> stream =
new ChunkSampleStream<>(
trackGroupInfo.trackType,
embeddedTrackTypes,
embeddedTrackFormats,
chunkSource,
this,
allocator,
positionUs,
drmSessionManager,
loadErrorHandlingPolicy,
eventDispatcher);
synchronized (this) {
// The map is also accessed on the loading thread so synchronize access.
trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler);
}
return stream;
}
@Nullable
private static Descriptor findAdaptationSetSwitchingProperty(List<Descriptor> descriptors) {
return findDescriptor(descriptors, "urn:mpeg:dash:adaptation-set-switching:2016");
}
@Nullable
private static Descriptor findTrickPlayProperty(List<Descriptor> descriptors) {
return findDescriptor(descriptors, "http://dashif.org/guidelines/trickmode");
}
@Nullable
private static Descriptor findDescriptor(List<Descriptor> descriptors, String schemeIdUri) {
for (int i = 0; i < descriptors.size(); i++) {
Descriptor descriptor = descriptors.get(i);
if (schemeIdUri.equals(descriptor.schemeIdUri)) {
return descriptor;
}
}
return null;
}
private static boolean hasEventMessageTrack(List<AdaptationSet> adaptationSets,
int[] adaptationSetIndices) {
for (int i : adaptationSetIndices) {
List<Representation> representations = adaptationSets.get(i).representations;
for (int j = 0; j < representations.size(); j++) {
Representation representation = representations.get(j);
if (!representation.inbandEventStreams.isEmpty()) {
return true;
}
}
}
return false;
}
private static Format[] getCea608TrackFormats(
List<AdaptationSet> adaptationSets, int[] adaptationSetIndices) {
for (int i : adaptationSetIndices) {
AdaptationSet adaptationSet = adaptationSets.get(i);
List<Descriptor> descriptors = adaptationSets.get(i).accessibilityDescriptors;
for (int j = 0; j < descriptors.size(); j++) {
Descriptor descriptor = descriptors.get(j);
if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) {
@Nullable String value = descriptor.value;
if (value == null) {
// There are embedded CEA-608 tracks, but service information is not declared.
return new Format[] {buildCea608TrackFormat(adaptationSet.id)};
}
String[] services = Util.split(value, ";");
Format[] formats = new Format[services.length];
for (int k = 0; k < services.length; k++) {
Matcher matcher = CEA608_SERVICE_DESCRIPTOR_REGEX.matcher(services[k]);
if (!matcher.matches()) {
// If we can't parse service information for all services, assume a single track.
return new Format[] {buildCea608TrackFormat(adaptationSet.id)};
}
formats[k] =
buildCea608TrackFormat(
adaptationSet.id,
/* language= */ matcher.group(2),
/* accessibilityChannel= */ Integer.parseInt(matcher.group(1)));
}
return formats;
}
}
}
return new Format[0];
}
private static Format buildCea608TrackFormat(int adaptationSetId) {
return buildCea608TrackFormat(
adaptationSetId, /* language= */ null, /* accessibilityChannel= */ Format.NO_VALUE);
}
private static Format buildCea608TrackFormat(
int adaptationSetId, @Nullable String language, int accessibilityChannel) {
String id =
adaptationSetId
+ ":cea608"
+ (accessibilityChannel != Format.NO_VALUE ? ":" + accessibilityChannel : "");
return new Format.Builder()
.setId(id)
.setSampleMimeType(MimeTypes.APPLICATION_CEA608)
.setLanguage(language)
.setAccessibilityChannel(accessibilityChannel)
.build();
}
// We won't assign the array to a variable that erases the generic type, and then write into it.
@SuppressWarnings({"unchecked", "rawtypes"})
private static ChunkSampleStream<DashChunkSource>[] newSampleStreamArray(int length) {
return new ChunkSampleStream[length];
}
private static final class TrackGroupInfo {
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({CATEGORY_PRIMARY, CATEGORY_EMBEDDED, CATEGORY_MANIFEST_EVENTS})
public @interface TrackGroupCategory {}
/**
* A normal track group that has its samples drawn from the stream.
* For example: a video Track Group or an audio Track Group.
*/
private static final int CATEGORY_PRIMARY = 0;
/**
* A track group whose samples are embedded within one of the primary streams. For example: an
* EMSG track has its sample embedded in emsg atoms in one of the primary streams.
*/
private static final int CATEGORY_EMBEDDED = 1;
/**
* A track group that has its samples listed explicitly in the DASH manifest file.
* For example: an EventStream track has its sample (Events) included directly in the DASH
* manifest file.
*/
private static final int CATEGORY_MANIFEST_EVENTS = 2;
public final int[] adaptationSetIndices;
public final int trackType;
@TrackGroupCategory public final int trackGroupCategory;
public final int eventStreamGroupIndex;
public final int primaryTrackGroupIndex;
public final int embeddedEventMessageTrackGroupIndex;
public final int embeddedCea608TrackGroupIndex;
public static TrackGroupInfo primaryTrack(
int trackType,
int[] adaptationSetIndices,
int primaryTrackGroupIndex,
int embeddedEventMessageTrackGroupIndex,
int embeddedCea608TrackGroupIndex) {
return new TrackGroupInfo(
trackType,
CATEGORY_PRIMARY,
adaptationSetIndices,
primaryTrackGroupIndex,
embeddedEventMessageTrackGroupIndex,
embeddedCea608TrackGroupIndex,
/* eventStreamGroupIndex= */ -1);
}
public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices,
int primaryTrackGroupIndex) {
return new TrackGroupInfo(
C.TRACK_TYPE_METADATA,
CATEGORY_EMBEDDED,
adaptationSetIndices,
primaryTrackGroupIndex,
C.INDEX_UNSET,
C.INDEX_UNSET,
/* eventStreamGroupIndex= */ -1);
}
public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices,
int primaryTrackGroupIndex) {
return new TrackGroupInfo(
C.TRACK_TYPE_TEXT,
CATEGORY_EMBEDDED,
adaptationSetIndices,
primaryTrackGroupIndex,
C.INDEX_UNSET,
C.INDEX_UNSET,
/* eventStreamGroupIndex= */ -1);
}
public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) {
return new TrackGroupInfo(
C.TRACK_TYPE_METADATA,
CATEGORY_MANIFEST_EVENTS,
new int[0],
/* primaryTrackGroupIndex= */ -1,
C.INDEX_UNSET,
C.INDEX_UNSET,
eventStreamIndex);
}
private TrackGroupInfo(
int trackType,
@TrackGroupCategory int trackGroupCategory,
int[] adaptationSetIndices,
int primaryTrackGroupIndex,
int embeddedEventMessageTrackGroupIndex,
int embeddedCea608TrackGroupIndex,
int eventStreamGroupIndex) {
this.trackType = trackType;
this.adaptationSetIndices = adaptationSetIndices;
this.trackGroupCategory = trackGroupCategory;
this.primaryTrackGroupIndex = primaryTrackGroupIndex;
this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex;
this.embeddedCea608TrackGroupIndex = embeddedCea608TrackGroupIndex;
this.eventStreamGroupIndex = eventStreamGroupIndex;
}
}
}