mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
ed2e4dd91e
commit
3171c86bdb
11 changed files with 871 additions and 74 deletions
|
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
|
@ -40,7 +41,7 @@ public final class EventMessageDecoder implements MetadataDecoder {
|
||||||
String value = emsgData.readNullTerminatedString();
|
String value = emsgData.readNullTerminatedString();
|
||||||
long timescale = emsgData.readUnsignedInt();
|
long timescale = emsgData.readUnsignedInt();
|
||||||
emsgData.skipBytes(4); // presentation_time_delta
|
emsgData.skipBytes(4); // presentation_time_delta
|
||||||
long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale;
|
long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale);
|
||||||
long id = emsgData.readUnsignedInt();
|
long id = emsgData.readUnsignedInt();
|
||||||
byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size);
|
byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size);
|
||||||
return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData));
|
return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
@ -55,4 +56,63 @@ public final class EventMessageDecoderTest {
|
||||||
assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4});
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.manifest;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
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_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_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_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.
|
* 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() {
|
public void testParseCea608AccessibilityChannel() {
|
||||||
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel(
|
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel(
|
||||||
buildCea608AccessibilityDescriptors("CC1=eng")));
|
buildCea608AccessibilityDescriptors("CC1=eng")));
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source.dash;
|
package com.google.android.exoplayer2.source.dash;
|
||||||
|
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
import com.google.android.exoplayer2.C;
|
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.AdaptationSet;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
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.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.source.dash.manifest.Representation;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DASH {@link MediaPeriod}.
|
* A DASH {@link MediaPeriod}.
|
||||||
|
|
@ -61,9 +67,11 @@ import java.util.List;
|
||||||
|
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
|
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
|
||||||
|
private EventSampleStream[] eventSampleStreams;
|
||||||
private CompositeSequenceableLoader sequenceableLoader;
|
private CompositeSequenceableLoader sequenceableLoader;
|
||||||
private DashManifest manifest;
|
private DashManifest manifest;
|
||||||
private int periodIndex;
|
private int periodIndex;
|
||||||
|
private List<EventStream> eventStreams;
|
||||||
|
|
||||||
public DashMediaPeriod(int id, DashManifest manifest, int periodIndex,
|
public DashMediaPeriod(int id, DashManifest manifest, int periodIndex,
|
||||||
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
|
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
|
||||||
|
|
@ -79,22 +87,41 @@ import java.util.List;
|
||||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||||
this.allocator = allocator;
|
this.allocator = allocator;
|
||||||
sampleStreams = newSampleStreamArray(0);
|
sampleStreams = newSampleStreamArray(0);
|
||||||
|
eventSampleStreams = new EventSampleStream[0];
|
||||||
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
|
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
|
||||||
Pair<TrackGroupArray, TrackGroupInfo[]> result =
|
Period period = manifest.getPeriod(periodIndex);
|
||||||
buildTrackGroups(manifest.getPeriod(periodIndex).adaptationSets);
|
eventStreams = period.eventStreams;
|
||||||
|
Pair<TrackGroupArray, TrackGroupInfo[]> result = buildTrackGroups(period.adaptationSets,
|
||||||
|
eventStreams);
|
||||||
trackGroups = result.first;
|
trackGroups = result.first;
|
||||||
trackGroupInfos = result.second;
|
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) {
|
public void updateManifest(DashManifest manifest, int periodIndex) {
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
this.periodIndex = periodIndex;
|
this.periodIndex = periodIndex;
|
||||||
|
Period period = manifest.getPeriod(periodIndex);
|
||||||
if (sampleStreams != null) {
|
if (sampleStreams != null) {
|
||||||
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
||||||
sampleStream.getChunkSource().updateManifest(manifest, periodIndex);
|
sampleStream.getChunkSource().updateManifest(manifest, periodIndex);
|
||||||
}
|
}
|
||||||
callback.onContinueLoadingRequested(this);
|
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() {
|
public void release() {
|
||||||
|
|
@ -122,8 +149,27 @@ import java.util.List;
|
||||||
@Override
|
@Override
|
||||||
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
||||||
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
||||||
HashMap<Integer, ChunkSampleStream<DashChunkSource>> primarySampleStreams = new HashMap<>();
|
Map<Integer, ChunkSampleStream<DashChunkSource>> primarySampleStreams = new HashMap<>();
|
||||||
// First pass for primary tracks.
|
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++) {
|
for (int i = 0; i < selections.length; i++) {
|
||||||
if (streams[i] instanceof ChunkSampleStream) {
|
if (streams[i] instanceof ChunkSampleStream) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|
@ -136,10 +182,11 @@ import java.util.List;
|
||||||
primarySampleStreams.put(trackGroupIndex, stream);
|
primarySampleStreams.put(trackGroupIndex, stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streams[i] == null && selections[i] != null) {
|
if (streams[i] == null && selections[i] != null) {
|
||||||
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
|
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
|
||||||
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
|
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
|
||||||
if (trackGroupInfo.isPrimary) {
|
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) {
|
||||||
ChunkSampleStream<DashChunkSource> stream = buildSampleStream(trackGroupInfo,
|
ChunkSampleStream<DashChunkSource> stream = buildSampleStream(trackGroupInfo,
|
||||||
selections[i], positionUs);
|
selections[i], positionUs);
|
||||||
primarySampleStreams.put(trackGroupIndex, stream);
|
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++) {
|
for (int i = 0; i < selections.length; i++) {
|
||||||
if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream)
|
if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream)
|
||||||
&& (selections[i] == null || !mayRetainStreamFlags[i])) {
|
&& (selections[i] == null || !mayRetainStreamFlags[i])) {
|
||||||
|
|
@ -161,7 +240,7 @@ import java.util.List;
|
||||||
if (selections[i] != null) {
|
if (selections[i] != null) {
|
||||||
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
|
int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup());
|
||||||
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
|
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
|
||||||
if (!trackGroupInfo.isPrimary) {
|
if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) {
|
||||||
ChunkSampleStream<?> primaryStream = primarySampleStreams.get(
|
ChunkSampleStream<?> primaryStream = primarySampleStreams.get(
|
||||||
trackGroupInfo.primaryTrackGroupIndex);
|
trackGroupInfo.primaryTrackGroupIndex);
|
||||||
SampleStream stream = streams[i];
|
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
|
@Override
|
||||||
|
|
@ -215,6 +290,9 @@ import java.util.List;
|
||||||
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
||||||
sampleStream.seekToUs(positionUs);
|
sampleStream.seekToUs(positionUs);
|
||||||
}
|
}
|
||||||
|
for (EventSampleStream sampleStream : eventSampleStreams) {
|
||||||
|
sampleStream.seekToUs(positionUs);
|
||||||
|
}
|
||||||
return positionUs;
|
return positionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,62 +306,25 @@ import java.util.List;
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
|
private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(
|
||||||
List<AdaptationSet> adaptationSets) {
|
List<AdaptationSet> adaptationSets, List<EventStream> eventStreams) {
|
||||||
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
|
int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);
|
||||||
|
|
||||||
int primaryGroupCount = groupedAdaptationSetIndices.length;
|
int primaryGroupCount = groupedAdaptationSetIndices.length;
|
||||||
boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];
|
boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];
|
||||||
boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount];
|
boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount];
|
||||||
int totalGroupCount = primaryGroupCount;
|
int totalEmbeddedTrackGroupCount = identifyEmbeddedTracks(primaryGroupCount, adaptationSets,
|
||||||
for (int i = 0; i < primaryGroupCount; i++) {
|
groupedAdaptationSetIndices, primaryGroupHasEventMessageTrackFlags,
|
||||||
if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) {
|
primaryGroupHasCea608TrackFlags);
|
||||||
primaryGroupHasEventMessageTrackFlags[i] = true;
|
|
||||||
totalGroupCount++;
|
|
||||||
}
|
|
||||||
if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) {
|
|
||||||
primaryGroupHasCea608TrackFlags[i] = true;
|
|
||||||
totalGroupCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount + eventStreams.size();
|
||||||
TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];
|
TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];
|
||||||
TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];
|
TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];
|
||||||
|
|
||||||
int trackGroupCount = 0;
|
int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos(adaptationSets,
|
||||||
for (int i = 0; i < primaryGroupCount; i++) {
|
groupedAdaptationSetIndices, primaryGroupCount, primaryGroupHasEventMessageTrackFlags,
|
||||||
int[] adaptationSetIndices = groupedAdaptationSetIndices[i];
|
primaryGroupHasCea608TrackFlags, trackGroups, trackGroupInfos);
|
||||||
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]);
|
buildManifestEventTrackGroupInfos(eventStreams, trackGroups, trackGroupInfos, trackGroupCount);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);
|
return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);
|
||||||
}
|
}
|
||||||
|
|
@ -326,6 +367,90 @@ import java.util.List;
|
||||||
? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices;
|
? 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,
|
private ChunkSampleStream<DashChunkSource> buildSampleStream(TrackGroupInfo trackGroupInfo,
|
||||||
TrackSelection selection, long positionUs) {
|
TrackSelection selection, long positionUs) {
|
||||||
int embeddedTrackCount = 0;
|
int embeddedTrackCount = 0;
|
||||||
|
|
@ -402,24 +527,75 @@ import java.util.List;
|
||||||
|
|
||||||
private static final class TrackGroupInfo {
|
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[] adaptationSetIndices;
|
||||||
public final int trackType;
|
public final int trackType;
|
||||||
public final boolean isPrimary;
|
public @TrackGroupCategory final int trackGroupCategory;
|
||||||
|
|
||||||
|
public final int eventStreamGroupIndex;
|
||||||
public final int primaryTrackGroupIndex;
|
public final int primaryTrackGroupIndex;
|
||||||
public final boolean hasEmbeddedEventMessageTrack;
|
public final boolean hasEmbeddedEventMessageTrack;
|
||||||
public final boolean hasEmbeddedCea608Track;
|
public final boolean hasEmbeddedCea608Track;
|
||||||
|
|
||||||
public TrackGroupInfo(int trackType, int[] adaptationSetIndices, int primaryTrackGroupIndex,
|
public static TrackGroupInfo primaryTrack(int trackType, int[] adaptationSetIndices,
|
||||||
boolean isPrimary, boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track) {
|
int primaryTrackGroupIndex, boolean hasEmbeddedEventMessageTrack,
|
||||||
this.trackType = trackType;
|
boolean hasEmbeddedCea608Track) {
|
||||||
this.adaptationSetIndices = adaptationSetIndices;
|
return new TrackGroupInfo(trackType, CATEGORY_PRIMARY, adaptationSetIndices,
|
||||||
this.primaryTrackGroupIndex = primaryTrackGroupIndex;
|
primaryTrackGroupIndex, hasEmbeddedEventMessageTrack, hasEmbeddedCea608Track, -1);
|
||||||
this.isPrimary = isPrimary;
|
|
||||||
this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack;
|
|
||||||
this.hasEmbeddedCea608Track = hasEmbeddedCea608Track;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -107,7 +107,9 @@ public class DashManifest {
|
||||||
Period period = getPeriod(periodIndex);
|
Period period = getPeriod(periodIndex);
|
||||||
ArrayList<AdaptationSet> copyAdaptationSets =
|
ArrayList<AdaptationSet> copyAdaptationSets =
|
||||||
copyAdaptationSets(period.adaptationSets, keys);
|
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;
|
long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET;
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ import android.text.TextUtils;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
import android.util.Xml;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||||
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
|
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.SegmentList;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate;
|
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;
|
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.UriUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.android.exoplayer2.util.XmlPullParserUtil;
|
import com.google.android.exoplayer2.util.XmlPullParserUtil;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -47,6 +50,7 @@ import org.xml.sax.helpers.DefaultHandler;
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
import org.xmlpull.v1.XmlPullParserFactory;
|
import org.xmlpull.v1.XmlPullParserFactory;
|
||||||
|
import org.xmlpull.v1.XmlSerializer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A parser of media presentation description files.
|
* A parser of media presentation description files.
|
||||||
|
|
@ -197,6 +201,7 @@ public class DashManifestParser extends DefaultHandler
|
||||||
long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET);
|
long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET);
|
||||||
SegmentBase segmentBase = null;
|
SegmentBase segmentBase = null;
|
||||||
List<AdaptationSet> adaptationSets = new ArrayList<>();
|
List<AdaptationSet> adaptationSets = new ArrayList<>();
|
||||||
|
List<EventStream> eventStreams = new ArrayList<>();
|
||||||
boolean seenFirstBaseUrl = false;
|
boolean seenFirstBaseUrl = false;
|
||||||
do {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
|
|
@ -207,6 +212,8 @@ public class DashManifestParser extends DefaultHandler
|
||||||
}
|
}
|
||||||
} else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) {
|
} else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) {
|
||||||
adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase));
|
adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase));
|
||||||
|
} else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) {
|
||||||
|
eventStreams.add(parseEventStream(xpp));
|
||||||
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
|
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
|
||||||
segmentBase = parseSegmentBase(xpp, null);
|
segmentBase = parseSegmentBase(xpp, null);
|
||||||
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
|
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
|
||||||
|
|
@ -216,11 +223,12 @@ public class DashManifestParser extends DefaultHandler
|
||||||
}
|
}
|
||||||
} while (!XmlPullParserUtil.isEndTag(xpp, "Period"));
|
} 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) {
|
protected Period buildPeriod(String id, long startMs, List<AdaptationSet> adaptationSets,
|
||||||
return new Period(id, startMs, adaptationSets);
|
List<EventStream> eventStreams) {
|
||||||
|
return new Period(id, startMs, adaptationSets, eventStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdaptationSet parsing.
|
// AdaptationSet parsing.
|
||||||
|
|
@ -387,7 +395,7 @@ public class DashManifestParser extends DefaultHandler
|
||||||
Log.w(TAG, "Skipping malformed cenc:pssh data");
|
Log.w(TAG, "Skipping malformed cenc:pssh data");
|
||||||
data = null;
|
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) {
|
&& xpp.next() == XmlPullParser.TEXT) {
|
||||||
// The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady.
|
// The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady.
|
||||||
data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID,
|
data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID,
|
||||||
|
|
@ -660,6 +668,143 @@ public class DashManifestParser extends DefaultHandler
|
||||||
startNumber, duration, timeline, initializationTemplate, mediaTemplate);
|
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)
|
protected List<SegmentTimelineElement> parseSegmentTimeline(XmlPullParser xpp)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();
|
List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source.dash.manifest;
|
package com.google.android.exoplayer2.source.dash.manifest;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -27,7 +28,7 @@ public class Period {
|
||||||
/**
|
/**
|
||||||
* The period identifier, if one exists.
|
* The period identifier, if one exists.
|
||||||
*/
|
*/
|
||||||
public final String id;
|
@Nullable public final String id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The start time of the period in milliseconds.
|
* The start time of the period in milliseconds.
|
||||||
|
|
@ -39,15 +40,32 @@ public class Period {
|
||||||
*/
|
*/
|
||||||
public final List<AdaptationSet> adaptationSets;
|
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 id The period identifier. May be null.
|
||||||
* @param startMs The start time of the period in milliseconds.
|
* @param startMs The start time of the period in milliseconds.
|
||||||
* @param adaptationSets The adaptation sets belonging to the period.
|
* @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.id = id;
|
||||||
this.startMs = startMs;
|
this.startMs = startMs;
|
||||||
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
|
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
|
||||||
|
this.eventStreams = Collections.unmodifiableList(eventStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue