Supports Out-of-band, in MPD EventStream.

MPD file may include multiple EventStreams in its Periods, which contains Events
that the application may need to handle/respond to.
This change adds support for parsing the EventStream/Event nodes from MPD
file, and exposing these EventStreams as a metadata sample stream that application
can respond in a similar way to other metadata events.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=175017697
This commit is contained in:
hoangtc 2017-11-08 09:35:11 -08:00 committed by Oliver Woodman
parent ed2e4dd91e
commit 3171c86bdb
11 changed files with 871 additions and 74 deletions

View file

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -40,7 +41,7 @@ public final class EventMessageDecoder implements MetadataDecoder {
String value = emsgData.readNullTerminatedString();
long timescale = emsgData.readUnsignedInt();
emsgData.skipBytes(4); // presentation_time_delta
long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale;
long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale);
long id = emsgData.readUnsignedInt();
byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size);
return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData));

View file

@ -0,0 +1,90 @@
/*
* 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.metadata.emsg;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
/**
* Encodes data that can be decoded by {@link EventMessageDecoder}. This class isn't thread safe.
*/
public final class EventMessageEncoder {
private final ByteArrayOutputStream byteArrayOutputStream;
private final DataOutputStream dataOutputStream;
public EventMessageEncoder() {
byteArrayOutputStream = new ByteArrayOutputStream(512);
dataOutputStream = new DataOutputStream(byteArrayOutputStream);
}
/**
* Encodes an {@link EventMessage} to a byte array that can be decoded by
* {@link EventMessageDecoder}.
*
* @param eventMessage The event message to be encoded.
* @param timescale Timescale of the event message, in units per second.
* @param presentationTimeUs The presentation time of the event message in microseconds.
* @return The serialized byte array.
*/
@Nullable
public byte[] encode(EventMessage eventMessage, long timescale, long presentationTimeUs) {
Assertions.checkArgument(timescale >= 0);
byteArrayOutputStream.reset();
try {
writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri);
String nonNullValue = eventMessage.value != null ? eventMessage.value : "";
writeNullTerminatedString(dataOutputStream, nonNullValue);
writeUnsignedInt(dataOutputStream, timescale);
long presentationTime = Util.scaleLargeTimestamp(presentationTimeUs, timescale,
C.MICROS_PER_SECOND);
writeUnsignedInt(dataOutputStream, presentationTime);
long duration = Util.scaleLargeTimestamp(eventMessage.durationMs, timescale, 1000);
writeUnsignedInt(dataOutputStream, duration);
writeUnsignedInt(dataOutputStream, eventMessage.id);
dataOutputStream.write(eventMessage.messageData);
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
dataOutputStream.close();
} catch (IOException ignored) {
// ignored
}
}
}
private static void writeNullTerminatedString(DataOutputStream dataOutputStream, String value)
throws IOException {
dataOutputStream.writeBytes(value);
dataOutputStream.writeByte(0);
}
private static void writeUnsignedInt(DataOutputStream outputStream, long value)
throws IOException {
outputStream.writeByte((int) (value >>> 24) & 0xFF);
outputStream.writeByte((int) (value >>> 16) & 0xFF);
outputStream.writeByte((int) (value >>> 8) & 0xFF);
outputStream.writeByte((int) value & 0xFF);
}
}

View file

@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -55,4 +56,63 @@ public final class EventMessageDecoderTest {
assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4});
}
@Test
public void testEncodeEventStream() throws IOException {
EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403,
new byte[] {0, 1, 2, 3, 4});
byte[] expectedEmsgBody = new byte[] {
117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test"
49, 50, 51, 0, // value = "123"
0, 0, -69, -128, // timescale = 48000
0, 0, -69, -128, // presentation_time_delta = 48
0, 2, 50, -128, // event_duration = 144000
0, 15, 67, -45, // id = 1000403
0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4}
byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000, 1000000);
assertThat(encodedByteArray).isEqualTo(expectedEmsgBody);
}
@Test
public void testEncodeDecodeEventStream() throws IOException {
EventMessage expectedEmsg = new EventMessage("urn:test", "123", 3000, 1000403,
new byte[] {0, 1, 2, 3, 4});
byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000, 1);
MetadataInputBuffer buffer = new MetadataInputBuffer();
buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray);
EventMessageDecoder decoder = new EventMessageDecoder();
Metadata metadata = decoder.decode(buffer);
assertThat(metadata.length()).isEqualTo(1);
assertThat(metadata.get(0)).isEqualTo(expectedEmsg);
}
@Test
public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException {
EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403,
new byte[] {0, 1, 2, 3, 4});
byte[] expectedEmsgBody = new byte[] {
117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test"
49, 50, 51, 0, // value = "123"
0, 0, -69, -128, // timescale = 48000
0, 0, -69, -128, // presentation_time_delta = 48
0, 2, 50, -128, // event_duration = 144000
0, 15, 67, -45, // id = 1000403
0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4}
EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402,
new byte[] {4, 3, 2, 1, 0});
byte[] expectedEmsgBody1 = new byte[] {
117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test"
49, 50, 51, 0, // value = "123"
0, 0, -69, -128, // timescale = 48000
0, 0, -69, -128, // presentation_time_delta = 48
0, 2, 50, -128, // event_duration = 144000
0, 15, 67, -46, // id = 1000402
4, 3, 2, 1, 0}; // message_data = {4, 3, 2, 1, 0}
EventMessageEncoder eventMessageEncoder = new EventMessageEncoder();
byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000, 1000000);
assertThat(encodedByteArray).isEqualTo(expectedEmsgBody);
byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000, 1000000);
assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1);
}
}

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:DASH:schema:MPD:2011" xmlns:yt="http://youtube.com/yt/2012/10/10" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="dynamic" availabilityStartTime="2016-10-14T17:00:17" timeShiftBufferDepth="PT7200.000S" minimumUpdatePeriod="PT2.000S" yt:earliestMediaSequence="0" yt:mpdRequestTime="2016-10-14T18:29:17.082" yt:mpdResponseTime="2016-10-14T18:29:17.194">
<Period start="PT0.000S" yt:segmentIngestTime="2016-10-14T17:00:14.257">
<EventStream schemeIdUri="urn:uuid:XYZY" timescale="1000" value="call">
<Event presentationTime="0" duration="10000" id="0">+ 1 800 10101010</Event>
</EventStream>
<EventStream schemeIdUri="urn:dvb:iptv:cpm:2014">
<Event presentationTime="300" duration="1500" id="1"><![CDATA[<BroadcastEvent>
<Program crid="crid://broadcaster.example.com/ABCDEF"/>
<InstanceDescription>
<Title xml:lang="en">The title</Title>
<Synopsis xml:lang="en" length="medium">The description</Synopsis>
<ParentalGuidance>
<mpeg7:ParentalRating href="urn:dvb:iptv:rating:2014:15"/>
<mpeg7:Region>GB</mpeg7:Region>
</ParentalGuidance>
</InstanceDescription>
</BroadcastEvent>]]></Event>
</EventStream>
<EventStream schemeIdUri="urn:scte:scte35:2014:xml+bin">
<Event timescale="90000" presentationTime="1000" duration="1000" id="2"><scte35:Signal>
<scte35:Binary>
/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==
</scte35:Binary>
</scte35:Signal></Event>
</EventStream>
<SegmentTemplate startNumber="0" timescale="1000" media="sq/$Number$">
<SegmentTimeline>
<S d="2002" t="6009" r="2"/>
<S d="1985"/>
<S d="2000"/>
</SegmentTimeline>
</SegmentTemplate>
<AdaptationSet id="0" mimeType="audio/mp4" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>
<Representation id="140" codecs="mp4a.40.2" audioSamplingRate="48000" startWithSAP="1" bandwidth="144000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/140/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/audio%2Fmp4/live/1/gir/yes/noclen/1/signature/B5137EA0CC278C07DD056D204E863CC81EDEB39E.1AD5D242EBC94922EDA7165353A89A5E08A4103A/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" mimeType="video/mp4" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>
<Representation id="133" codecs="avc1.4d4015" width="426" height="240" startWithSAP="1" maxPlayoutRate="1" bandwidth="258000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/133/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/90154AE9C5C9D9D519CBF2E43AB0A1778375992D.40E2E855ADFB38FA7E95E168FEEEA6796B080BD7/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="134" codecs="avc1.4d401e" width="640" height="360" startWithSAP="1" maxPlayoutRate="1" bandwidth="646000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/134/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/5C094AEFDCEB1A4D2F3C05F8BD095C336EF0E1C3.7AE6B9951B0237AAE6F031927AACAC4974BAFFAA/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="135" codecs="avc1.4d401f" width="854" height="480" startWithSAP="1" maxPlayoutRate="1" bandwidth="1171000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/135/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/1F7660CA4E5B4AE4D60E18795680E34CDD2EF3C9.800B0A1D5F490DE142CCF4C88C64FD21D42129/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="160" codecs="avc1.42c00b" width="256" height="144" startWithSAP="1" maxPlayoutRate="1" bandwidth="124000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/160/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/94EB61673784DF0C4237A1A866F2E171C8A64ADB.AEC00AA06C2278FEA8702FB62693B70D8977F46C/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="136" codecs="avc1.4d401f" width="1280" height="720" startWithSAP="1" maxPlayoutRate="1" bandwidth="2326000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/136/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/6D8C34FC30A1F1A4F700B61180D1C4CCF6274844.29EBCB4A837DE626C52C66CF650519E61C2FF0BF/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.manifest;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import java.util.Collections;
@ -31,6 +32,7 @@ public class DashManifestParserTest extends InstrumentationTestCase {
private static final String SAMPLE_MPD_1 = "sample_mpd_1";
private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type";
private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template";
private static final String SAMPLE_MPD_4_EVENT_STREAM = "sample_mpd_4_event_stream";
/**
* Simple test to ensure the sample manifests parse without any exceptions being thrown.
@ -69,6 +71,52 @@ public class DashManifestParserTest extends InstrumentationTestCase {
}
}
public void testParseMediaPresentationDescriptionCanParseEventStream()
throws IOException {
DashManifestParser parser = new DashManifestParser();
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_4_EVENT_STREAM));
Period period = mpd.getPeriod(0);
assertEquals(3, period.eventStreams.size());
// assert text-only event stream
EventStream eventStream1 = period.eventStreams.get(0);
assertEquals(1, eventStream1.events.length);
EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0,
"+ 1 800 10101010".getBytes());
assertEquals(expectedEvent1, eventStream1.events[0]);
// assert CData-structured event stream
EventStream eventStream2 = period.eventStreams.get(1);
assertEquals(1, eventStream2.events.length);
assertEquals(
new EventMessage("urn:dvb:iptv:cpm:2014", "", 1500000, 1,
("<![CDATA[<BroadcastEvent>\n"
+ " <Program crid=\"crid://broadcaster.example.com/ABCDEF\"/>\n"
+ " <InstanceDescription>\n"
+ " <Title xml:lang=\"en\">The title</Title>\n"
+ " <Synopsis xml:lang=\"en\" length=\"medium\">The description</Synopsis>\n"
+ " <ParentalGuidance>\n"
+ " <mpeg7:ParentalRating href=\"urn:dvb:iptv:rating:2014:15\"/>\n"
+ " <mpeg7:Region>GB</mpeg7:Region>\n"
+ " </ParentalGuidance>\n"
+ " </InstanceDescription>\n"
+ " </BroadcastEvent>]]>").getBytes()),
eventStream2.events[0]);
// assert xml-structured event stream
EventStream eventStream3 = period.eventStreams.get(2);
assertEquals(1, eventStream3.events.length);
assertEquals(
new EventMessage("urn:scte:scte35:2014:xml+bin", "", 1000000, 2,
("<scte35:Signal>\n"
+ " <scte35:Binary>\n"
+ " /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\n"
+ " </scte35:Binary>\n"
+ " </scte35:Signal>").getBytes()),
eventStream3.events[0]);
}
public void testParseCea608AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel(
buildCea608AccessibilityDescriptors("CC1=eng")));

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.dash;
import android.support.annotation.IntDef;
import android.util.Pair;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
@ -32,16 +33,21 @@ import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSamp
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.LoaderErrorThrower;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A DASH {@link MediaPeriod}.
@ -61,9 +67,11 @@ import java.util.List;
private Callback callback;
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
private EventSampleStream[] eventSampleStreams;
private CompositeSequenceableLoader sequenceableLoader;
private DashManifest manifest;
private int periodIndex;
private List<EventStream> eventStreams;
public DashMediaPeriod(int id, DashManifest manifest, int periodIndex,
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
@ -79,22 +87,41 @@ import java.util.List;
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.allocator = allocator;
sampleStreams = newSampleStreamArray(0);
eventSampleStreams = new EventSampleStream[0];
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
Pair<TrackGroupArray, TrackGroupInfo[]> result =
buildTrackGroups(manifest.getPeriod(periodIndex).adaptationSets);
Period period = manifest.getPeriod(periodIndex);
eventStreams = period.eventStreams;
Pair<TrackGroupArray, TrackGroupInfo[]> result = buildTrackGroups(period.adaptationSets,
eventStreams);
trackGroups = result.first;
trackGroupInfos = result.second;
}
/**
* Updates the {@link DashManifest} and the index of this period in the manifest.
* <p>
* @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<DashChunkSource> 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<Integer, ChunkSampleStream<DashChunkSource>> primarySampleStreams = new HashMap<>();
// First pass for primary tracks.
Map<Integer, ChunkSampleStream<DashChunkSource>> primarySampleStreams = new HashMap<>();
List<EventSampleStream> 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<Integer, ChunkSampleStream<DashChunkSource>> 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<DashChunkSource> 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<EventSampleStream> 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<Integer, ChunkSampleStream<DashChunkSource>> 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<DashChunkSource> 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<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
List<AdaptationSet> adaptationSets) {
List<AdaptationSet> adaptationSets, List<EventStream> 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<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++) {
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.
* <p>
* @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<AdaptationSet> 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<AdaptationSet> 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<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++) {
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<EventStream> 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<DashChunkSource> 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;
}
}
}

View file

@ -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;
}
}

View file

@ -107,7 +107,9 @@ public class DashManifest {
Period period = getPeriod(periodIndex);
ArrayList<AdaptationSet> 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;

View file

@ -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<AdaptationSet> adaptationSets = new ArrayList<>();
List<EventStream> 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<AdaptationSet> adaptationSets) {
return new Period(id, startMs, adaptationSets);
protected Period buildPeriod(String id, long startMs, List<AdaptationSet> adaptationSets,
List<EventStream> 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.
* <p>
* @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<Pair<Long, EventMessage>> timedEvents = new ArrayList<>();
ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512);
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "Event")) {
Pair<Long, EventMessage> 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<Long, EventMessage> 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.
* <p>
* @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 <Event> and </Event> 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<Long, EventMessage> 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 <Event></Event> 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 <Event> and </Event>, 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<SegmentTimelineElement> parseSegmentTimeline(XmlPullParser xpp)
throws XmlPullParserException, IOException {
List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();

View file

@ -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;
}
}

View file

@ -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<AdaptationSet> adaptationSets;
/**
* The event stream belonging to the period.
*/
public final List<EventStream> 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<AdaptationSet> adaptationSets) {
public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets) {
this(id, startMs, adaptationSets, Collections.<EventStream>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<AdaptationSet> adaptationSets,
List<EventStream> eventStreams) {
this.id = id;
this.startMs = startMs;
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
this.eventStreams = Collections.unmodifiableList(eventStreams);
}
/**