From 01151c9c65810a9fff439f5901a85bde4e6a4f63 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:10:52 +0000 Subject: [PATCH 1/4] Don't append base uri if chunkUrl is absolute. --- .../exoplayer/smoothstreaming/SmoothStreamingManifest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index 271949b58b..7a6a32e44a 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -274,7 +274,7 @@ public class SmoothStreamingManifest { String chunkUrl = chunkTemplate .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate)) .replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes.get(chunkIndex))); - return baseUri.buildUpon().appendEncodedPath(chunkUrl).build(); + return Util.getMergedUri(baseUri, chunkUrl); } } From cf80c4d9cb3de06e9861bb85ca3242ded6e6b072 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:12:04 +0000 Subject: [PATCH 2/4] Allow passing of optional parameters in MediaDrm key requests. --- .../demo/full/player/DashRendererBuilder.java | 4 ++-- .../SmoothStreamingRendererBuilder.java | 2 +- .../drm/StreamingDrmSessionManager.java | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java index f998d6e30f..8ffd60218a 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java @@ -279,8 +279,8 @@ public class DashRendererBuilder implements RendererBuilder, public static Pair getDrmSessionManagerData(DemoPlayer player, MediaDrmCallback drmCallback) throws UnsupportedSchemeException { StreamingDrmSessionManager streamingDrmSessionManager = new StreamingDrmSessionManager( - DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, player.getMainHandler(), - player); + DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, null, + player.getMainHandler(), player); return Pair.create((DrmSessionManager) streamingDrmSessionManager, getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 7d88519b45..fd9c220cb2 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -252,7 +252,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, public static DrmSessionManager getDrmSessionManager(UUID uuid, DemoPlayer player, MediaDrmCallback drmCallback) throws UnsupportedSchemeException { - return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, + return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); } diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index 8d4e697d4d..866c5f96ef 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -30,6 +30,7 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -61,6 +62,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private final Handler eventHandler; private final EventListener eventListener; private final MediaDrm mediaDrm; + private final HashMap optionalKeyRequestParameters; /* package */ final MediaDrmHandler mediaDrmHandler; /* package */ final MediaDrmCallback callback; @@ -79,20 +81,33 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private byte[] schemePsshData; private byte[] sessionId; + /** + * @deprecated Use the other constructor, passing null as {@code optionalKeyRequestParameters}. + */ + @Deprecated + public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback, + Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException { + this(uuid, playbackLooper, callback, null, eventHandler, eventListener); + } + /** * @param uuid The UUID of the drm scheme. * @param playbackLooper The looper associated with the media playback thread. Should usually be * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}. * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @throws UnsupportedSchemeException If the specified DRM scheme is not supported. */ public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback, - Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException { + HashMap optionalKeyRequestParameters, Handler eventHandler, + EventListener eventListener) throws UnsupportedSchemeException { this.uuid = uuid; this.callback = callback; + this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventHandler = eventHandler; this.eventListener = eventListener; mediaDrm = new MediaDrm(uuid); @@ -250,7 +265,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { KeyRequest keyRequest; try { keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType, - MediaDrm.KEY_TYPE_STREAMING, null); + MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); } catch (NotProvisionedException e) { onKeysError(e); From 2f0a1779e2183b990885b099d1beed38438d9bac Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:13:52 +0000 Subject: [PATCH 3/4] Stop piping PSSH information through the extractor. It's cleaner to not inject data into the extractor only so that it can be read out as though it were parsed from the stream. This is also an incremental step towards fixing Github issue #119. --- .../exoplayer/chunk/Mp4MediaChunk.java | 23 ++++++++++++++++--- .../exoplayer/dash/DashChunkSource.java | 2 +- .../parser/mp4/FragmentedMp4Extractor.java | 14 ----------- .../SmoothStreamingChunkSource.java | 17 ++++++++------ 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java index a4d05cacd7..e39c53ebff 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java @@ -40,6 +40,17 @@ public final class Mp4MediaChunk extends MediaChunk { private MediaFormat mediaFormat; private Map psshInfo; + /** + * @deprecated Use the other constructor, passing null as {@code psshInfo}. + */ + @Deprecated + public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, + int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, + Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) { + this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex, + extractor, null, maybeSelfContained, sampleOffsetUs); + } + /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. @@ -49,6 +60,8 @@ public final class Mp4MediaChunk extends MediaChunk { * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param extractor The extractor that will be used to extract the samples. + * @param psshInfo Pssh data. May be null if pssh data is present within the stream, meaning it + * can be obtained directly from {@code extractor}, or if no pssh data is required. * @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might * contain a moov atom defining the media format of the chunk. This parameter can always be * safely set to true. Setting to false where the chunk is known to not be self contained may @@ -56,12 +69,13 @@ public final class Mp4MediaChunk extends MediaChunk { * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor. */ public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, - int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, - Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) { + int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor, + Map psshInfo, boolean maybeSelfContained, long sampleOffsetUs) { super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); this.extractor = extractor; this.maybeSelfContained = maybeSelfContained; this.sampleOffsetUs = sampleOffsetUs; + this.psshInfo = psshInfo; } @Override @@ -97,7 +111,10 @@ public final class Mp4MediaChunk extends MediaChunk { } if (prepared) { mediaFormat = extractor.getFormat(); - psshInfo = extractor.getPsshInfo(); + Map extractorPsshInfo = extractor.getPsshInfo(); + if (extractorPsshInfo != null) { + psshInfo = extractorPsshInfo; + } } } return prepared; diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 9bcb1aa3b8..d0c123bdc9 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -438,7 +438,7 @@ public class DashChunkSource implements ChunkSource { startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); } else { return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, - endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, false, + endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, null, false, presentationTimeOffsetUs); } } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java index 9950aecd2a..34f0404083 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java @@ -189,20 +189,6 @@ public final class FragmentedMp4Extractor implements Extractor { this.track = track; } - /** - * Sideloads pssh information into the extractor, so that it can be read through - * {@link #getPsshInfo()}. - * - * @param uuid The UUID of the scheme for which information is being sideloaded. - * @param data The corresponding data. - */ - public void putPsshInfo(UUID uuid, byte[] data) { - // TODO: This is for SmoothStreaming. Consider using something other than - // FragmentedMp4Extractor.getPsshInfo to obtain the pssh data for that use case, so that we can - // remove this method. - psshData.put(uuid, data); - } - @Override public Map getPsshInfo() { return psshData.isEmpty() ? null : psshData; diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 2b676e6b52..936fdf824d 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -48,6 +48,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.UUID; /** * An {@link ChunkSource} for SmoothStreaming. @@ -69,6 +71,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { private final int maxHeight; private final SparseArray extractors; + private final Map psshInfo; private final SmoothStreamingFormat[] formats; private SmoothStreamingManifest currentManifest; @@ -140,6 +143,9 @@ public class SmoothStreamingChunkSource implements ChunkSource { byte[] keyId = getKeyId(protectionElement.data); trackEncryptionBoxes = new TrackEncryptionBox[1]; trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); + psshInfo = Collections.singletonMap(protectionElement.uuid, protectionElement.data); + } else { + psshInfo = null; } int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; @@ -163,9 +169,6 @@ public class SmoothStreamingChunkSource implements ChunkSource { FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale, mediaFormat, trackEncryptionBoxes)); - if (protectionElement != null) { - extractor.putPsshInfo(protectionElement.uuid, protectionElement.data); - } extractors.put(trackIndex, extractor); } this.maxHeight = maxHeight; @@ -296,8 +299,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex); Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, - extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, currentAbsoluteChunkIndex, - isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); + extractors.get(Integer.parseInt(selectedFormat.id)), psshInfo, dataSource, + currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); out.chunk = mediaChunk; } @@ -361,7 +364,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { } private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, - Extractor extractor, DataSource dataSource, int chunkIndex, + Extractor extractor, Map psshInfo, DataSource dataSource, int chunkIndex, boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) { int nextChunkIndex = isLast ? -1 : chunkIndex + 1; long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs; @@ -370,7 +373,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs, - nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs); + nextStartTimeUs, nextChunkIndex, extractor, psshInfo, false, -chunkStartTimeUs); } private static byte[] getKeyId(byte[] initData) { From c4b2a0121230cb533a658fd6c55b4c01199a3000 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 8 Dec 2014 20:15:06 +0000 Subject: [PATCH 4/4] Allow out-of-band pssh data for DASH playbacks. This fixes the referenced issue, except that the MPD parser needs to actually parse out UUID and binary data for schemes that we wish to support. Alternatively, it's easy to applications to do this themselves by extending the parser and overriding the parseContentProtection and buildContentProtection methods. Github Issue: #119 --- .../exoplayer/dash/DashChunkSource.java | 27 ++++++++++++++++--- .../exoplayer/dash/mpd/ContentProtection.java | 20 ++++++++++++-- .../MediaPresentationDescriptionParser.java | 2 +- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index d0c123bdc9..932a8ea598 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.Mp4MediaChunk; import com.google.android.exoplayer.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.ContentProtection; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.RangedUri; @@ -53,6 +54,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.UUID; /** * An {@link ChunkSource} for DASH streams. @@ -92,6 +95,7 @@ public class DashChunkSource implements ChunkSource { private final ManifestFetcher manifestFetcher; private final int adaptationSetIndex; private final int[] representationIndices; + private final Map psshInfo; private MediaPresentationDescription currentManifest; private boolean finishedCurrentManifest; @@ -180,6 +184,7 @@ public class DashChunkSource implements ChunkSource { this.evaluation = new Evaluation(); this.headerBuilder = new StringBuilder(); + psshInfo = getPsshInfo(currentManifest, adaptationSetIndex); Representation[] representations = getFilteredRepresentations(currentManifest, adaptationSetIndex, representationIndices); long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US) @@ -438,7 +443,7 @@ public class DashChunkSource implements ChunkSource { startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); } else { return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs, - endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, null, false, + endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, psshInfo, false, presentationTimeOffsetUs); } } @@ -463,8 +468,8 @@ public class DashChunkSource implements ChunkSource { private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest, int adaptationSetIndex, int[] representationIndices) { - List representations = - manifest.periods.get(0).adaptationSets.get(adaptationSetIndex).representations; + AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); + List representations = adaptationSet.representations; if (representationIndices == null) { Representation[] filteredRepresentations = new Representation[representations.size()]; representations.toArray(filteredRepresentations); @@ -478,6 +483,22 @@ public class DashChunkSource implements ChunkSource { } } + private static Map getPsshInfo(MediaPresentationDescription manifest, + int adaptationSetIndex) { + AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); + if (adaptationSet.contentProtections.isEmpty()) { + return null; + } else { + Map psshInfo = new HashMap(); + for (ContentProtection contentProtection : adaptationSet.contentProtections) { + if (contentProtection.uuid != null && contentProtection.data != null) { + psshInfo.put(contentProtection.uuid, contentProtection.data); + } + } + return psshInfo.isEmpty() ? null : psshInfo; + } + } + private static MediaPresentationDescription buildManifest(List representations) { Representation firstRepresentation = representations.get(0); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); 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 bd6acca9af..c8f7cfb501 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,8 @@ */ package com.google.android.exoplayer.dash.mpd; +import java.util.UUID; + /** * Represents a ContentProtection tag in an AdaptationSet. */ @@ -26,10 +28,24 @@ public class ContentProtection { public final String schemeUriId; /** - * @param schemeUriId Identifies the content protection scheme. + * The UUID of the protection scheme. May be null. */ - public ContentProtection(String schemeUriId) { + public final UUID uuid; + + /** + * Protection scheme specific data. May be null. + */ + public final byte[] data; + + /** + * @param schemeUriId Identifies the content protection scheme. + * @param uuid The UUID of the protection scheme, if known. May be null. + * @param data Protection scheme specific initialization data. May be null. + */ + public ContentProtection(String schemeUriId, UUID uuid, byte[] data) { this.schemeUriId = schemeUriId; + this.uuid = uuid; + this.data = data; } } 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 bf1ba532b7..a8ed7c03f2 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 @@ -257,7 +257,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } protected ContentProtection buildContentProtection(String schemeIdUri) { - return new ContentProtection(schemeIdUri); + return new ContentProtection(schemeIdUri, null, null); } /**