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/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..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, 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); } /** 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); 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) { 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); } }