/* 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 java.util.ArrayList; import java.util.UUID; import org.mozilla.gecko.mozglue.JNIObject; import org.mozilla.gecko.annotation.WrapForJNI; import org.mozilla.gecko.AppConstants; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaCrypto; import android.media.MediaDrm; import android.util.Log; import android.os.Build; 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"; // A flag to avoid using the native object that has been destroyed. private boolean mDestroyed; private GeckoMediaDrm mImpl; public static ArrayList mProxyList = new ArrayList(); private static boolean isSystemSupported() { // Support versions >= LOLLIPOP if (AppConstants.Versions.preLollipop) { if (DEBUG) Log.d(LOGTAG, "System Not supported !!, current SDK version is " + Build.VERSION.SDK_INT); return false; } return true; } @WrapForJNI public static boolean isSchemeSupported(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; } @WrapForJNI public static boolean IsCryptoSchemeSupported(String keySystem, 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; } @WrapForJNI public static boolean CanDecode(String mimeType) { for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) { MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); if (info.isEncoder()) { continue; } for (String m : info.getSupportedTypes()) { if (m.equals(mimeType)) { return true; } } } if (DEBUG) Log.d(LOGTAG, "cannot decode mimetype = " + mimeType); 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(MediaDrmProxy proxy, Callbacks callbacks) { mNativeCallbacks = callbacks; mProxy = proxy; } @Override public void onSessionCreated(int createSessionToken, int promiseId, byte[] sessionId, byte[] request) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request); } } @Override public void onSessionUpdated(int promiseId, byte[] sessionId) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionUpdated(promiseId, sessionId); } } @Override public void onSessionClosed(int promiseId, byte[] sessionId) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionClosed(promiseId, sessionId); } } @Override public void onSessionMessage(byte[] sessionId, int sessionMessageType, byte[] request) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionMessage(sessionId, sessionMessageType, request); } } @Override public void onSessionError(byte[] sessionId, String message) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionError(sessionId, message); } } @Override public void onSessionBatchedKeyChanged(byte[] sessionId, SessionKeyInfo[] keyInfos) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos); } } @Override public void onRejectPromise(int promiseId, String message) { if (!mProxy.isDestroyed()) { mNativeCallbacks.onRejectPromise(promiseId, message); } } } // MediaDrmProxyCallbacks public boolean isDestroyed() { return mDestroyed; } @WrapForJNI(calledFrom = "gecko") public static MediaDrmProxy create(String keySystem, Callbacks nativeCallbacks, boolean isRemote) { // TODO: Will implement {Local,Remote}MediaDrmBridge instantiation by // '''isRemote''' flag in Bug 1307818. MediaDrmProxy proxy = new MediaDrmProxy(keySystem, nativeCallbacks); return proxy; } MediaDrmProxy(String keySystem, Callbacks nativeCallbacks) { if (DEBUG) Log.d(LOGTAG, "Constructing MediaDrmProxy"); // TODO: Bug 1306185 will implement the LocalMediaDrmBridge as an impl // of GeckoMediaDrm for in-process decoding mode. //mImpl = new LocalMediaDrmBridge(keySystem); mImpl.setCallbacks(new MediaDrmProxyCallbacks(this, nativeCallbacks)); mProxyList.add(this); } @WrapForJNI private void createSession(int createSessionToken, int promiseId, String initDataType, byte[] initData) { if (DEBUG) Log.d(LOGTAG, "createSession, promiseId = " + promiseId); mImpl.createSession(createSessionToken, promiseId, initDataType, initData); } @WrapForJNI private void updateSession(int promiseId, String sessionId, byte[] response) { if (DEBUG) Log.d(LOGTAG, "updateSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")"); mImpl.updateSession(promiseId, sessionId, response); } @WrapForJNI private void closeSession(int promiseId, String sessionId) { if (DEBUG) Log.d(LOGTAG, "closeSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")"); mImpl.closeSession(promiseId, sessionId); } @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"); mProxyList.remove(this); mImpl.release(); } }