Skip to content

Commit

Permalink
Supports Out-of-band, in MPD EventStream.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
botaydotcom authored and ojw28 committed Nov 13, 2017
1 parent ed2e4dd commit 3171c86
Show file tree
Hide file tree
Showing 11 changed files with 867 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;

Expand All @@ -40,7 +41,7 @@ public Metadata decode(MetadataInputBuffer inputBuffer) {
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

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;
Expand Down Expand Up @@ -55,4 +56,63 @@ public void testDecodeEventMessage() {
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);
}

}
61 changes: 61 additions & 0 deletions library/dash/src/androidTest/assets/sample_mpd_4_event_stream
Original file line number Diff line number Diff line change
@@ -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>

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;
Expand All @@ -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.
Expand Down Expand Up @@ -69,6 +71,52 @@ public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IO
}
}

public void testParseMediaPresentationDescriptionCanParseEventStream()
throws IOException {
DashManifestParser parser = new DashManifestParser();
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_4_EVENT_STREAM));

Period period = mpd.getPeriod(0);
assertEquals(3, period.eventStreams.size());
// assert text-only event stream
EventStream eventStream1 = period.eventStreams.get(0);
assertEquals(1, eventStream1.events.length);
EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0,
"+ 1 800 10101010".getBytes());
assertEquals(expectedEvent1, eventStream1.events[0]);

// assert CData-structured event stream
EventStream eventStream2 = period.eventStreams.get(1);
assertEquals(1, eventStream2.events.length);
assertEquals(
new EventMessage("urn:dvb:iptv:cpm:2014", "", 1500000, 1,
("<![CDATA[<BroadcastEvent>\n"
+ " <Program crid=\"crid://broadcaster.example.com/ABCDEF\"/>\n"
+ " <InstanceDescription>\n"
+ " <Title xml:lang=\"en\">The title</Title>\n"
+ " <Synopsis xml:lang=\"en\" length=\"medium\">The description</Synopsis>\n"
+ " <ParentalGuidance>\n"
+ " <mpeg7:ParentalRating href=\"urn:dvb:iptv:rating:2014:15\"/>\n"
+ " <mpeg7:Region>GB</mpeg7:Region>\n"
+ " </ParentalGuidance>\n"
+ " </InstanceDescription>\n"
+ " </BroadcastEvent>]]>").getBytes()),
eventStream2.events[0]);

// assert xml-structured event stream
EventStream eventStream3 = period.eventStreams.get(2);
assertEquals(1, eventStream3.events.length);
assertEquals(
new EventMessage("urn:scte:scte35:2014:xml+bin", "", 1000000, 2,
("<scte35:Signal>\n"
+ " <scte35:Binary>\n"
+ " /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\n"
+ " </scte35:Binary>\n"
+ " </scte35:Signal>").getBytes()),
eventStream3.events[0]);
}

public void testParseCea608AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel(
buildCea608AccessibilityDescriptors("CC1=eng")));
Expand Down
Loading

0 comments on commit 3171c86

Please sign in to comment.