From acd1b9acff42e79d1671ab3def7bdb141c742baa Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 31 Mar 2015 11:28:51 +0100 Subject: [PATCH] Enable ContentProtect elements at the Representation level --- .../exoplayer/dash/mpd/ContentProtection.java | 35 ++++- .../MediaPresentationDescriptionParser.java | 135 ++++++++++++++++-- 2 files changed, 161 insertions(+), 9 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java index c8f7cfb501..8f02fdc6f4 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer.dash.mpd; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Util; + +import java.util.Arrays; import java.util.UUID; /** @@ -43,9 +47,38 @@ public class ContentProtection { * @param data Protection scheme specific initialization data. May be null. */ public ContentProtection(String schemeUriId, UUID uuid, byte[] data) { - this.schemeUriId = schemeUriId; + this.schemeUriId = Assertions.checkNotNull(schemeUriId); this.uuid = uuid; this.data = data; } + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ContentProtection)) { + return false; + } + if (obj == this) { + return true; + } + + ContentProtection other = (ContentProtection) obj; + return schemeUriId.equals(other.schemeUriId) + && Util.areEqual(uuid, other.uuid) + && Arrays.equals(data, other.data); + } + + @Override + public int hashCode() { + int hashCode = 1; + + hashCode = hashCode * 37 + schemeUriId.hashCode(); + if (uuid != null) { + hashCode = hashCode * 37 + uuid.hashCode(); + } + if (data != null) { + hashCode = hashCode * 37 + Arrays.hashCode(data); + } + return hashCode; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 86722c0020..7767fa8810 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -38,6 +38,8 @@ import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; /** @@ -178,24 +180,22 @@ public class MediaPresentationDescriptionParser extends DefaultHandler int contentType = parseAdaptationSetTypeFromMimeType(mimeType); int id = -1; - List contentProtections = null; + ContentProtectionsBuilder contentProtectionsBuilder = new ContentProtectionsBuilder(); List representations = new ArrayList(); do { xpp.next(); if (isStartTag(xpp, "BaseURL")) { baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "ContentProtection")) { - if (contentProtections == null) { - contentProtections = new ArrayList(); - } - contentProtections.add(parseContentProtection(xpp)); + contentProtectionsBuilder.addAdaptationSetProtection(parseContentProtection(xpp)); } else if (isStartTag(xpp, "ContentComponent")) { id = Integer.parseInt(xpp.getAttributeValue(null, "id")); contentType = checkAdaptationSetTypeConsistency(contentType, parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); } else if (isStartTag(xpp, "Representation")) { Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs, - periodDurationMs, mimeType, language, segmentBase); + periodDurationMs, mimeType, language, segmentBase, contentProtectionsBuilder); + contentProtectionsBuilder.endRepresentation(); contentType = checkAdaptationSetTypeConsistency(contentType, parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); representations.add(representation); @@ -211,7 +211,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } while (!isEndTag(xpp, "AdaptationSet")); - return buildAdaptationSet(id, contentType, representations, contentProtections); + return buildAdaptationSet(id, contentType, representations, contentProtectionsBuilder.build()); } protected AdaptationSet buildAdaptationSet(int id, int contentType, @@ -289,7 +289,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl, long periodStartMs, long periodDurationMs, String mimeType, String language, - SegmentBase segmentBase) throws XmlPullParserException, IOException { + SegmentBase segmentBase, ContentProtectionsBuilder contentProtectionsBuilder) + throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); @@ -312,6 +313,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } else if (isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, periodDurationMs); + } else if (isStartTag(xpp, "ContentProtection")) { + contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp)); } } while (!isEndTag(xpp, "Representation")); @@ -577,4 +580,120 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return value == null ? defaultValue : value; } + /** + * Builds a list of {@link ContentProtection} elements for an {@link AdaptationSet}. + *

+ * If child Representation elements contain ContentProtection elements, then it is required that + * they all define the same ones. If they do, the ContentProtection elements are bubbled up to the + * AdaptationSet. Child Representation elements defining different ContentProtection elements is + * considered an error. + */ + protected static final class ContentProtectionsBuilder implements Comparator { + + private ArrayList adaptationSetProtections; + private ArrayList representationProtections; + private ArrayList currentRepresentationProtections; + + private boolean representationProtectionsSet; + + /** + * Adds a {@link ContentProtection} found in the AdaptationSet element. + * + * @param contentProtection The {@link ContentProtection} to add. + */ + public void addAdaptationSetProtection(ContentProtection contentProtection) { + if (adaptationSetProtections == null) { + adaptationSetProtections = new ArrayList(); + } + maybeAddContentProtection(adaptationSetProtections, contentProtection); + } + + /** + * Adds a {@link ContentProtection} found in a child Representation element. + * + * @param contentProtection The {@link ContentProtection} to add. + */ + public void addRepresentationProtection(ContentProtection contentProtection) { + if (currentRepresentationProtections == null) { + currentRepresentationProtections = new ArrayList(); + } + maybeAddContentProtection(currentRepresentationProtections, contentProtection); + } + + /** + * Should be invoked after processing each child Representation element, in order to apply + * consistency checks. + */ + public void endRepresentation() { + if (!representationProtectionsSet) { + if (currentRepresentationProtections != null) { + Collections.sort(currentRepresentationProtections, this); + } + representationProtections = currentRepresentationProtections; + representationProtectionsSet = true; + } else { + // Assert that each Representation element defines the same ContentProtection elements. + if (currentRepresentationProtections == null) { + Assertions.checkState(representationProtections == null); + } else { + Collections.sort(currentRepresentationProtections, this); + Assertions.checkState(currentRepresentationProtections.equals(representationProtections)); + } + } + currentRepresentationProtections = null; + } + + /** + * Returns the final list of consistent {@link ContentProtection} elements. + */ + public ArrayList build() { + if (adaptationSetProtections == null) { + return representationProtections; + } else if (representationProtections == null) { + return adaptationSetProtections; + } else { + // Bubble up ContentProtection elements found in the child Representation elements. + for (int i = 0; i < representationProtections.size(); i++) { + maybeAddContentProtection(adaptationSetProtections, representationProtections.get(i)); + } + return adaptationSetProtections; + } + } + + /** + * Checks a ContentProtection for consistency with the given list, adding it if necessary. + *

    + *
  • If the new ContentProtection matches another in the list, it's consistent and is not + * added to the list. + *
  • If the new ContentProtection has the same schemeUriId as another ContentProtection in the + * list, but its other attributes do not match, then it's inconsistent and an + * {@link IllegalStateException} is thrown. + *
  • Else the new ContentProtection has a unique schemeUriId, it's consistent and is added. + *
+ * + * @param contentProtections The list of ContentProtection elements currently known. + * @param contentProtection The ContentProtection to add. + */ + private void maybeAddContentProtection(List contentProtections, + ContentProtection contentProtection) { + if (!contentProtections.contains(contentProtection)) { + for (int i = 0; i < contentProtections.size(); i++) { + // If contains returned false (no complete match), but find a matching schemeUriId, then + // the MPD contains inconsistent ContentProtection data. + Assertions.checkState( + !contentProtections.get(i).schemeUriId.equals(contentProtection.schemeUriId)); + } + contentProtections.add(contentProtection); + } + } + + // Comparator implementation. + + @Override + public int compare(ContentProtection first, ContentProtection second) { + return first.schemeUriId.compareTo(second.schemeUriId); + } + + } + }