• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.drm;
17 
18 import android.annotation.SuppressLint;
19 import android.os.Handler;
20 import android.os.Looper;
21 import android.os.Message;
22 import androidx.annotation.IntDef;
23 import androidx.annotation.Nullable;
24 import androidx.annotation.RequiresApi;
25 import com.google.android.exoplayer2.C;
26 import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
27 import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
28 import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
29 import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
30 import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
31 import com.google.android.exoplayer2.util.Assertions;
32 import com.google.android.exoplayer2.util.Log;
33 import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
34 import com.google.android.exoplayer2.util.Util;
35 import java.lang.annotation.Documented;
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.UUID;
44 
45 /** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */
46 @RequiresApi(18)
47 public class DefaultDrmSessionManager implements DrmSessionManager {
48 
49   /**
50    * Builder for {@link DefaultDrmSessionManager} instances.
51    *
52    * <p>See {@link #Builder} for the list of default values.
53    */
54   public static final class Builder {
55 
56     private final HashMap<String, String> keyRequestParameters;
57     private UUID uuid;
58     private ExoMediaDrm.Provider exoMediaDrmProvider;
59     private boolean multiSession;
60     private int[] useDrmSessionsForClearContentTrackTypes;
61     private boolean playClearSamplesWithoutKeys;
62     private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
63 
64     /**
65      * Creates a builder with default values. The default values are:
66      *
67      * <ul>
68      *   <li>{@link #setKeyRequestParameters keyRequestParameters}: An empty map.
69      *   <li>{@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}.
70      *   <li>{@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link
71      *       FrameworkMediaDrm#DEFAULT_PROVIDER}.
72      *   <li>{@link #setMultiSession multiSession}: {@code false}.
73      *   <li>{@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks.
74      *   <li>{@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}.
75      *   <li>{@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link
76      *       DefaultLoadErrorHandlingPolicy}.
77      * </ul>
78      */
Builder()79     public Builder() {
80       keyRequestParameters = new HashMap<>();
81       uuid = C.WIDEVINE_UUID;
82       exoMediaDrmProvider = FrameworkMediaDrm.DEFAULT_PROVIDER;
83       loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
84       useDrmSessionsForClearContentTrackTypes = new int[0];
85     }
86 
87     /**
88      * Sets the key request parameters to pass as the last argument to {@link
89      * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null if not parameters need to
90      * be passed.
91      *
92      * <p>Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}.
93      *
94      * @param keyRequestParameters A map with parameters.
95      * @return This builder.
96      */
setKeyRequestParameters(@ullable Map<String, String> keyRequestParameters)97     public Builder setKeyRequestParameters(@Nullable Map<String, String> keyRequestParameters) {
98       this.keyRequestParameters.clear();
99       if (keyRequestParameters != null) {
100         this.keyRequestParameters.putAll(keyRequestParameters);
101       }
102       return this;
103     }
104 
105     /**
106      * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use.
107      *
108      * @param uuid The UUID of the DRM scheme.
109      * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}.
110      * @return This builder.
111      */
setUuidAndExoMediaDrmProvider( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider)112     public Builder setUuidAndExoMediaDrmProvider(
113         UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) {
114       this.uuid = Assertions.checkNotNull(uuid);
115       this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider);
116       return this;
117     }
118 
119     /**
120      * Sets whether this session manager is allowed to acquire multiple simultaneous sessions.
121      *
122      * <p>Users should pass false when a single key request will obtain all keys required to decrypt
123      * the associated content. {@code multiSession} is required when content uses key rotation.
124      *
125      * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous
126      *     sessions.
127      * @return This builder.
128      */
setMultiSession(boolean multiSession)129     public Builder setMultiSession(boolean multiSession) {
130       this.multiSession = multiSession;
131       return this;
132     }
133 
134     /**
135      * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear
136      * sections of the media content.
137      *
138      * <p>Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders
139      * when transitioning between clear and encrypted sections of content.
140      *
141      * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO}
142      *     and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of
143      *     whether the content is clear or encrypted.
144      * @return This builder.
145      * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains
146      *     track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}.
147      */
setUseDrmSessionsForClearContent( int... useDrmSessionsForClearContentTrackTypes)148     public Builder setUseDrmSessionsForClearContent(
149         int... useDrmSessionsForClearContentTrackTypes) {
150       for (int trackType : useDrmSessionsForClearContentTrackTypes) {
151         Assertions.checkArgument(
152             trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
153       }
154       this.useDrmSessionsForClearContentTrackTypes =
155           useDrmSessionsForClearContentTrackTypes.clone();
156       return this;
157     }
158 
159     /**
160      * Sets whether clear samples within protected content should be played when keys for the
161      * encrypted part of the content have yet to be loaded.
162      *
163      * @param playClearSamplesWithoutKeys Whether clear samples within protected content should be
164      *     played when keys for the encrypted part of the content have yet to be loaded.
165      * @return This builder.
166      */
setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys)167     public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) {
168       this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
169       return this;
170     }
171 
172     /**
173      * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests.
174      *
175      * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
176      * @return This builder.
177      */
setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy)178     public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
179       this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy);
180       return this;
181     }
182 
183     /** Builds a {@link DefaultDrmSessionManager} instance. */
build(MediaDrmCallback mediaDrmCallback)184     public DefaultDrmSessionManager build(MediaDrmCallback mediaDrmCallback) {
185       return new DefaultDrmSessionManager(
186           uuid,
187           exoMediaDrmProvider,
188           mediaDrmCallback,
189           keyRequestParameters,
190           multiSession,
191           useDrmSessionsForClearContentTrackTypes,
192           playClearSamplesWithoutKeys,
193           loadErrorHandlingPolicy);
194     }
195   }
196 
197   /**
198    * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does
199    * not contain scheme data for the required UUID.
200    */
201   public static final class MissingSchemeDataException extends Exception {
202 
MissingSchemeDataException(UUID uuid)203     private MissingSchemeDataException(UUID uuid) {
204       super("Media does not support uuid: " + uuid);
205     }
206   }
207 
208   /**
209    * A key for specifying PlayReady custom data in the key request parameters passed to {@link
210    * Builder#setKeyRequestParameters(Map)}.
211    */
212   public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
213 
214   /**
215    * Determines the action to be done after a session acquired. One of {@link #MODE_PLAYBACK},
216    * {@link #MODE_QUERY}, {@link #MODE_DOWNLOAD} or {@link #MODE_RELEASE}.
217    */
218   @Documented
219   @Retention(RetentionPolicy.SOURCE)
220   @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE})
221   public @interface Mode {}
222   /**
223    * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline
224    * licenses.
225    */
226   public static final int MODE_PLAYBACK = 0;
227   /** Restores an offline license to allow its status to be queried. */
228   public static final int MODE_QUERY = 1;
229   /** Downloads an offline license or renews an existing one. */
230   public static final int MODE_DOWNLOAD = 2;
231   /** Releases an existing offline license. */
232   public static final int MODE_RELEASE = 3;
233   /** Number of times to retry for initial provisioning and key request for reporting error. */
234   public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
235 
236   private static final String TAG = "DefaultDrmSessionMgr";
237 
238   private final UUID uuid;
239   private final ExoMediaDrm.Provider exoMediaDrmProvider;
240   private final MediaDrmCallback callback;
241   private final HashMap<String, String> keyRequestParameters;
242   private final boolean multiSession;
243   private final int[] useDrmSessionsForClearContentTrackTypes;
244   private final boolean playClearSamplesWithoutKeys;
245   private final ProvisioningManagerImpl provisioningManagerImpl;
246   private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
247 
248   private final List<DefaultDrmSession> sessions;
249   private final List<DefaultDrmSession> provisioningSessions;
250 
251   private int prepareCallsCount;
252   @Nullable private ExoMediaDrm exoMediaDrm;
253   @Nullable private DefaultDrmSession placeholderDrmSession;
254   @Nullable private DefaultDrmSession noMultiSessionDrmSession;
255   @Nullable private Looper playbackLooper;
256   private int mode;
257   @Nullable private byte[] offlineLicenseKeySetId;
258 
259   /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;
260 
261   /**
262    * @param uuid The UUID of the drm scheme.
263    * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
264    * @param callback Performs key and provisioning requests.
265    * @param keyRequestParameters An optional map of parameters to pass as the last argument to
266    *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
267    * @deprecated Use {@link Builder} instead.
268    */
269   @SuppressWarnings("deprecation")
270   @Deprecated
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters)271   public DefaultDrmSessionManager(
272       UUID uuid,
273       ExoMediaDrm exoMediaDrm,
274       MediaDrmCallback callback,
275       @Nullable HashMap<String, String> keyRequestParameters) {
276     this(
277         uuid,
278         exoMediaDrm,
279         callback,
280         keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
281         /* multiSession= */ false,
282         INITIAL_DRM_REQUEST_RETRY_COUNT);
283   }
284 
285   /**
286    * @param uuid The UUID of the drm scheme.
287    * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
288    * @param callback Performs key and provisioning requests.
289    * @param keyRequestParameters An optional map of parameters to pass as the last argument to
290    *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
291    * @param multiSession A boolean that specify whether multiple key session support is enabled.
292    *     Default is false.
293    * @deprecated Use {@link Builder} instead.
294    */
295   @Deprecated
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters, boolean multiSession)296   public DefaultDrmSessionManager(
297       UUID uuid,
298       ExoMediaDrm exoMediaDrm,
299       MediaDrmCallback callback,
300       @Nullable HashMap<String, String> keyRequestParameters,
301       boolean multiSession) {
302     this(
303         uuid,
304         exoMediaDrm,
305         callback,
306         keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
307         multiSession,
308         INITIAL_DRM_REQUEST_RETRY_COUNT);
309   }
310 
311   /**
312    * @param uuid The UUID of the drm scheme.
313    * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
314    * @param callback Performs key and provisioning requests.
315    * @param keyRequestParameters An optional map of parameters to pass as the last argument to
316    *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
317    * @param multiSession A boolean that specify whether multiple key session support is enabled.
318    *     Default is false.
319    * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and
320    *     key request before reporting error.
321    * @deprecated Use {@link Builder} instead.
322    */
323   @Deprecated
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters, boolean multiSession, int initialDrmRequestRetryCount)324   public DefaultDrmSessionManager(
325       UUID uuid,
326       ExoMediaDrm exoMediaDrm,
327       MediaDrmCallback callback,
328       @Nullable HashMap<String, String> keyRequestParameters,
329       boolean multiSession,
330       int initialDrmRequestRetryCount) {
331     this(
332         uuid,
333         new ExoMediaDrm.AppManagedProvider(exoMediaDrm),
334         callback,
335         keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
336         multiSession,
337         /* useDrmSessionsForClearContentTrackTypes= */ new int[0],
338         /* playClearSamplesWithoutKeys= */ false,
339         new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount));
340   }
341 
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider, MediaDrmCallback callback, HashMap<String, String> keyRequestParameters, boolean multiSession, int[] useDrmSessionsForClearContentTrackTypes, boolean playClearSamplesWithoutKeys, LoadErrorHandlingPolicy loadErrorHandlingPolicy)342   private DefaultDrmSessionManager(
343       UUID uuid,
344       ExoMediaDrm.Provider exoMediaDrmProvider,
345       MediaDrmCallback callback,
346       HashMap<String, String> keyRequestParameters,
347       boolean multiSession,
348       int[] useDrmSessionsForClearContentTrackTypes,
349       boolean playClearSamplesWithoutKeys,
350       LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
351     Assertions.checkNotNull(uuid);
352     Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
353     this.uuid = uuid;
354     this.exoMediaDrmProvider = exoMediaDrmProvider;
355     this.callback = callback;
356     this.keyRequestParameters = keyRequestParameters;
357     this.multiSession = multiSession;
358     this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes;
359     this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
360     this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
361     provisioningManagerImpl = new ProvisioningManagerImpl();
362     mode = MODE_PLAYBACK;
363     sessions = new ArrayList<>();
364     provisioningSessions = new ArrayList<>();
365   }
366 
367   /**
368    * Sets the mode, which determines the role of sessions acquired from the instance. This must be
369    * called before {@link #acquireSession(Looper, MediaSourceEventDispatcher, DrmInitData)} or
370    * {@link #acquirePlaceholderSession} is called.
371    *
372    * <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
373    * required.
374    *
375    * <p>{@code mode} must be one of these:
376    *
377    * <ul>
378    *   <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
379    *       requested otherwise the offline license is restored.
380    *   <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
381    *       is restored.
382    *   <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
383    *       requested otherwise the offline license is renewed.
384    *   <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline
385    *       license is released.
386    * </ul>
387    *
388    * @param mode The mode to be set.
389    * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
390    */
setMode(@ode int mode, @Nullable byte[] offlineLicenseKeySetId)391   public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) {
392     Assertions.checkState(sessions.isEmpty());
393     if (mode == MODE_QUERY || mode == MODE_RELEASE) {
394       Assertions.checkNotNull(offlineLicenseKeySetId);
395     }
396     this.mode = mode;
397     this.offlineLicenseKeySetId = offlineLicenseKeySetId;
398   }
399 
400   // DrmSessionManager implementation.
401 
402   @Override
prepare()403   public final void prepare() {
404     if (prepareCallsCount++ == 0) {
405       Assertions.checkState(exoMediaDrm == null);
406       exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
407       exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
408     }
409   }
410 
411   @Override
release()412   public final void release() {
413     if (--prepareCallsCount == 0) {
414       Assertions.checkNotNull(exoMediaDrm).release();
415       exoMediaDrm = null;
416     }
417   }
418 
419   @Override
canAcquireSession(DrmInitData drmInitData)420   public boolean canAcquireSession(DrmInitData drmInitData) {
421     if (offlineLicenseKeySetId != null) {
422       // An offline license can be restored so a session can always be acquired.
423       return true;
424     }
425     List<SchemeData> schemeDatas = getSchemeDatas(drmInitData, uuid, true);
426     if (schemeDatas.isEmpty()) {
427       if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) {
428         // Assume scheme specific data will be added before the session is opened.
429         Log.w(
430             TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid);
431       } else {
432         // No data for this manager's scheme.
433         return false;
434       }
435     }
436     String schemeType = drmInitData.schemeType;
437     if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {
438       // If there is no scheme information, assume patternless AES-CTR.
439       return true;
440     } else if (C.CENC_TYPE_cbc1.equals(schemeType)
441         || C.CENC_TYPE_cbcs.equals(schemeType)
442         || C.CENC_TYPE_cens.equals(schemeType)) {
443       // API support for AES-CBC and pattern encryption was added in API 24. However, the
444       // implementation was not stable until API 25.
445       return Util.SDK_INT >= 25;
446     }
447     // Unknown schemes, assume one of them is supported.
448     return true;
449   }
450 
451   @Override
452   @Nullable
acquirePlaceholderSession(Looper playbackLooper, int trackType)453   public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) {
454     assertExpectedPlaybackLooper(playbackLooper);
455     ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
456     boolean avoidPlaceholderDrmSessions =
457         FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
458             && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
459     // Avoid attaching a session to sparse formats.
460     if (avoidPlaceholderDrmSessions
461         || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET
462         || exoMediaDrm.getExoMediaCryptoType() == null) {
463       return null;
464     }
465     maybeCreateMediaDrmHandler(playbackLooper);
466     if (placeholderDrmSession == null) {
467       DefaultDrmSession placeholderDrmSession =
468           createNewDefaultSession(
469               /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true);
470       sessions.add(placeholderDrmSession);
471       this.placeholderDrmSession = placeholderDrmSession;
472     }
473     placeholderDrmSession.acquire(/* eventDispatcher= */ null);
474     return placeholderDrmSession;
475   }
476 
477   @Override
acquireSession( Looper playbackLooper, @Nullable MediaSourceEventDispatcher eventDispatcher, DrmInitData drmInitData)478   public DrmSession acquireSession(
479       Looper playbackLooper,
480       @Nullable MediaSourceEventDispatcher eventDispatcher,
481       DrmInitData drmInitData) {
482     assertExpectedPlaybackLooper(playbackLooper);
483     maybeCreateMediaDrmHandler(playbackLooper);
484 
485     @Nullable List<SchemeData> schemeDatas = null;
486     if (offlineLicenseKeySetId == null) {
487       schemeDatas = getSchemeDatas(drmInitData, uuid, false);
488       if (schemeDatas.isEmpty()) {
489         final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
490         if (eventDispatcher != null) {
491           eventDispatcher.dispatch(
492               (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionManagerError(error),
493               DrmSessionEventListener.class);
494         }
495         return new ErrorStateDrmSession(new DrmSessionException(error));
496       }
497     }
498 
499     @Nullable DefaultDrmSession session;
500     if (!multiSession) {
501       session = noMultiSessionDrmSession;
502     } else {
503       // Only use an existing session if it has matching init data.
504       session = null;
505       for (DefaultDrmSession existingSession : sessions) {
506         if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) {
507           session = existingSession;
508           break;
509         }
510       }
511     }
512 
513     if (session == null) {
514       // Create a new session.
515       session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false);
516       if (!multiSession) {
517         noMultiSessionDrmSession = session;
518       }
519       sessions.add(session);
520     }
521     session.acquire(eventDispatcher);
522     return session;
523   }
524 
525   @Override
526   @Nullable
getExoMediaCryptoType(DrmInitData drmInitData)527   public Class<? extends ExoMediaCrypto> getExoMediaCryptoType(DrmInitData drmInitData) {
528     return canAcquireSession(drmInitData)
529         ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType()
530         : null;
531   }
532 
533   // Internal methods.
534 
assertExpectedPlaybackLooper(Looper playbackLooper)535   private void assertExpectedPlaybackLooper(Looper playbackLooper) {
536     Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
537     this.playbackLooper = playbackLooper;
538   }
539 
maybeCreateMediaDrmHandler(Looper playbackLooper)540   private void maybeCreateMediaDrmHandler(Looper playbackLooper) {
541     if (mediaDrmHandler == null) {
542       mediaDrmHandler = new MediaDrmHandler(playbackLooper);
543     }
544   }
545 
createNewDefaultSession( @ullable List<SchemeData> schemeDatas, boolean isPlaceholderSession)546   private DefaultDrmSession createNewDefaultSession(
547       @Nullable List<SchemeData> schemeDatas, boolean isPlaceholderSession) {
548     Assertions.checkNotNull(exoMediaDrm);
549     // Placeholder sessions should always play clear samples without keys.
550     boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession;
551     return new DefaultDrmSession(
552         uuid,
553         exoMediaDrm,
554         /* provisioningManager= */ provisioningManagerImpl,
555         /* releaseCallback= */ this::onSessionReleased,
556         schemeDatas,
557         mode,
558         playClearSamplesWithoutKeys,
559         isPlaceholderSession,
560         offlineLicenseKeySetId,
561         keyRequestParameters,
562         callback,
563         Assertions.checkNotNull(playbackLooper),
564         loadErrorHandlingPolicy);
565   }
566 
onSessionReleased(DefaultDrmSession drmSession)567   private void onSessionReleased(DefaultDrmSession drmSession) {
568     sessions.remove(drmSession);
569     if (placeholderDrmSession == drmSession) {
570       placeholderDrmSession = null;
571     }
572     if (noMultiSessionDrmSession == drmSession) {
573       noMultiSessionDrmSession = null;
574     }
575     if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) {
576       // Other sessions were waiting for the released session to complete a provision operation.
577       // We need to have one of those sessions perform the provision operation instead.
578       provisioningSessions.get(1).provision();
579     }
580     provisioningSessions.remove(drmSession);
581   }
582 
583   /**
584    * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}.
585    *
586    * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}.
587    * @param uuid The UUID.
588    * @param allowMissingData Whether a {@link SchemeData} with null {@link SchemeData#data} may be
589    *     returned.
590    * @return The extracted {@link SchemeData} instances, or an empty list if no suitable data is
591    *     present.
592    */
getSchemeDatas( DrmInitData drmInitData, UUID uuid, boolean allowMissingData)593   private static List<SchemeData> getSchemeDatas(
594       DrmInitData drmInitData, UUID uuid, boolean allowMissingData) {
595     // Look for matching scheme data (matching the Common PSSH box for ClearKey).
596     List<SchemeData> matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount);
597     for (int i = 0; i < drmInitData.schemeDataCount; i++) {
598       SchemeData schemeData = drmInitData.get(i);
599       boolean uuidMatches =
600           schemeData.matches(uuid)
601               || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID));
602       if (uuidMatches && (schemeData.data != null || allowMissingData)) {
603         matchingSchemeDatas.add(schemeData);
604       }
605     }
606     return matchingSchemeDatas;
607   }
608 
609   @SuppressLint("HandlerLeak")
610   private class MediaDrmHandler extends Handler {
611 
MediaDrmHandler(Looper looper)612     public MediaDrmHandler(Looper looper) {
613       super(looper);
614     }
615 
616     @Override
handleMessage(Message msg)617     public void handleMessage(Message msg) {
618       byte[] sessionId = (byte[]) msg.obj;
619       if (sessionId == null) {
620         // The event is not associated with any particular session.
621         return;
622       }
623       for (DefaultDrmSession session : sessions) {
624         if (session.hasSessionId(sessionId)) {
625           session.onMediaDrmEvent(msg.what);
626           return;
627         }
628       }
629     }
630   }
631 
632   private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager {
633     @Override
provisionRequired(DefaultDrmSession session)634     public void provisionRequired(DefaultDrmSession session) {
635       if (provisioningSessions.contains(session)) {
636         // The session has already requested provisioning.
637         return;
638       }
639       provisioningSessions.add(session);
640       if (provisioningSessions.size() == 1) {
641         // This is the first session requesting provisioning, so have it perform the operation.
642         session.provision();
643       }
644     }
645 
646     @Override
onProvisionCompleted()647     public void onProvisionCompleted() {
648       for (DefaultDrmSession session : provisioningSessions) {
649         session.onProvisionCompleted();
650       }
651       provisioningSessions.clear();
652     }
653 
654     @Override
onProvisionError(Exception error)655     public void onProvisionError(Exception error) {
656       for (DefaultDrmSession session : provisioningSessions) {
657         session.onProvisionError(error);
658       }
659       provisioningSessions.clear();
660     }
661   }
662 
663   private class MediaDrmEventListener implements OnEventListener {
664 
665     @Override
onEvent( ExoMediaDrm md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data)666     public void onEvent(
667         ExoMediaDrm md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) {
668       Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
669     }
670   }
671 }
672