result = buildTrackGroups(period.adaptationSets,
+ eventStreams);
trackGroups = result.first;
trackGroupInfos = result.second;
}
+ /**
+ * 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;
+ Period period = manifest.getPeriod(periodIndex);
if (sampleStreams != null) {
for (ChunkSampleStream sampleStream : sampleStreams) {
sampleStream.getChunkSource().updateManifest(manifest, periodIndex);
}
callback.onContinueLoadingRequested(this);
}
+ eventStreams = period.eventStreams;
+ for (EventSampleStream eventSampleStream : eventSampleStreams) {
+ for (EventStream eventStream : eventStreams) {
+ if (eventStream.id().equals(eventSampleStream.eventStreamId())) {
+ eventSampleStream.updateEventStream(eventStream, manifest.dynamic);
+ break;
+ }
+ }
+ }
}
public void release() {
@@ -122,8 +149,27 @@ import java.util.List;
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
- HashMap> primarySampleStreams = new HashMap<>();
- // First pass for primary tracks.
+ Map> primarySampleStreams = new HashMap<>();
+ List eventSampleStreamList = new ArrayList<>();
+
+ selectPrimarySampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags,
+ positionUs, primarySampleStreams);
+ selectEventSampleStreams(selections, mayRetainStreamFlags, streams,
+ streamResetFlags, eventSampleStreamList);
+ selectEmbeddedSampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags,
+ positionUs, primarySampleStreams);
+
+ sampleStreams = newSampleStreamArray(primarySampleStreams.size());
+ primarySampleStreams.values().toArray(sampleStreams);
+ eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()];
+ eventSampleStreamList.toArray(eventSampleStreams);
+ sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
+ return positionUs;
+ }
+
+ private void selectPrimarySampleStreams(TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags,
+ long positionUs, Map> primarySampleStreams) {
for (int i = 0; i < selections.length; i++) {
if (streams[i] instanceof ChunkSampleStream) {
@SuppressWarnings("unchecked")
@@ -136,10 +182,11 @@ import java.util.List;
primarySampleStreams.put(trackGroupIndex, stream);
}
}
+
if (streams[i] == null && selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
- if (trackGroupInfo.isPrimary) {
+ if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) {
ChunkSampleStream stream = buildSampleStream(trackGroupInfo,
selections[i], positionUs);
primarySampleStreams.put(trackGroupIndex, stream);
@@ -148,7 +195,39 @@ import java.util.List;
}
}
}
- // Second pass for embedded tracks.
+ }
+
+ private void selectEventSampleStreams(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags,
+ List eventSampleStreamsList) {
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] instanceof EventSampleStream) {
+ EventSampleStream stream = (EventSampleStream) streams[i];
+ if (selections[i] == null || !mayRetainStreamFlags[i]) {
+ streams[i] = null;
+ } else {
+ eventSampleStreamsList.add(stream);
+ }
+ }
+
+ if (streams[i] == null && selections[i] != null) {
+ int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
+ TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
+ if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) {
+ EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex);
+ Format format = selections[i].getTrackGroup().getFormat(0);
+ EventSampleStream stream = new EventSampleStream(eventStream, format, manifest.dynamic);
+ streams[i] = stream;
+ streamResetFlags[i] = true;
+ eventSampleStreamsList.add(stream);
+ }
+ }
+ }
+ }
+
+ private void selectEmbeddedSampleStreams(TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags,
+ long positionUs, Map> primarySampleStreams) {
for (int i = 0; i < selections.length; i++) {
if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream)
&& (selections[i] == null || !mayRetainStreamFlags[i])) {
@@ -161,7 +240,7 @@ import java.util.List;
if (selections[i] != null) {
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
- if (!trackGroupInfo.isPrimary) {
+ if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) {
ChunkSampleStream> primaryStream = primarySampleStreams.get(
trackGroupInfo.primaryTrackGroupIndex);
SampleStream stream = streams[i];
@@ -177,10 +256,6 @@ import java.util.List;
}
}
}
- sampleStreams = newSampleStreamArray(primarySampleStreams.size());
- primarySampleStreams.values().toArray(sampleStreams);
- sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
- return positionUs;
}
@Override
@@ -215,6 +290,9 @@ import java.util.List;
for (ChunkSampleStream sampleStream : sampleStreams) {
sampleStream.seekToUs(positionUs);
}
+ for (EventSampleStream sampleStream : eventSampleStreams) {
+ sampleStream.seekToUs(positionUs);
+ }
return positionUs;
}
@@ -228,62 +306,25 @@ import java.util.List;
// Internal methods.
private static Pair buildTrackGroups(
- List adaptationSets) {
+ List adaptationSets, List eventStreams) {
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
int primaryGroupCount = groupedAdaptationSetIndices.length;
boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];
boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount];
- int totalGroupCount = primaryGroupCount;
- for (int i = 0; i < primaryGroupCount; i++) {
- if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {
- primaryGroupHasEventMessageTrackFlags[i] = true;
- totalGroupCount++;
- }
- if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) {
- primaryGroupHasCea608TrackFlags[i] = true;
- totalGroupCount++;
- }
- }
+ int totalEmbeddedTrackGroupCount = identifyEmbeddedTracks(primaryGroupCount, adaptationSets,
+ groupedAdaptationSetIndices, primaryGroupHasEventMessageTrackFlags,
+ primaryGroupHasCea608TrackFlags);
+ int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount + eventStreams.size();
TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];
TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];
- int trackGroupCount = 0;
- for (int i = 0; i < primaryGroupCount; i++) {
- int[] adaptationSetIndices = groupedAdaptationSetIndices[i];
- List 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++) {
- formats[j] = representations.get(j).format;
- }
+ int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos(adaptationSets,
+ groupedAdaptationSetIndices, primaryGroupCount, primaryGroupHasEventMessageTrackFlags,
+ primaryGroupHasCea608TrackFlags, trackGroups, trackGroupInfos);
- AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);
- int primaryTrackGroupIndex = trackGroupCount;
- boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i];
- boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i];
-
- trackGroups[trackGroupCount] = new TrackGroup(formats);
- trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(firstAdaptationSet.type,
- adaptationSetIndices, primaryTrackGroupIndex, true, hasEventMessageTrack, hasCea608Track);
- if (hasEventMessageTrack) {
- Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg",
- MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);
- trackGroups[trackGroupCount] = new TrackGroup(format);
- trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_METADATA,
- adaptationSetIndices, primaryTrackGroupIndex, false, false, false);
- }
- if (hasCea608Track) {
- Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608",
- MimeTypes.APPLICATION_CEA608, 0, null);
- trackGroups[trackGroupCount] = new TrackGroup(format);
- trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_TEXT,
- adaptationSetIndices, primaryTrackGroupIndex, false, false, false);
- }
- }
+ buildManifestEventTrackGroupInfos(eventStreams, trackGroups, trackGroupInfos, trackGroupCount);
return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);
}
@@ -326,6 +367,90 @@ import java.util.List;
? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : 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 containing boolean flag, each
+ * indicates whether the corresponding primary track group contains an embedded event message
+ * track.
+ * @param primaryGroupHasCea608TrackFlags An output array containing boolean flag, each
+ * indicates whether the corresponding primary track group contains an embedded Cea608 track.
+ * @return Total number of embedded tracks.
+ */
+ private static int identifyEmbeddedTracks(int primaryGroupCount,
+ List adaptationSets, int[][] groupedAdaptationSetIndices,
+ boolean[] primaryGroupHasEventMessageTrackFlags, boolean[] primaryGroupHasCea608TrackFlags) {
+ int numEmbeddedTrack = 0;
+ for (int i = 0; i < primaryGroupCount; i++) {
+ if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {
+ primaryGroupHasEventMessageTrackFlags[i] = true;
+ numEmbeddedTrack++;
+ }
+ if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) {
+ primaryGroupHasCea608TrackFlags[i] = true;
+ numEmbeddedTrack++;
+ }
+ }
+ return numEmbeddedTrack;
+ }
+
+ private static int buildPrimaryAndEmbeddedTrackGroupInfos(List adaptationSets,
+ int[][] groupedAdaptationSetIndices, int primaryGroupCount,
+ boolean[] primaryGroupHasEventMessageTrackFlags, boolean[] primaryGroupHasCea608TrackFlags,
+ TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos) {
+ int trackGroupCount = 0;
+ for (int i = 0; i < primaryGroupCount; i++) {
+ int[] adaptationSetIndices = groupedAdaptationSetIndices[i];
+ List 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++) {
+ formats[j] = representations.get(j).format;
+ }
+
+ AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);
+ int primaryTrackGroupIndex = trackGroupCount;
+ boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i];
+ boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i];
+
+ trackGroups[trackGroupCount] = new TrackGroup(formats);
+ trackGroupInfos[trackGroupCount++] = TrackGroupInfo.primaryTrack(firstAdaptationSet.type,
+ adaptationSetIndices, primaryTrackGroupIndex, hasEventMessageTrack, hasCea608Track);
+ if (hasEventMessageTrack) {
+ Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg",
+ MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);
+ trackGroups[trackGroupCount] = new TrackGroup(format);
+ trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices,
+ primaryTrackGroupIndex);
+ }
+ if (hasCea608Track) {
+ Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608",
+ MimeTypes.APPLICATION_CEA608, 0, null);
+ trackGroups[trackGroupCount] = new TrackGroup(format);
+ trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedCea608Track(
+ adaptationSetIndices, primaryTrackGroupIndex);
+ }
+ }
+ return trackGroupCount;
+ }
+
+ private static void buildManifestEventTrackGroupInfos(List eventStreams,
+ TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos, int existingTrackGroupCount) {
+ for (int i = 0; i < eventStreams.size(); i++) {
+ EventStream eventStream = eventStreams.get(i);
+ Format format = Format.createSampleFormat(eventStream.id(), MimeTypes.APPLICATION_EMSG, null,
+ Format.NO_VALUE, null);
+ trackGroups[existingTrackGroupCount] = new TrackGroup(format);
+ trackGroupInfos[existingTrackGroupCount++] = TrackGroupInfo.mpdEventTrack(i);
+ }
+ }
+
private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo,
TrackSelection selection, long positionUs) {
int embeddedTrackCount = 0;
@@ -402,24 +527,75 @@ import java.util.List;
private static final class TrackGroupInfo {
+ @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;
- public final boolean isPrimary;
+ public @TrackGroupCategory final int trackGroupCategory;
+ public final int eventStreamGroupIndex;
public final int primaryTrackGroupIndex;
public final boolean hasEmbeddedEventMessageTrack;
public final boolean hasEmbeddedCea608Track;
- public TrackGroupInfo(int trackType, int[] adaptationSetIndices, int primaryTrackGroupIndex,
- boolean isPrimary, boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track) {
- this.trackType = trackType;
- this.adaptationSetIndices = adaptationSetIndices;
- this.primaryTrackGroupIndex = primaryTrackGroupIndex;
- this.isPrimary = isPrimary;
- this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack;
- this.hasEmbeddedCea608Track = hasEmbeddedCea608Track;
+ public static TrackGroupInfo primaryTrack(int trackType, int[] adaptationSetIndices,
+ int primaryTrackGroupIndex, boolean hasEmbeddedEventMessageTrack,
+ boolean hasEmbeddedCea608Track) {
+ return new TrackGroupInfo(trackType, CATEGORY_PRIMARY, adaptationSetIndices,
+ primaryTrackGroupIndex, hasEmbeddedEventMessageTrack, hasEmbeddedCea608Track, -1);
}
+ public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices,
+ int primaryTrackGroupIndex) {
+ return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_EMBEDDED,
+ adaptationSetIndices, primaryTrackGroupIndex, false, false, -1);
+ }
+
+ public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices,
+ int primaryTrackGroupIndex) {
+ return new TrackGroupInfo(C.TRACK_TYPE_TEXT, CATEGORY_EMBEDDED,
+ adaptationSetIndices, primaryTrackGroupIndex, false, false, -1);
+ }
+
+ public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) {
+ return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_MANIFEST_EVENTS,
+ null, -1, false, false, eventStreamIndex);
+ }
+
+ private TrackGroupInfo(int trackType, @TrackGroupCategory int trackGroupCategory,
+ int[] adaptationSetIndices, int primaryTrackGroupIndex,
+ boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track,
+ int eventStreamGroupIndex) {
+ this.trackType = trackType;
+ this.adaptationSetIndices = adaptationSetIndices;
+ this.trackGroupCategory = trackGroupCategory;
+ this.primaryTrackGroupIndex = primaryTrackGroupIndex;
+ this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack;
+ this.hasEmbeddedCea608Track = hasEmbeddedCea608Track;
+ this.eventStreamGroupIndex = eventStreamGroupIndex;
+ }
}
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java
new file mode 100644
index 0000000000..549bfdef7b
--- /dev/null
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 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 com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.metadata.emsg.EventMessage;
+import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.dash.manifest.EventStream;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link SampleStream} consisting of serialized {@link EventMessage}s read from an
+ * {@link EventStream}.
+ */
+/* package */ final class EventSampleStream implements SampleStream {
+
+ private final Format upstreamFormat;
+ private final EventMessageEncoder eventMessageEncoder;
+
+ private long[] eventTimesUs;
+ private boolean eventStreamUpdatable;
+ private EventStream eventStream;
+
+ private boolean isFormatSentDownstream;
+ private int currentIndex;
+ private long pendingSeekPositionUs;
+
+ EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) {
+ this.upstreamFormat = upstreamFormat;
+ eventMessageEncoder = new EventMessageEncoder();
+ pendingSeekPositionUs = C.TIME_UNSET;
+ updateEventStream(eventStream, eventStreamUpdatable);
+ }
+
+ void updateEventStream(EventStream eventStream, boolean eventStreamUpdatable) {
+ long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1];
+
+ this.eventStreamUpdatable = eventStreamUpdatable;
+ this.eventStream = eventStream;
+ this.eventTimesUs = eventStream.presentationTimesUs;
+ if (pendingSeekPositionUs != C.TIME_UNSET) {
+ seekToUs(pendingSeekPositionUs);
+ } else if (lastReadPositionUs != C.TIME_UNSET) {
+ currentIndex = Util.binarySearchCeil(eventTimesUs, lastReadPositionUs, false, false);
+ }
+ }
+
+ String eventStreamId() {
+ return eventStream.id();
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (formatRequired || !isFormatSentDownstream) {
+ formatHolder.format = upstreamFormat;
+ isFormatSentDownstream = true;
+ return C.RESULT_FORMAT_READ;
+ }
+ if (currentIndex == eventTimesUs.length) {
+ if (!eventStreamUpdatable) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ } else {
+ return C.RESULT_NOTHING_READ;
+ }
+ }
+ int sampleIndex = currentIndex++;
+ byte[] serializedEvent = eventMessageEncoder.encode(eventStream.events[sampleIndex],
+ eventStream.timescale, eventTimesUs[sampleIndex]);
+ if (serializedEvent != null) {
+ buffer.ensureSpaceForWrite(serializedEvent.length);
+ buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME);
+ buffer.data.put(serializedEvent);
+ buffer.timeUs = eventTimesUs[sampleIndex];
+ return C.RESULT_BUFFER_READ;
+ } else {
+ return C.RESULT_NOTHING_READ;
+ }
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ int newIndex =
+ Math.max(currentIndex, Util.binarySearchCeil(eventTimesUs, positionUs, true, false));
+ int skipped = newIndex - currentIndex;
+ currentIndex = newIndex;
+ return skipped;
+ }
+
+ /**
+ * Seeks to the specified position in microseconds.
+ *
+ * @param positionUs The seek position in microseconds.
+ */
+ public void seekToUs(long positionUs) {
+ currentIndex = Util.binarySearchCeil(eventTimesUs, positionUs, true, false);
+ boolean isPendingSeek = eventStreamUpdatable && currentIndex == eventTimesUs.length;
+ pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET;
+ }
+
+}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
index cd02e27fce..1ab94ccd30 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
@@ -107,7 +107,9 @@ public class DashManifest {
Period period = getPeriod(periodIndex);
ArrayList copyAdaptationSets =
copyAdaptationSets(period.adaptationSets, keys);
- copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets));
+ Period copiedPeriod = new Period(period.id, period.startMs - shiftMs, copyAdaptationSets,
+ period.eventStreams);
+ copyPeriods.add(copiedPeriod);
}
}
long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET;
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index 72df69f7e9..97ea07e065 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -20,12 +20,14 @@ import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.util.Pair;
+import android.util.Xml;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
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.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;
@@ -36,6 +38,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.UriUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.util.XmlPullParserUtil;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -47,6 +50,7 @@ import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
/**
* A parser of media presentation description files.
@@ -197,6 +201,7 @@ public class DashManifestParser extends DefaultHandler
long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET);
SegmentBase segmentBase = null;
List adaptationSets = new ArrayList<>();
+ List eventStreams = new ArrayList<>();
boolean seenFirstBaseUrl = false;
do {
xpp.next();
@@ -207,6 +212,8 @@ public class DashManifestParser extends DefaultHandler
}
} else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase));
+ } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) {
+ eventStreams.add(parseEventStream(xpp));
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, null);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
@@ -216,11 +223,12 @@ public class DashManifestParser extends DefaultHandler
}
} while (!XmlPullParserUtil.isEndTag(xpp, "Period"));
- return Pair.create(buildPeriod(id, startMs, adaptationSets), durationMs);
+ return Pair.create(buildPeriod(id, startMs, adaptationSets, eventStreams), durationMs);
}
- protected Period buildPeriod(String id, long startMs, List adaptationSets) {
- return new Period(id, startMs, adaptationSets);
+ protected Period buildPeriod(String id, long startMs, List adaptationSets,
+ List eventStreams) {
+ return new Period(id, startMs, adaptationSets, eventStreams);
}
// AdaptationSet parsing.
@@ -387,7 +395,7 @@ public class DashManifestParser extends DefaultHandler
Log.w(TAG, "Skipping malformed cenc:pssh data");
data = null;
}
- } else if (uuid == C.PLAYREADY_UUID && XmlPullParserUtil.isStartTag(xpp, "mspr:pro")
+ } else if (C.PLAYREADY_UUID.equals(uuid) && XmlPullParserUtil.isStartTag(xpp, "mspr:pro")
&& xpp.next() == XmlPullParser.TEXT) {
// The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady.
data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID,
@@ -660,6 +668,143 @@ public class DashManifestParser extends DefaultHandler
startNumber, duration, timeline, initializationTemplate, mediaTemplate);
}
+ /**
+ * /**
+ * Parses a single EventStream node in the manifest.
+ *
+ * @param xpp The current xml parser.
+ * @return The {@link EventStream} parsed from this EventStream node.
+ * @throws XmlPullParserException If there is any error parsing this node.
+ * @throws IOException If there is any error reading from the underlying input stream.
+ */
+ protected EventStream parseEventStream(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+ String schemeIdUri = parseString(xpp, "schemeIdUri", "");
+ String value = parseString(xpp, "value", "");
+ long timescale = parseLong(xpp, "timescale", 1);
+ List> timedEvents = new ArrayList<>();
+ ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512);
+ do {
+ xpp.next();
+ if (XmlPullParserUtil.isStartTag(xpp, "Event")) {
+ Pair timedEvent = parseEvent(xpp, schemeIdUri, value, timescale,
+ scratchOutputStream);
+ timedEvents.add(timedEvent);
+ }
+ } while (!XmlPullParserUtil.isEndTag(xpp, "EventStream"));
+
+ long[] presentationTimesUs = new long[timedEvents.size()];
+ EventMessage[] events = new EventMessage[timedEvents.size()];
+ for (int i = 0; i < timedEvents.size(); i++) {
+ Pair timedEvent = timedEvents.get(i);
+ presentationTimesUs[i] = timedEvent.first;
+ events[i] = timedEvent.second;
+ }
+ return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events);
+ }
+
+ protected EventStream buildEventStream(String schemeIdUri, String value, long timescale,
+ long[] presentationTimesUs, EventMessage[] events) {
+ return new EventStream(schemeIdUri, value, timescale, presentationTimesUs, events);
+ }
+
+ /**
+ * Parses a single Event node in the manifest.
+ *
+ * @param xpp The current xml parser.
+ * @param schemeIdUri The schemeIdUri of the parent EventStream.
+ * @param value The schemeIdUri of the parent EventStream.
+ * @param timescale The timescale of the parent EventStream.
+ * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize data
+ * in between and tags into.
+ * @return The {@link EventStream} parsed from this EventStream node.
+ * @throws XmlPullParserException If there is any error parsing this node.
+ * @throws IOException If there is any error reading from the underlying input stream.
+ */
+ protected Pair parseEvent(XmlPullParser xpp, String schemeIdUri, String value,
+ long timescale, ByteArrayOutputStream scratchOutputStream)
+ throws IOException, XmlPullParserException {
+ long id = parseLong(xpp, "id", 0);
+ long duration = parseLong(xpp, "duration", C.TIME_UNSET);
+ long presentationTime = parseLong(xpp, "presentationTime", 0);
+ long durationMs = Util.scaleLargeTimestamp(duration, 1000, timescale);
+ long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND,
+ timescale);
+ byte[] eventObject = parseEventObject(xpp, scratchOutputStream);
+ return new Pair<>(presentationTimesUs, buildEvent(schemeIdUri, value, id, durationMs,
+ eventObject));
+ }
+
+ /**
+ * Parses everything between as a byte array string.
+ *
+ * @param xpp The current xml parser.
+ * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize byte
+ * array data into.
+ * @return The serialized byte array.
+ * @throws XmlPullParserException If there is any error parsing this node.
+ * @throws IOException If there is any error reading from the underlying input stream.
+ */
+ protected byte[] parseEventObject(XmlPullParser xpp, ByteArrayOutputStream scratchOutputStream)
+ throws XmlPullParserException, IOException {
+ scratchOutputStream.reset();
+ XmlSerializer xmlSerializer = Xml.newSerializer();
+ xmlSerializer.setOutput(scratchOutputStream, null);
+ // Start reading everything between and , and serialize them into an Xml
+ // byte array.
+ xpp.nextToken();
+ while (!XmlPullParserUtil.isEndTag(xpp, "Event")) {
+ switch (xpp.getEventType()) {
+ case (XmlPullParser.START_DOCUMENT):
+ xmlSerializer.startDocument(null, false);
+ break;
+ case (XmlPullParser.END_DOCUMENT):
+ xmlSerializer.endDocument();
+ break;
+ case (XmlPullParser.START_TAG):
+ xmlSerializer.startTag(xpp.getNamespace(), xpp.getName());
+ for (int i = 0; i < xpp.getAttributeCount(); i++) {
+ xmlSerializer.attribute(xpp.getAttributeNamespace(i), xpp.getAttributeName(i),
+ xpp.getAttributeValue(i));
+ }
+ break;
+ case (XmlPullParser.END_TAG):
+ xmlSerializer.endTag(xpp.getNamespace(), xpp.getName());
+ break;
+ case (XmlPullParser.TEXT):
+ xmlSerializer.text(xpp.getText());
+ break;
+ case (XmlPullParser.CDSECT):
+ xmlSerializer.cdsect(xpp.getText());
+ break;
+ case (XmlPullParser.ENTITY_REF):
+ xmlSerializer.entityRef(xpp.getText());
+ break;
+ case (XmlPullParser.IGNORABLE_WHITESPACE):
+ xmlSerializer.ignorableWhitespace(xpp.getText());
+ break;
+ case (XmlPullParser.PROCESSING_INSTRUCTION):
+ xmlSerializer.processingInstruction(xpp.getText());
+ break;
+ case (XmlPullParser.COMMENT):
+ xmlSerializer.comment(xpp.getText());
+ break;
+ case (XmlPullParser.DOCDECL):
+ xmlSerializer.docdecl(xpp.getText());
+ break;
+ default: // fall out
+ }
+ xpp.nextToken();
+ }
+ xmlSerializer.flush();
+ return scratchOutputStream.toByteArray();
+ }
+
+ protected EventMessage buildEvent(String schemeIdUri, String value, long id,
+ long durationMs, byte[] messageData) {
+ return new EventMessage(schemeIdUri, value, durationMs, id, messageData);
+ }
+
protected List parseSegmentTimeline(XmlPullParser xpp)
throws XmlPullParserException, IOException {
List segmentTimeline = new ArrayList<>();
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java
new file mode 100644
index 0000000000..8a4e1ad058
--- /dev/null
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 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.manifest;
+
+import com.google.android.exoplayer2.metadata.emsg.EventMessage;
+
+/**
+ * A DASH in-MPD EventStream element, as defined by ISO/IEC 23009-1, 2nd edition, section 5.10.
+ */
+public final class EventStream {
+
+ /**
+ * {@link EventMessage}s in the event stream.
+ */
+ public final EventMessage[] events;
+
+ /**
+ * Presentation time of the events in microsecond, sorted in ascending order.
+ */
+ public final long[] presentationTimesUs;
+
+ /**
+ * The scheme URI.
+ */
+ public final String schemeIdUri;
+
+ /**
+ * The value of the event stream. Use empty string if not defined in manifest.
+ */
+ public final String value;
+
+ /**
+ * The timescale in units per seconds, as defined in the manifest.
+ */
+ public final long timescale;
+
+ public EventStream(String schemeIdUri, String value, long timescale, long[] presentationTimesUs,
+ EventMessage[] events) {
+ this.schemeIdUri = schemeIdUri;
+ this.value = value;
+ this.timescale = timescale;
+ this.presentationTimesUs = presentationTimesUs;
+ this.events = events;
+ }
+
+ /**
+ * A constructed id of this {@link EventStream}. Equal to {@code schemeIdUri + "/" + value}.
+ */
+ public String id() {
+ return schemeIdUri + "/" + value;
+ }
+
+}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java
index 269a63b7a9..bb1dbdac5d 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.dash.manifest;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.util.Collections;
import java.util.List;
@@ -27,7 +28,7 @@ public class Period {
/**
* The period identifier, if one exists.
*/
- public final String id;
+ @Nullable public final String id;
/**
* The start time of the period in milliseconds.
@@ -39,15 +40,32 @@ public class Period {
*/
public final List adaptationSets;
+ /**
+ * The event stream belonging to the period.
+ */
+ public final List eventStreams;
+
/**
* @param id The period identifier. May be null.
* @param startMs The start time of the period in milliseconds.
* @param adaptationSets The adaptation sets belonging to the period.
*/
- public Period(String id, long startMs, List adaptationSets) {
+ public Period(@Nullable String id, long startMs, List adaptationSets) {
+ this(id, startMs, adaptationSets, Collections.emptyList());
+ }
+
+ /**
+ * @param id The period identifier. May be null.
+ * @param startMs The start time of the period in milliseconds.
+ * @param adaptationSets The adaptation sets belonging to the period.
+ * @param eventStreams The {@link EventStream}s belonging to the period.
+ */
+ public Period(@Nullable String id, long startMs, List adaptationSets,
+ List eventStreams) {
this.id = id;
this.startMs = startMs;
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
+ this.eventStreams = Collections.unmodifiableList(eventStreams);
}
/**