diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index fd6996aa80..266988246d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -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)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java new file mode 100644 index 0000000000..2ddbfb4708 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -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); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index 1ce0ccb93d..f7f0c63300 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -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); + } + } diff --git a/library/dash/src/androidTest/assets/sample_mpd_4_event_stream b/library/dash/src/androidTest/assets/sample_mpd_4_event_stream new file mode 100644 index 0000000000..4352205d3f --- /dev/null +++ b/library/dash/src/androidTest/assets/sample_mpd_4_event_stream @@ -0,0 +1,61 @@ + + + + + + 1 800 10101010 + + + + + + The title + The description + + + GB + + + ]]> + + + + + /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA== + + + + + + + + + + + + + + + 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/ + + + + + + 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/ + + + 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/ + + + 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/ + + + 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/ + + + 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/ + + + + + diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 3ce4b37ec6..b24a7da878 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -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, + ("\n" + + " \n" + + " \n" + + " The title\n" + + " The description\n" + + " \n" + + " \n" + + " GB\n" + + " \n" + + " \n" + + " ]]>").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, + ("\n" + + " \n" + + " /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\n" + + " \n" + + " ").getBytes()), + eventStream3.events[0]); + } + public void testParseCea608AccessibilityChannel() { assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel( buildCea608AccessibilityDescriptors("CC1=eng"))); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 35f3c2e129..b5ce45b2f5 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -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[] sampleStreams; + private EventSampleStream[] eventSampleStreams; private CompositeSequenceableLoader sequenceableLoader; private DashManifest manifest; private int periodIndex; + private List 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 result = - buildTrackGroups(manifest.getPeriod(periodIndex).adaptationSets); + Period period = manifest.getPeriod(periodIndex); + eventStreams = period.eventStreams; + Pair 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); } /**