/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.media; import android.annotation.SuppressLint; import android.media.MediaCrypto; import android.media.MediaDrm; import android.os.Build; import android.util.Log; import java.util.ArrayList; import java.util.UUID; import org.mozilla.gecko.annotation.WrapForJNI; import org.mozilla.gecko.mozglue.JNIObject; public final class MediaDrmProxy { private static final String LOGTAG = "GeckoMediaDrmProxy"; private static final boolean DEBUG = false; private static final UUID WIDEVINE_SCHEME_UUID = new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL); private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha"; @WrapForJNI private static final String AAC = "audio/mp4a-latm"; @WrapForJNI private static final String AVC = "video/avc"; @WrapForJNI private static final String VORBIS = "audio/vorbis"; @WrapForJNI private static final String VP8 = "video/x-vnd.on2.vp8"; @WrapForJNI private static final String VP9 = "video/x-vnd.on2.vp9"; @WrapForJNI private static final String OPUS = "audio/opus"; @WrapForJNI private static final String FLAC = "audio/flac"; public static final ArrayList sProxyList = new ArrayList(); // A flag to avoid using the native object that has been destroyed. private boolean mDestroyed; private GeckoMediaDrm mImpl; private String mDrmStubId; private static boolean isSystemSupported() { // Support versions >= Marshmallow if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (DEBUG) Log.d(LOGTAG, "System Not supported !!, current SDK version is " + Build.VERSION.SDK_INT); return false; } return true; } @SuppressLint("NewApi") @WrapForJNI public static boolean isSchemeSupported(final String keySystem) { if (!isSystemSupported()) { return false; } if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) { return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID) && MediaCrypto.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID); } if (DEBUG) Log.d(LOGTAG, "isSchemeSupported key sytem = " + keySystem); return false; } @SuppressLint("NewApi") @WrapForJNI public static boolean IsCryptoSchemeSupported(final String keySystem, final String container) { if (!isSystemSupported()) { return false; } if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) { return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID, container); } if (DEBUG) Log.d(LOGTAG, "cannot decrypt key sytem = " + keySystem + ", container = " + container); return false; } // Interface for callback to native. public interface Callbacks { void onSessionCreated(int createSessionToken, int promiseId, byte[] sessionId, byte[] request); void onSessionUpdated(int promiseId, byte[] sessionId); void onSessionClosed(int promiseId, byte[] sessionId); void onSessionMessage(byte[] sessionId, int sessionMessageType, byte[] request); void onSessionError(byte[] sessionId, String message); // MediaDrm.KeyStatus is available in API level 23(M) // https://developer.android.com/reference/android/media/MediaDrm.KeyStatus.html // For compatibility between L and M above, we'll unwrap the KeyStatus structure // and store the keyid and status into SessionKeyInfo and pass to native(MediaDrmCDMProxy). void onSessionBatchedKeyChanged(byte[] sessionId, SessionKeyInfo[] keyInfos); void onRejectPromise(int promiseId, String message); } // Callbacks public static class NativeMediaDrmProxyCallbacks extends JNIObject implements Callbacks { @WrapForJNI(calledFrom = "gecko") NativeMediaDrmProxyCallbacks() {} @Override @WrapForJNI(dispatchTo = "gecko") public native void onSessionCreated( int createSessionToken, int promiseId, byte[] sessionId, byte[] request); @Override @WrapForJNI(dispatchTo = "gecko") public native void onSessionUpdated(int promiseId, byte[] sessionId); @Override @WrapForJNI(dispatchTo = "gecko") public native void onSessionClosed(int promiseId, byte[] sessionId); @Override @WrapForJNI(dispatchTo = "gecko") public native void onSessionMessage(byte[] sessionId, int sessionMessageType, byte[] request); @Override @WrapForJNI(dispatchTo = "gecko") public native void onSessionError(byte[] sessionId, String message); @Override @WrapForJNI(dispatchTo = "gecko") public native void onSessionBatchedKeyChanged(byte[] sessionId, SessionKeyInfo[] keyInfos); @Override @WrapForJNI(dispatchTo = "gecko") public native void onRejectPromise(int promiseId, String message); @Override // JNIObject protected void disposeNative() { throw new UnsupportedOperationException(); } } // NativeMediaDrmProxyCallbacks // A proxy to callback from LocalMediaDrmBridge to native instance. public static class MediaDrmProxyCallbacks implements GeckoMediaDrm.Callbacks { private final Callbacks mNativeCallbacks; private final MediaDrmProxy mProxy; public MediaDrmProxyCallbacks(final MediaDrmProxy proxy, final Callbacks callbacks) { mNativeCallbacks = callbacks; mProxy = proxy; } @Override public void onSessionCreated( final int createSessionToken, final int promiseId, final byte[] sessionId, final byte[] request) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request); } } @Override public void onSessionUpdated(final int promiseId, final byte[] sessionId) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionUpdated(promiseId, sessionId); } } @Override public void onSessionClosed(final int promiseId, final byte[] sessionId) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionClosed(promiseId, sessionId); } } @Override public void onSessionMessage( final byte[] sessionId, final int sessionMessageType, final byte[] request) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionMessage(sessionId, sessionMessageType, request); } } @Override public void onSessionError(final byte[] sessionId, final String message) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionError(sessionId, message); } } @Override public void onSessionBatchedKeyChanged( final byte[] sessionId, final SessionKeyInfo[] keyInfos) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos); } } @Override public void onRejectPromise(final int promiseId, final String message) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onRejectPromise(promiseId, message); } } } // MediaDrmProxyCallbacks public boolean isDestroyed() { return mDestroyed; } @WrapForJNI(calledFrom = "gecko") public static MediaDrmProxy create(final String keySystem, final Callbacks nativeCallbacks) { final MediaDrmProxy proxy = new MediaDrmProxy(keySystem, nativeCallbacks); return proxy; } MediaDrmProxy(final String keySystem, final Callbacks nativeCallbacks) { if (DEBUG) Log.d(LOGTAG, "Constructing MediaDrmProxy"); try { mDrmStubId = UUID.randomUUID().toString(); final IMediaDrmBridge remoteBridge = RemoteManager.getInstance().createRemoteMediaDrmBridge(keySystem, mDrmStubId); mImpl = new RemoteMediaDrmBridge(remoteBridge); mImpl.setCallbacks(new MediaDrmProxyCallbacks(this, nativeCallbacks)); sProxyList.add(this); } catch (final Exception e) { Log.e(LOGTAG, "Constructing MediaDrmProxy ... error", e); } } @WrapForJNI private void createSession( final int createSessionToken, final int promiseId, final String initDataType, final byte[] initData) { if (DEBUG) Log.d(LOGTAG, "createSession, promiseId = " + promiseId); mImpl.createSession(createSessionToken, promiseId, initDataType, initData); } @WrapForJNI private void updateSession(final int promiseId, final String sessionId, final byte[] response) { if (DEBUG) Log.d(LOGTAG, "updateSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")"); mImpl.updateSession(promiseId, sessionId, response); } @WrapForJNI private void closeSession(final int promiseId, final String sessionId) { if (DEBUG) Log.d(LOGTAG, "closeSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")"); mImpl.closeSession(promiseId, sessionId); } @WrapForJNI(calledFrom = "gecko") private String getStubId() { return mDrmStubId; } @WrapForJNI public boolean setServerCertificate(final byte[] cert) { try { mImpl.setServerCertificate(cert); return true; } catch (final RuntimeException e) { return false; } } // Get corresponding MediaCrypto object by a generated UUID for MediaCodec. // Will be called on MediaFormatReader's TaskQueue. @WrapForJNI public static MediaCrypto getMediaCrypto(final String stubId) { for (final MediaDrmProxy proxy : sProxyList) { if (proxy.getStubId().equals(stubId)) { return proxy.getMediaCryptoFromBridge(); } } if (DEBUG) Log.d(LOGTAG, " NULL crytpo "); return null; } @WrapForJNI // Called when natvie object is destroyed. private void destroy() { if (DEBUG) Log.d(LOGTAG, "destroy!! Native object is destroyed."); if (mDestroyed) { return; } mDestroyed = true; release(); } private void release() { if (DEBUG) Log.d(LOGTAG, "release"); sProxyList.remove(this); mImpl.release(); } private MediaCrypto getMediaCryptoFromBridge() { return mImpl != null ? mImpl.getMediaCrypto() : null; } }