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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; 17 18 import android.annotation.SuppressLint; 19 import android.annotation.TargetApi; 20 import android.os.Handler; 21 import android.os.Looper; 22 import android.os.Message; 23 import androidx.annotation.IntDef; 24 import androidx.annotation.Nullable; 25 import org.mozilla.thirdparty.com.google.android.exoplayer2.C; 26 import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; 27 import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; 28 import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; 29 import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; 30 import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; 31 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; 32 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EventDispatcher; 33 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; 34 import org.mozilla.thirdparty.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 @TargetApi(18) 47 public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> { 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<ExoMediaCrypto> 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 */ 79 @SuppressWarnings("unchecked") Builder()80 public Builder() { 81 keyRequestParameters = new HashMap<>(); 82 uuid = C.WIDEVINE_UUID; 83 exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER; 84 loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); 85 useDrmSessionsForClearContentTrackTypes = new int[0]; 86 } 87 88 /** 89 * Sets the key request parameters to pass as the last argument to {@link 90 * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. 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(Map<String, String> keyRequestParameters)97 public Builder setKeyRequestParameters(Map<String, String> keyRequestParameters) { 98 this.keyRequestParameters.clear(); 99 this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters)); 100 return this; 101 } 102 103 /** 104 * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use. 105 * 106 * @param uuid The UUID of the DRM scheme. 107 * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}. 108 * @return This builder. 109 */ 110 @SuppressWarnings({"rawtypes", "unchecked"}) setUuidAndExoMediaDrmProvider( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider)111 public Builder setUuidAndExoMediaDrmProvider( 112 UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) { 113 this.uuid = Assertions.checkNotNull(uuid); 114 this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider); 115 return this; 116 } 117 118 /** 119 * Sets whether this session manager is allowed to acquire multiple simultaneous sessions. 120 * 121 * <p>Users should pass false when a single key request will obtain all keys required to decrypt 122 * the associated content. {@code multiSession} is required when content uses key rotation. 123 * 124 * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous 125 * sessions. 126 * @return This builder. 127 */ setMultiSession(boolean multiSession)128 public Builder setMultiSession(boolean multiSession) { 129 this.multiSession = multiSession; 130 return this; 131 } 132 133 /** 134 * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear 135 * sections of the media content. 136 * 137 * <p>Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders 138 * when transitioning between clear and encrypted sections of content. 139 * 140 * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO} 141 * and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of 142 * whether the content is clear or encrypted. 143 * @return This builder. 144 * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains 145 * track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}. 146 */ setUseDrmSessionsForClearContent( int... useDrmSessionsForClearContentTrackTypes)147 public Builder setUseDrmSessionsForClearContent( 148 int... useDrmSessionsForClearContentTrackTypes) { 149 for (int trackType : useDrmSessionsForClearContentTrackTypes) { 150 Assertions.checkArgument( 151 trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO); 152 } 153 this.useDrmSessionsForClearContentTrackTypes = 154 useDrmSessionsForClearContentTrackTypes.clone(); 155 return this; 156 } 157 158 /** 159 * Sets whether clear samples within protected content should be played when keys for the 160 * encrypted part of the content have yet to be loaded. 161 * 162 * @param playClearSamplesWithoutKeys Whether clear samples within protected content should be 163 * played when keys for the encrypted part of the content have yet to be loaded. 164 * @return This builder. 165 */ setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys)166 public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) { 167 this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; 168 return this; 169 } 170 171 /** 172 * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests. 173 * 174 * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. 175 * @return This builder. 176 */ setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy)177 public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { 178 this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy); 179 return this; 180 } 181 182 /** Builds a {@link DefaultDrmSessionManager} instance. */ build(MediaDrmCallback mediaDrmCallback)183 public DefaultDrmSessionManager<ExoMediaCrypto> build(MediaDrmCallback mediaDrmCallback) { 184 return new DefaultDrmSessionManager<>( 185 uuid, 186 exoMediaDrmProvider, 187 mediaDrmCallback, 188 keyRequestParameters, 189 multiSession, 190 useDrmSessionsForClearContentTrackTypes, 191 playClearSamplesWithoutKeys, 192 loadErrorHandlingPolicy); 193 } 194 } 195 196 /** 197 * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does 198 * not contain scheme data for the required UUID. 199 */ 200 public static final class MissingSchemeDataException extends Exception { 201 MissingSchemeDataException(UUID uuid)202 private MissingSchemeDataException(UUID uuid) { 203 super("Media does not support uuid: " + uuid); 204 } 205 } 206 207 /** 208 * A key for specifying PlayReady custom data in the key request parameters passed to {@link 209 * Builder#setKeyRequestParameters(Map)}. 210 */ 211 public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; 212 213 /** 214 * Determines the action to be done after a session acquired. One of {@link #MODE_PLAYBACK}, 215 * {@link #MODE_QUERY}, {@link #MODE_DOWNLOAD} or {@link #MODE_RELEASE}. 216 */ 217 @Documented 218 @Retention(RetentionPolicy.SOURCE) 219 @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE}) 220 public @interface Mode {} 221 /** 222 * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline 223 * licenses. 224 */ 225 public static final int MODE_PLAYBACK = 0; 226 /** Restores an offline license to allow its status to be queried. */ 227 public static final int MODE_QUERY = 1; 228 /** Downloads an offline license or renews an existing one. */ 229 public static final int MODE_DOWNLOAD = 2; 230 /** Releases an existing offline license. */ 231 public static final int MODE_RELEASE = 3; 232 /** Number of times to retry for initial provisioning and key request for reporting error. */ 233 public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; 234 235 private static final String TAG = "DefaultDrmSessionMgr"; 236 237 private final UUID uuid; 238 private final ExoMediaDrm.Provider<T> exoMediaDrmProvider; 239 private final MediaDrmCallback callback; 240 private final HashMap<String, String> keyRequestParameters; 241 private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher; 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<T>> sessions; 249 private final List<DefaultDrmSession<T>> provisioningSessions; 250 251 private int prepareCallsCount; 252 @Nullable private ExoMediaDrm<T> exoMediaDrm; 253 @Nullable private DefaultDrmSession<T> placeholderDrmSession; 254 @Nullable private DefaultDrmSession<T> 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<T> exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters)271 public DefaultDrmSessionManager( 272 UUID uuid, 273 ExoMediaDrm<T> 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<T> exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters, boolean multiSession)296 public DefaultDrmSessionManager( 297 UUID uuid, 298 ExoMediaDrm<T> 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<T> exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters, boolean multiSession, int initialDrmRequestRetryCount)324 public DefaultDrmSessionManager( 325 UUID uuid, 326 ExoMediaDrm<T> 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 342 // the constructor does not initialize fields: offlineLicenseKeySetId 343 @SuppressWarnings("nullness:initialization.fields.uninitialized") DefaultDrmSessionManager( UUID uuid, ExoMediaDrm.Provider<T> exoMediaDrmProvider, MediaDrmCallback callback, HashMap<String, String> keyRequestParameters, boolean multiSession, int[] useDrmSessionsForClearContentTrackTypes, boolean playClearSamplesWithoutKeys, LoadErrorHandlingPolicy loadErrorHandlingPolicy)344 private DefaultDrmSessionManager( 345 UUID uuid, 346 ExoMediaDrm.Provider<T> exoMediaDrmProvider, 347 MediaDrmCallback callback, 348 HashMap<String, String> keyRequestParameters, 349 boolean multiSession, 350 int[] useDrmSessionsForClearContentTrackTypes, 351 boolean playClearSamplesWithoutKeys, 352 LoadErrorHandlingPolicy loadErrorHandlingPolicy) { 353 Assertions.checkNotNull(uuid); 354 Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); 355 this.uuid = uuid; 356 this.exoMediaDrmProvider = exoMediaDrmProvider; 357 this.callback = callback; 358 this.keyRequestParameters = keyRequestParameters; 359 this.eventDispatcher = new EventDispatcher<>(); 360 this.multiSession = multiSession; 361 this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes; 362 this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; 363 this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; 364 provisioningManagerImpl = new ProvisioningManagerImpl(); 365 mode = MODE_PLAYBACK; 366 sessions = new ArrayList<>(); 367 provisioningSessions = new ArrayList<>(); 368 } 369 370 /** 371 * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events. 372 * 373 * @param handler A handler to use when delivering events to {@code eventListener}. 374 * @param eventListener A listener of events. 375 */ addListener(Handler handler, DefaultDrmSessionEventListener eventListener)376 public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) { 377 eventDispatcher.addListener(handler, eventListener); 378 } 379 380 /** 381 * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners. 382 * 383 * @param eventListener The listener to remove. 384 */ removeListener(DefaultDrmSessionEventListener eventListener)385 public final void removeListener(DefaultDrmSessionEventListener eventListener) { 386 eventDispatcher.removeListener(eventListener); 387 } 388 389 /** 390 * Sets the mode, which determines the role of sessions acquired from the instance. This must be 391 * called before {@link #acquireSession(Looper, DrmInitData)} or {@link 392 * #acquirePlaceholderSession} is called. 393 * 394 * <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when 395 * required. 396 * 397 * <p>{@code mode} must be one of these: 398 * 399 * <ul> 400 * <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is 401 * requested otherwise the offline license is restored. 402 * <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license 403 * is restored. 404 * <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is 405 * requested otherwise the offline license is renewed. 406 * <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline 407 * license is released. 408 * </ul> 409 * 410 * @param mode The mode to be set. 411 * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. 412 */ setMode(@ode int mode, @Nullable byte[] offlineLicenseKeySetId)413 public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) { 414 Assertions.checkState(sessions.isEmpty()); 415 if (mode == MODE_QUERY || mode == MODE_RELEASE) { 416 Assertions.checkNotNull(offlineLicenseKeySetId); 417 } 418 this.mode = mode; 419 this.offlineLicenseKeySetId = offlineLicenseKeySetId; 420 } 421 422 // DrmSessionManager implementation. 423 424 @Override prepare()425 public final void prepare() { 426 if (prepareCallsCount++ == 0) { 427 Assertions.checkState(exoMediaDrm == null); 428 exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); 429 exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); 430 } 431 } 432 433 @Override release()434 public final void release() { 435 if (--prepareCallsCount == 0) { 436 Assertions.checkNotNull(exoMediaDrm).release(); 437 exoMediaDrm = null; 438 } 439 } 440 441 @Override canAcquireSession(DrmInitData drmInitData)442 public boolean canAcquireSession(DrmInitData drmInitData) { 443 if (offlineLicenseKeySetId != null) { 444 // An offline license can be restored so a session can always be acquired. 445 return true; 446 } 447 List<SchemeData> schemeDatas = getSchemeDatas(drmInitData, uuid, true); 448 if (schemeDatas.isEmpty()) { 449 if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) { 450 // Assume scheme specific data will be added before the session is opened. 451 Log.w( 452 TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid); 453 } else { 454 // No data for this manager's scheme. 455 return false; 456 } 457 } 458 String schemeType = drmInitData.schemeType; 459 if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { 460 // If there is no scheme information, assume patternless AES-CTR. 461 return true; 462 } else if (C.CENC_TYPE_cbc1.equals(schemeType) 463 || C.CENC_TYPE_cbcs.equals(schemeType) 464 || C.CENC_TYPE_cens.equals(schemeType)) { 465 // API support for AES-CBC and pattern encryption was added in API 24. However, the 466 // implementation was not stable until API 25. 467 return Util.SDK_INT >= 25; 468 } 469 // Unknown schemes, assume one of them is supported. 470 return true; 471 } 472 473 @Override 474 @Nullable acquirePlaceholderSession(Looper playbackLooper, int trackType)475 public DrmSession<T> acquirePlaceholderSession(Looper playbackLooper, int trackType) { 476 assertExpectedPlaybackLooper(playbackLooper); 477 ExoMediaDrm<T> exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); 478 boolean avoidPlaceholderDrmSessions = 479 FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) 480 && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; 481 // Avoid attaching a session to sparse formats. 482 if (avoidPlaceholderDrmSessions 483 || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET 484 || exoMediaDrm.getExoMediaCryptoType() == null) { 485 return null; 486 } 487 maybeCreateMediaDrmHandler(playbackLooper); 488 if (placeholderDrmSession == null) { 489 DefaultDrmSession<T> placeholderDrmSession = 490 createNewDefaultSession( 491 /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true); 492 sessions.add(placeholderDrmSession); 493 this.placeholderDrmSession = placeholderDrmSession; 494 } 495 placeholderDrmSession.acquire(); 496 return placeholderDrmSession; 497 } 498 499 @Override acquireSession(Looper playbackLooper, DrmInitData drmInitData)500 public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) { 501 assertExpectedPlaybackLooper(playbackLooper); 502 maybeCreateMediaDrmHandler(playbackLooper); 503 504 @Nullable List<SchemeData> schemeDatas = null; 505 if (offlineLicenseKeySetId == null) { 506 schemeDatas = getSchemeDatas(drmInitData, uuid, false); 507 if (schemeDatas.isEmpty()) { 508 final MissingSchemeDataException error = new MissingSchemeDataException(uuid); 509 eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error)); 510 return new ErrorStateDrmSession<>(new DrmSessionException(error)); 511 } 512 } 513 514 @Nullable DefaultDrmSession<T> session; 515 if (!multiSession) { 516 session = noMultiSessionDrmSession; 517 } else { 518 // Only use an existing session if it has matching init data. 519 session = null; 520 for (DefaultDrmSession<T> existingSession : sessions) { 521 if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) { 522 session = existingSession; 523 break; 524 } 525 } 526 } 527 528 if (session == null) { 529 // Create a new session. 530 session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false); 531 if (!multiSession) { 532 noMultiSessionDrmSession = session; 533 } 534 sessions.add(session); 535 } 536 session.acquire(); 537 return session; 538 } 539 540 @Override 541 @Nullable getExoMediaCryptoType(DrmInitData drmInitData)542 public Class<T> getExoMediaCryptoType(DrmInitData drmInitData) { 543 return canAcquireSession(drmInitData) 544 ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType() 545 : null; 546 } 547 548 // Internal methods. 549 assertExpectedPlaybackLooper(Looper playbackLooper)550 private void assertExpectedPlaybackLooper(Looper playbackLooper) { 551 Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); 552 this.playbackLooper = playbackLooper; 553 } 554 maybeCreateMediaDrmHandler(Looper playbackLooper)555 private void maybeCreateMediaDrmHandler(Looper playbackLooper) { 556 if (mediaDrmHandler == null) { 557 mediaDrmHandler = new MediaDrmHandler(playbackLooper); 558 } 559 } 560 createNewDefaultSession( @ullable List<SchemeData> schemeDatas, boolean isPlaceholderSession)561 private DefaultDrmSession<T> createNewDefaultSession( 562 @Nullable List<SchemeData> schemeDatas, boolean isPlaceholderSession) { 563 Assertions.checkNotNull(exoMediaDrm); 564 // Placeholder sessions should always play clear samples without keys. 565 boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; 566 return new DefaultDrmSession<>( 567 uuid, 568 exoMediaDrm, 569 /* provisioningManager= */ provisioningManagerImpl, 570 /* releaseCallback= */ this::onSessionReleased, 571 schemeDatas, 572 mode, 573 playClearSamplesWithoutKeys, 574 isPlaceholderSession, 575 offlineLicenseKeySetId, 576 keyRequestParameters, 577 callback, 578 Assertions.checkNotNull(playbackLooper), 579 eventDispatcher, 580 loadErrorHandlingPolicy); 581 } 582 onSessionReleased(DefaultDrmSession<T> drmSession)583 private void onSessionReleased(DefaultDrmSession<T> drmSession) { 584 sessions.remove(drmSession); 585 if (placeholderDrmSession == drmSession) { 586 placeholderDrmSession = null; 587 } 588 if (noMultiSessionDrmSession == drmSession) { 589 noMultiSessionDrmSession = null; 590 } 591 if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { 592 // Other sessions were waiting for the released session to complete a provision operation. 593 // We need to have one of those sessions perform the provision operation instead. 594 provisioningSessions.get(1).provision(); 595 } 596 provisioningSessions.remove(drmSession); 597 } 598 599 /** 600 * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}. 601 * 602 * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. 603 * @param uuid The UUID. 604 * @param allowMissingData Whether a {@link SchemeData} with null {@link SchemeData#data} may be 605 * returned. 606 * @return The extracted {@link SchemeData} instances, or an empty list if no suitable data is 607 * present. 608 */ getSchemeDatas( DrmInitData drmInitData, UUID uuid, boolean allowMissingData)609 private static List<SchemeData> getSchemeDatas( 610 DrmInitData drmInitData, UUID uuid, boolean allowMissingData) { 611 // Look for matching scheme data (matching the Common PSSH box for ClearKey). 612 List<SchemeData> matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount); 613 for (int i = 0; i < drmInitData.schemeDataCount; i++) { 614 SchemeData schemeData = drmInitData.get(i); 615 boolean uuidMatches = 616 schemeData.matches(uuid) 617 || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID)); 618 if (uuidMatches && (schemeData.data != null || allowMissingData)) { 619 matchingSchemeDatas.add(schemeData); 620 } 621 } 622 return matchingSchemeDatas; 623 } 624 625 @SuppressLint("HandlerLeak") 626 private class MediaDrmHandler extends Handler { 627 MediaDrmHandler(Looper looper)628 public MediaDrmHandler(Looper looper) { 629 super(looper); 630 } 631 632 @Override handleMessage(Message msg)633 public void handleMessage(Message msg) { 634 byte[] sessionId = (byte[]) msg.obj; 635 if (sessionId == null) { 636 // The event is not associated with any particular session. 637 return; 638 } 639 for (DefaultDrmSession<T> session : sessions) { 640 if (session.hasSessionId(sessionId)) { 641 session.onMediaDrmEvent(msg.what); 642 return; 643 } 644 } 645 } 646 } 647 648 private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager<T> { 649 @Override provisionRequired(DefaultDrmSession<T> session)650 public void provisionRequired(DefaultDrmSession<T> session) { 651 if (provisioningSessions.contains(session)) { 652 // The session has already requested provisioning. 653 return; 654 } 655 provisioningSessions.add(session); 656 if (provisioningSessions.size() == 1) { 657 // This is the first session requesting provisioning, so have it perform the operation. 658 session.provision(); 659 } 660 } 661 662 @Override onProvisionCompleted()663 public void onProvisionCompleted() { 664 for (DefaultDrmSession<T> session : provisioningSessions) { 665 session.onProvisionCompleted(); 666 } 667 provisioningSessions.clear(); 668 } 669 670 @Override onProvisionError(Exception error)671 public void onProvisionError(Exception error) { 672 for (DefaultDrmSession<T> session : provisioningSessions) { 673 session.onProvisionError(error); 674 } 675 provisioningSessions.clear(); 676 } 677 } 678 679 private class MediaDrmEventListener implements OnEventListener<T> { 680 681 @Override onEvent( ExoMediaDrm<? extends T> md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data)682 public void onEvent( 683 ExoMediaDrm<? extends T> md, 684 @Nullable byte[] sessionId, 685 int event, 686 int extra, 687 @Nullable byte[] data) { 688 Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); 689 } 690 } 691 } 692