Android MediaCas 자작 예제
Android 기기에서 자작 MediaCas plugin 템플릿을 작성해 보았다.
먼저 AOSP에 포함되어 있는 Clearkey MediaCas plugin을 분석 및 테스트해 보고, 이후 자작 MediaCas plugin을 구현해 본다.
Clearkey MediaCas plugin 분석
- AOSP 내 소스 경로: frameworks/av/drm/mediacas/plugins/clearkey/
- ClearKeySystemId = 0xF6D8
- TS 스트림은 AES-128 CBC로 encrypt 되고, Clearkey MediaCas plugin에 의해서 decrypt 된다.
- 아래와 같이 빌드할 수 있다.
$ m libclearkeycasplugin또는
$ cd frameworks/av/drm/mediadrm/plugins/clearkey/ $ mm결과로 libclearkeycasplugin.so 파일이 빌드된다. 또, 테스트 프로그램으로 ClearKeyFetcherTest 파일이 빌드된다.
- frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.cpp 파일에서 주요 내용은 다음과 같다.
static const int32_t sClearKeySystemId = 0xF6D8; bool ClearKeyCasFactory::isSystemIdSupported(int32_t CA_system_id) const { return CA_system_id == sClearKeySystemId; } status_t ClearKeyCasFactory::queryPlugins(std::vector<CasPluginDescriptor> *descriptors) const { descriptors->clear(); descriptors->push_back({sClearKeySystemId, String8("Clear Key CAS")}); return OK; } - ExoPlayer에서 CleareKey TS 스트림을 play 테스트하려면, ExoPlayer의 demos/main/src/main/assets/media.exolist.json 파일을 열어서 아래 예와 같이 추가한다.
{ "name": "ClearKey CAS (TS)", "uri": "http://{ClearKey TS 스트림 경로}" } - Player 동작 흐름
- MediaExtractor를 통해 스트림 내의 CA_descriptor 등을 탐지하여, CAS 채널이면 아래와 같이 MediaCas 객체를 생성하고 세션을 open 한다.
MediaCas mediaCas = new MediaCas(CA_SYSTEM_ID); MediaCas.Session session = mediaCas.openSession(); - ECM이 잡히면 ECM을 전달한다.
session.processEcm(ecmData); - MediaDescrambler를 생성하고 open 한 세션과 연결한다.
MediaDescrambler descrambler = new MediaDescrambler(CA_SYSTEM_ID); descrambler.setMediaCasSession(sessionId); mediaCodec.setDescrambler(descrambler); mediaCodec.configure(format, surface, descrambler, 0);이 시점부터 MediaCodec 입력 버퍼는 자동으로 descrambling 대상이 된다.
mediaCodec.queueSecureInputBuffer(...);
- MediaExtractor를 통해 스트림 내의 CA_descriptor 등을 탐지하여, CAS 채널이면 아래와 같이 MediaCas 객체를 생성하고 세션을 open 한다.
- Sub-sample descrambling
- 컴포넌트: MediaDescrambler
- 데이터 흐름은 다음과 같다.
MediaExtractor | | (Encrypted ES + SubSample info) v MediaCodec (frameworks/base/media/java/android/media/MediaCodec.java) (MediaCodec.CryptoInfo 클래스: frameworks/base/media/java/android/media/MediaCodec.java) | | queueSecureInputBuffer()로 SubSample array 전달 v MediaDescrambler (frameworks/base/media/java/android/media/MediaDescrambler.java) (descramble(ByteBuffer srcBuf, ByteBuffer dstBuf, MediaCodec.CryptoInfo cryptoInfo)) | | Binder call v IDescrambler::descramble() (descramble(ScramblingControl scramblingControl, SubSample[] subSamples, SharedBuffer srcBuffer, long srcOffset, DestinationBuffer dstBuffer, long dstOffset) | v MediaCas plugin descrambler (DescramblerPlugin 상속) (descramble(bool secure, ScramblingControl scramblingControl, size_t numSubSamples, const SubSample *subSamples, const void *srcPtr, int32_t srcOffset, void *dstPtr, int32_t dstOffset, AString *errorDetailMsg)) | | descramble() 각 sub-sample을 descrambling v Decoder (Clear ES)
자작 MediaCas 플러그인 제작
여기서는 예로 MYCAS 이름의 MediaCas plugin을 제작해 본다.
- Android.mk 파일을 아래와 같이 작성한다.
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_SRC_FILES := \ MyCasFactory.cpp \ MyCasPlugin.cpp LOCAL_MODULE := libmycasplugin LOCAL_MODULE_RELATIVE_PATH := mediacas LOCAL_MODULE_CLASS := SHARED_LIBRARIES LOCAL_MODULE_TAGS := optional LOCAL_PROPRIETARY_MODULE := true LOCAL_SHARED_LIBRARIES := \ libutils \ liblog \ libstagefright_foundation LOCAL_HEADER_LIBRARIES := \ media_plugin_headers include $(BUILD_SHARED_LIBRARY) - MyCasFactory.h 파일을 아래와 같이 작성한다.
#ifndef MYCAS_FACTORY_H #define MYCAS_FACTORY_H #include <media/cas/CasAPI.h> extern "C" android::CasFactory *createCasFactory(); namespace android { namespace mycas { class MyCasFactory : public CasFactory { public: MyCasFactory(); virtual ~MyCasFactory(); virtual bool isSystemIdSupported(int32_t CA_system_id) const override; virtual status_t queryPlugins(std::vector<CasPluginDescriptor> *descriptors) const override; virtual status_t createPlugin(int32_t CA_system_id, void *appData, CasPluginCallback callback, CasPlugin **plugin) override; virtual status_t createPlugin(int32_t CA_system_id, void *appData, CasPluginCallbackExt callbackExt, CasPlugin **plugin) override; }; }; }; #endif /* MYCAS_FACTORY_H */ - MyCasFactory.cpp 파일을 아래와 같이 작성한다.
#define LOG_TAG "MYCAS" #include <utils/Log.h> #include <new> #include <media/stagefright/MediaErrors.h> #include "MyCasFactory.h" #include "MyCasPlugin.h" #define MYCAS_CA_SYSTEM_ID 0x0005 #define MYCAS_PLUGIN_NAME "MYCAS" android::CasFactory* createCasFactory() { ALOGI("%s", __func__); android::mycas::MyCasFactory* mycasFactory = new(std::nothrow) android::mycas::MyCasFactory(); return mycasFactory; } namespace android { namespace mycas { MyCasFactory::MyCasFactory() { } MyCasFactory::~MyCasFactory() { } bool MyCasFactory::isSystemIdSupported(int32_t CA_system_id) const { if (CA_system_id == MYCAS_CA_SYSTEM_ID) { ALOGD("%s: CA_system_id 0x%04X is supported", __func__, CA_system_id); return true; } ALOGD("%s: CA_system_id 0x%04X is not supported", __func__, CA_system_id); return false; } status_t MyCasFactory::queryPlugins(std::vector<CasPluginDescriptor> *descriptors) const { ALOGI("%s", __func__); descriptors->clear(); descriptors->push_back({MYCAS_CA_SYSTEM_ID, String8(MYCAS_PLUGIN_NAME)}); return OK; } status_t MyCasFactory::createPlugin(int32_t CA_system_id, void *appData, CasPluginCallback callback, CasPlugin **plugin) { if (!isSystemIdSupported(CA_system_id)) { ALOGE("%s: Cannot create plug-in: Wrong CA_system_id 0x%04X", __func__, CA_system_id); return BAD_VALUE; } *plugin = new(std::nothrow) MyCasPlugin((void*)appData, callback); if (*plugin == nullptr) { ALOGE("%s: Failed to create MyCasPlugin", __func__); return ERROR_CAS_UNKNOWN; } ALOGI("%s: MyCasPlugin is created", __func__); return OK; } status_t MyCasFactory::createPlugin(int32_t CA_system_id, void *appData, CasPluginCallbackExt callbackExt, CasPlugin **plugin) { if (!isSystemIdSupported(CA_system_id)) { ALOGE("%s: Cannot create plug-in: Wrong CA_system_id 0x%04X", __func__, CA_system_id); return ERROR_UNSUPPORTED; } *plugin = new(std::nothrow) MyCasPlugin((void *)appData, callbackExt); if (*plugin == nullptr) { ALOGE("%s: Failed to create MyCasPlugin", __func__); return ERROR_CAS_UNKNOWN; } ALOGI("%s: MyCasPlugin is created", __func__); return OK; } } } - MyCasPlugin.h 파일을 아래와 같이 작성한다.
#ifndef MYCAS_PLUGIN_H #define MYCAS_PLUGIN_H #include <media/cas/CasAPI.h> #include <utils/Mutex.h> #include <utils/String8.h> namespace android { namespace mycas { class MyCasPlugin : public CasPlugin { public: MyCasPlugin(void *appData, CasPluginCallback callback); MyCasPlugin(void *appData, CasPluginCallbackExt callbackExt); virtual ~MyCasPlugin(); virtual status_t setStatusCallback(CasPluginStatusCallback callback) override; virtual status_t setPrivateData(const CasData &privateData) override; virtual status_t openSession(CasSessionId *sessionId) override; virtual status_t openSession(uint32_t sessionIntent, uint32_t scramblingMode, CasSessionId *sessionId) override; virtual status_t closeSession(const CasSessionId &sessionId) override; virtual status_t setSessionPrivateData(const CasSessionId &sessionId, const CasData &privateData) override; virtual status_t processEcm(const CasSessionId &sessionId, const CasEcm &ecm) override; virtual status_t processEmm(const CasEmm &emm) override; virtual status_t sendEvent(int32_t event, int32_t arg, const CasData &eventData) override; virtual status_t sendSessionEvent(const CasSessionId &sessionId, int32_t event, int32_t arg, const CasData &eventData) override; virtual status_t provision(const String8 &provisionString) override; virtual status_t refreshEntitlements(int32_t refreshType, const CasData &refreshData) override; public: CasPluginStatusCallback mStatusCallback; CasPluginCallback mCallback; CasPluginCallbackExt mCallbackExt; private: void *mAppData; }; }; }; #endif /* MYCAS_PLUGIN_H */참고로 위의 openSession()에 사용된 SessionIntent는 hardware/interfaces/cas/aidl/android/hardware/cas/SessionIntent.aidl 파일에 아래와 같이 정의되어 있다.
enum SessionIntent { LIVE, PLAYBACK, RECORD, TIMESHIFT, }대응하여 frameworks/base/media/java/android/media/MediaCas.java 파일에서 아래와 같이 정의되어 있다.
public static final int SESSION_USAGE_LIVE = android.hardware.cas.SessionIntent.LIVE; public static final int SESSION_USAGE_PLAYBACK = android.hardware.cas.SessionIntent.PLAYBACK; public static final int SESSION_USAGE_RECORD = android.hardware.cas.SessionIntent.RECORD; public static final int SESSION_USAGE_TIMESHIFT = android.hardware.cas.SessionIntent.TIMESHIFT;또, ScramblingMode는 hardware/interfaces/cas/aidl/android/hardware/cas/ScramblingMode.aidl 파일에 아래와 같이 정의되어 있다.
enum ScramblingMode { RESERVED = 0, DVB_CSA1, DVB_CSA2, DVB_CSA3_STANDARD, ... MULTI2, AES128, ... }대응하여 frameworks/base/media/java/android/media/MediaCas.java 파일에서 아래와 같이 정의되어 있다.
public static final int SCRAMBLING_MODE_DVB_CSA1 = android.hardware.cas.ScramblingMode.DVB_CSA1; public static final int SCRAMBLING_MODE_DVB_CSA2 = android.hardware.cas.ScramblingMode.DVB_CSA2; public static final int SCRAMBLING_MODE_DVB_CSA3_STANDARD = android.hardware.cas.ScramblingMode.DVB_CSA3_STANDARD; ... public static final int SCRAMBLING_MODE_MULTI2 = android.hardware.cas.ScramblingMode.MULTI2; public static final int SCRAMBLING_MODE_AES128 = android.hardware.cas.ScramblingMode.AES128; - MyCasPlugin.cpp 파일을 아래와 같이 작성한다.
#define LOG_TAG "MYCAS" #include <utils/Log.h> #include <media/stagefright/MediaErrors.h> #include "MyCasPlugin.h" namespace android { namespace mycas { static String8 sessionIdToString(const std::vector<uint8_t> &array) { String8 result; for (size_t i = 0; i < array.size(); i++) { result.appendFormat("%02X", array[i]); } if (result.isEmpty()) { result.append("(null)"); } return result; } MyCasPlugin::MyCasPlugin(void *appData, CasPluginCallback callback) : mCallback(callback), mAppData(appData) { } MyCasPlugin::MyCasPlugin(void *appData, CasPluginCallbackExt callbackExt) : mCallbackExt(callbackExt), mAppData(appData) { } MyCasPlugin::~MyCasPlugin() { } status_t MyCasPlugin::setStatusCallback(CasPluginStatusCallback callback) { ALOGI("%s", __func__); mStatusCallback = callback; return OK; } status_t MyCasPlugin::setPrivateData(const CasData &privateData) { ALOGI("%s", __func__); (void)privateData; return OK; } status_t MyCasPlugin::openSession(CasSessionId *sessionId __unused) { ALOGE("%s: Error: Deprecated method. Must use openSession_1_2()", __func__); return ERROR_CAS_CANNOT_HANDLE; } status_t MyCasPlugin::openSession(uint32_t sessionIntent, uint32_t scramblingMode, CasSessionId *sessionId) { ALOGI("%s: sessionIntent=%d, scramblingMode=%d", __func__, sessionIntent, scramblingMode); /* 실제 구현 */ ALOGD("%s: sessionId=%s", __func__, sessionIdToString(*sessionId).string()); return OK; } status_t MyCasPlugin::closeSession(const CasSessionId &sessionId) { ALOGI("%s: sessionId=%s", __func__, sessionIdToString(sessionId).string()); /* 실제 구현 */ return OK; } status_t MyCasPlugin::setSessionPrivateData(const CasSessionId &sessionId, const CasData &privateData) { ALOGI("%s: sessionId=%s", __func__, sessionIdToString(sessionId).string()); (void)privateData; return OK; } status_t MyCasPlugin::processEcm(const CasSessionId &sessionId, const CasEcm &ecm) { ALOGI("%s: sessionId=%s", __func__, sessionIdToString(sessionId).string()); /* 실제 구현 */ return OK; } status_t MyCasPlugin::processEmm(const CasEmm &emm) { ALOGI("%s", __func__); /* 실제 구현 */ return OK; } status_t MyCasPlugin::sendEvent(int32_t event, int32_t arg, const CasData &eventData) { ALOGI("%s: event=%d, arg=%d", __func__, event, arg); (void)eventData; return OK; } status_t MyCasPlugin::sendSessionEvent(const CasSessionId &sessionId, int32_t event, int32_t arg, const CasData &eventData) { ALOGI("%s: sessionId=%s, event=%d, arg=%d", __func__, sessionIdToString(sessionId).string(), event, arg); (void)eventData; return OK; } status_t MyCasPlugin::provision(const String8 &provisionString) { ALOGI("%s: str=%s", __func__, provisionString.string()); return OK; } status_t MyCasPlugin::refreshEntitlements(int32_t refreshType, const CasData &refreshData) { ALOGI("%s: refreshType=%d", __func__, refreshType); (void)refreshData; return OK; } } } - 이제 빌드하면 결과로 /vendor/lib/mediacas/libmycasplugin.so 파일이 빌드된다.
CBS 기반에서 MediaCas plugin 사용하기
CBS(Common Broadcast Stack) 기반에서 구현한 MediaCas plugin을 사용하려면 아래와 같이 할 수 있다.
- broadcaststack/libs/dtvcas/vendor/ 경로에 (예: broadcaststack/libs/dtvcas/vendor/mycas/) CAS wrapper, CAS wrapper factory를 구현한다. (필요시 project listener도 구현)
- broadcaststack/libs/dtvcas/gencas-library/src/main/java/com/google/android/tv/dtvcas/DtvCasBuilder.java 파일에서 아래 예와 같이 새로 구현한 CasWrapperFactory(여기서는 MyCasWrapperFactory)를 추가한다.
public static CasWrapperFactory[] createFactory(String config) { return new CasWrapperFactory[]{new WidevineCasWrapperFactory(config), new MyCasWrapperFactory(config)}; }참고로 만약에 ECM의 table ID가 0x80, 0x81로 toggle되는 시스템이 아니라면, broadcaststack/cbs_configs.json 파일에서 아래와 같이 true로 수정한 후, 해당 CAS wrapper에서 ECM 필터 설정을 구현하면 된다.
"cas": { "casWrapperToHandleEcmFilters": true, }, - broadcaststack/libs/dtvcas/gencas-library/Android.bp 파일에서 아래와 같이 새로 구현한 CAS 라이브러리를 추가한다.
android_library { static_libs: [ "dtvcas-base-library", "dtvcas-library", "dtvcas-widevine-library", "dtvcas-nagra-library", "dtvcas-synamedia-library", "dtvcas-irdeto-library", "dtvcas-mycas-library", ], } - TunerHAL에서 descrambler 연결
- MyCasPlugin.cpp 파일의 openSession() 함수에서 세션을 생성하고 keyslot을 생성한다.
- 아래와 같이 MediaCas와 Tuner HAL을 연결한다. (Tuner HAL 예, PMT를 파싱하여 ES PID를 추출하여 addPid()를 호출함)
frameworks/base/media/java/android/media/tv/tuner/Descrambler.java: addPid() 등 --- JNI --- -> {TunerHAL경로}/aidl/impl/Descrambler.cpp: addPid() 등 (PID 채널 open, Keyslot 얻어서 Keyslot에 PID channel 연결) - CBS에서 ECM이 잡히면 아래와 같이 ECM을 MediaCas 플러그인으로 전달한다.
mSession.processEcm(ecm); - 결과로 MyCasPlugin.cpp 파일의 processEcm() 함수가 호출되고, 여기에서 ECM을 처리하여 descrambling key를 얻고 descrambler에 key를 세팅한다.
맺음말
실제로 나는 프로젝트에서 MediaCas plugin이 필요하여 개발하였는데, 여기서는 추후 참조를 위하여 간단히 MediaCas 템플릿 정도를 제작하는 방법을 기록하였다.