从最顶层的音视频播放类APP至底层DrmPlugins,逐层向下介绍在Android体系下的DRM插件的开发与使用流程。方便了解、掌握=DRM开发的关键流程。首先,本文档整理Android DRM的架构中的重要类图,并同步对标我们代码中的文件和类图。理解DRM技术的关键在于理解密钥和码流的传递路线这两条主线再进一步加深强化对整个过程的熟悉。
Android
应用程序基于Android框架向上开发,DRM插件基于Android框架向下开发。Android framework code runs inside an app’s process.
从Android的DRM架构里,可以看到我们开发的插件在整个自顶向下的架构中是什么位置,以及粗略了解DRM插件是如何被使用的。
DRM框架的目的:能让安卓设备可以播放更多的内容,不同的内容和硬件设备可能使用的是不同的内容版权保护机制或者没有版权管理机制,但是安卓设备想尽可能多的播放更多内容。
DRM框架的功能:DRM框架负责提供的是DRM内容和许可之间的关联、处理管理权限。
DRM框架的好处:DRM框架为应用层的开发人员提供统一接口,隐藏了 DRM 操作的复杂性。DRM 框架为受保护和不受保护的内容提供了一致的操作模式。
从下图中看,从BINDER IPC PROXIES开始,有两种方式使用DRM插件。一个是经过IDrmManagerService——>DRM SERVER——>Legacy Drm Plugin,另一个是经过了IDrm和ICrypto——>Media DRM SERVER——>MediaDrm Plugins。
我理解,前一条路径,是为了遗留的老版插件准备的,在系统软件包中,这部分插件是供应商或者SOC制造商提供好的.so文件,放在了vendor/lib/drm/
文件夹里面。如何使用呢?DRM Plugin通过动态库的方式集成到设置中去。由DrmManagerService通过成员变量DrmManager调用loadPlugins()进行加载系统里所有的插件。
后一条路径,是使用HIDL化的DRM插件的。这一种是现用现构建插件,不是用我们放好的.so文件。DRM Plugin和CryptoPlugin是由各自的Factory创建出来的。
具体接口的定义可以看:frameworks/av/include/media
name | function | |
---|---|---|
Mediaplayer | MediaPlayer 类可用于控制音频/视频文件和流的播放 | MediaPlayer |
MediaExtractor | 解复用。视频解析,就是音视频分离,解析头信息,分别获取音视频流。 | MediaExtractor |
MediaDRM | 用于获取用于解密受保护媒体流的密钥,与MediaCrypto结合使用 | MediaDrm |
MediaCrypto | 与MediaCodec结合使用,用于解码加密的媒体信息 | MediaCrypto |
MediaCodec | 编码器/解码器组件。 | MediaCodec |
DrmManagerClient | DRM 框架的主要编程接口。应用程序必须实例化此类以通过 DRM 框架访问 DRM 代理。 | DrmManagerClient |
Android中的DRM HAL详解
HardWare Abstraction Layer. HAL是底层硬件和上层框架直接的接口,框架层通过HAL可以操作硬件设备,HAL的实现在用户空间。HAL 是一个抽象层,其中包含硬件供应商要实现的标准接口。借助 HAL,Android 可以忽略较低级别的驱动程序实现。借助 HAL,您可以顺利实现相关功能,而不会影响或更改更高级别的系统。Android8.0以下为旧版,等于以上为新版。
可通过调用 .hal
文件内的接口中定义的方法将数据发送到服务。具体方法有两类:
不返回值但未声明为 oneway
的方法仍会阻塞。
HAL interface definition language. HAL接口定义语言。HIDL定义的接口/方法,是由服务端去实现的,给客户端通过Binder去调用的。在 HIDL 接口中声明的所有方法都在一个方向上调用,要么从 HAL 发出,要么到 HAL。接口没有指定具体调用方向。需要从 HAL 发起调用的架构应该在 HAL 软件包中提供两个(或更多个)接口并从每个进程提供相应的接口。我们根据接口的调用方向来取名“客户端”或“服务器”(即 HAL 可以是一个接口的服务器,也可以是另一个接口的客户端)。
AIDL( Android Interface definition language) Android接口定义语言。是一种android内部进程间通信IPC( Interprocess Communication)接口的描述语言。在Android平台上,每个应用程序都运行在自己的进程空间,一个进程通常不能访问另一个进程的内存空间,而在开发Android应用中,经常会在不同的进程间传递对象,因此为了实现android内部进程间通信, Android提供了AIDL接口机制,用于约束两个进程间的通信规则,实现Android设备上两个进程间通信。
使用AIDL接口,进程之间的通信信息,首先会被转换成AIDL协议消息,然后发送给对方,对方收到AIDL协议消息后再转换成相应的对象,由于进程之间的通信信息需要双方转换,所以Android采用代理类在背后实现了信息的双向转换,代理类由Android编译器生成,对开发人员来说是透明的。
关于IBinder
binder inter-process communication (IPC)
Binder的目的:为了方便Android系统的快速移植、升级,提升系统稳定性,Android引入了HAL Binder的机制,把framework和HAL进行隔离,减少了framework和HAL的耦合性,使得framework部分可以直接被覆盖、更新,而不需要重新对HAL进行编译。
我们没有直接使用真正的Binder,而是使用的代理。
Binder RPC详细讲解
Remote Procedure Call,直接调用另一个进程中的方法。
RPC是基于IPC Binder机制实现的。Android系统中的Binder为IPC的一种实现方式,为Android系统RPC机制提供底层支持;其他常见的RPC还有COM组件、CORBA架构等。不同之处在于Android的RPC并不需要实现不同主机或不同操作系统间的远程调用,所以、它属于一个轻量级的IPC。
因为Binder的实体位于server这个进程,而它的引用却遍布各个client进程中,这淡化了进程间通信过程,整个系统仿佛运行在同一个面向对象的程序之中。
Server会实现hidl所定义的接口,Client进程会调用Server进程所实现的方法。
总结:Android系统的RPC = Binder进程间通信 + 在Binder基础上建立起来的进程间函数调用机制。
RPC中需要了解transaction和onTransact以及数据的传输是靠Parcel的。
IPC ( Inter-Process Communication ) 进程间通信 : 数据在 不同的进程 之间传递 ; 如 : 进程 A 发送数据到进程 B ;
RPC ( Remote Procedure Call ) 远程过程调用 : A 进程通过 IPC 发送数据到 B 进程 , B 进程调用自己本地的相关逻辑 , A 进程通过 RPC 调用了 B 进程的代码 ;
RPC 是在 IPC 基础上进行的封装 , IPC 负责数据的跨进程传输 ;
Binder RPC详细讲解
深入浅出Binder 详解Binder Bp Bn
transact onTransact
A Parcel is a container for a message that can be used to transport data between processes or threads.
Parcel提供了一套将对象(Object)序列化的机制,可以将序列化之后的数据写入到内核态的的共享内存中,其他进程可以通过Parcel可以从这块共享内存中读取处字节流并反序列化成原有对象。数据结构,读或写都是从当前指针位置处开始,读完或写完指针就向后移动这段距离。所以提取数据的时候是按顺序读取数据的。
AIDL原理学习链接:
Binder硬核资料 Android必须看
这里我们以老版本的DRM架构进行了解Android中DRM的重要类图。从这个过程中,可以了解到,自顶向下类的调用和继承关系以及DRM插件是从哪里开始开发的。
流程
对于播放器来说,它只和DRMExtrator和DRMSource交互。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8fv2J3tA-1688612423137)(D:\学习笔记\xmind\DRM架构.png)]
系统中存在两个进程:
drm drmserver
media mediadrmserver
drm相关的服务:
drm.drmManager:[drm.IDrmManagerService]
media.drm:[android.media.IMediaDrmService]
以IDrm为例,IDrm是一个Binder,其中既有服务端的接口,又有客户端接口。Bp是客户端,Bn是服务端
客户端:
BpDrm:BpInterface
服务端:
BnDrm:BpInterface
registerAsService
frameworks\av\drm\mediadrm\plugins\clearkey\hidl\service.cpp
HIDL 接口服务器(实现接口的对象)可注册为已命名的服务。
clearkey实现的hidl服务中,注册了drmFactory和cryptoFactory两个服务
DrmManagerClientImpl.cpp
getService
DrmHAL和Crypto HAL作为服务的使用者,获取由vendor真正实现的plugin里面的服务,例如clearkey等。
这是IDrm.cpp中,Binder处理客户端的数据发送请求的,将数据写入缓存。remote()就是BpBinder
remote()其实是BpBinder,transact执行IPCThreadState::self()->transact(mHandle, code, data, reply, flags),注意mHandle是0,代表ServiceManager;
这里主要是侧重于带DRM的视频播放过程。涉及MediaExtractor、MediaDRM、 MediaCodec、MediaCrypto,希望能在DRM这条线里,把上述几者负责的内容、在播放流程中的位置和作用能够讲清楚。
Android支持的DRM的播放方式有两种:一是基于基本码流ES的,如Widevine,Marlin,二是基于容器的如OMA drm。主要介绍ES Based DRM的媒体播放流程。
可参考资料:
2.exoplayer架构
3.drm解密过程 稀土
4.Android MediaDRM Frame Work
widewine 非常好!
几个问题:
1.MediaDRM的创建
2.MediaCodec的创建
3.MediaCrypto的创建
4.MediaCodec与MediaCrypto的关联,MediaCodec如何调用MediaCrypto进行播放,即MediaCrypto如何获取的加密码流?
5.MediaCrypto与MediaDRM如何关联,即MediaCrypto如何获取的license信息?
6.cryptoInfo有什么?从哪封装?从哪传递?
7.获取到加密码流和license的MediaCrypto是如何解密的?
1.create MediaDrm並且調用其openSession方法,該方法會返回一個sessionID,標識該次解碼工作。
2.create MediaCrypto給MediaCodec 。 它需要一個UUID和initdata,UUID是Widevine的Scheme ID,在Exoplayer的源碼中可以看到,在C.java裡面。而initData就是上面說到的sessionID.
3.對license server做license的call,得到的reponse就是我們需要的license了,此時只需要調用MediaDrm的provideKeyResponse(sessionId ,response ,keySetId)方法,視頻就可以自動開始播放了。接下来调用的是具体的DrmPlugin插件。
4.conclusion: MediaCodec負責解碼,
- 需要一個MediaCrypto, 獲取MediaDrm對的sessionId讓framework去尋找對應的license
需要一個MediaDrm,負責保存從服務器下載下來的license並且提供一個唯一的sessionId給MediaCrypto.
VidePlayerSubWnd
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
exoplayer = ExoPlayerFactory.newSimpleInstance(mContext, renderFactory, trackSelector, drmSessionManager);
private DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager;//用mediadrm创建一个drmSessionManager
PlayerMessage:
Defines a player message which can be sent with a PlayerMessage.Sender and received by a PlayerMessage.Target
VidePlayerSubWnd
mediaDrm = FrameworkMediaDrm.newInstance(uuid);//封装的Framework里面的MediaDrm
private DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager;//用mediadrm初始化一个drmSessionManager
exoplayer = ExoPlayerFactory.newSimpleInstance(mContext, renderFactory, trackSelector, drmSessionManager);
mediadrm什么时候openSession?
并传递给defaultDrmsessionManager.acquireSession内创建session 时初始化类成员变量。
MediaCodecRenderer的主线:render()
创建crypto
创建codec
配置codec
循环倒待解密数据
MdiaCodecRender.render()——>onInputFormatChanged()——>drmSessionManager.acquireSession()——>new session,session.aquire()——>openInternal(),doLicense())——>mediadrm.opensession获取sessionID后(在opensession的过程中,先获取了sessionID,再用sessionID创建了crypto(即用sessionID将crypto和drm关联在了一起))
(FrameworkMediaDrm里面 创建了MediaCrypto)
+ drmsession是怎么赋值的?
onInputFormatChanged {
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
}
+ acquireSession里面做了什么?
DefaultDrmSessionManager.java
if (session == null) {
// Create a new session.
session =
new DefaultDrmSession<>(
uuid,
mediaDrm,
this,
schemeDatas,
mode,
offlineLicenseKeySetId,
optionalKeyRequestParameters,
callback,
playbackLooper,
eventDispatcher,
initialDrmRequestRetryCount);
sessions.add(session);
}
session.acquire();
+ session.acquire函数
{
openInternal
doLicense
}
+ DefaultDrmSession.java
private boolean openInternal(boolean allowProvisioning) {
if (isOpen()) {
// Already opened
return true;
}
try {
sessionId = mediaDrm.openSession();
eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired);
mediaCrypto = mediaDrm.createMediaCrypto(sessionId);
state = STATE_OPENED;
return true;
} catch (NotProvisionedException e) {
if (allowProvisioning) {
provisioningManager.provisionRequired(this);
} else {
onError(e);
}
} catch (Exception e) {
onError(e);
}
return false;
}
1.mediaCodec什么时候创建,以及如何与crypto关联在一起的?
crypto在codec创建前完成了创建。
用drmSessionManager初始化MediaCodecRenderer
MediaCodecRenderer.java
1.codec的创建和配置crypto
maybeInitCodec()
{
drmSession = pendingDrmSession;
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
{
initCodecWithFallback{
initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto){
codec = MediaCodec.createByCodecName(name);
configureCodec(codecInfo,codec,format,crypto,configureWithOperatingRate)
}
}
}
}
MediaCodecRenderer的主线:render()
创建crypto
创建codec
配置codec
循环倒待解密数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQI6uIcN-1688612423141)(C:\Users\j00808041\AppData\Roaming\Typora\typora-user-images\image-20230508095121422.png)]
解密:
MediaCodecRenderer.java
不断喂码流
render函数 while(feedInputBuffer) {}
如果buffer加密了 就先获取cryptoInfo
getFrameworkCryptoInfo
codec.queueSecureInputBuffer(...)
再调用queueSecureInputBuffer
调用的是MediaCodec.java里面的native_queueSecureInputBuffer,解密信息封装在cryptoInfo里面
MediaCodec里面发送了消息:
真正的codec可能在:/frameworks/av/media/libstagefright/MediaCodec.cpp
sp
把需要解密的消息发送出去,包括指针、key、iv等;
MediaCodec::queueSecureInputBuffer()
MediaCodec自己收到消息后,onQueueInputBuffer里面对cryptoInfo进行了拆解,又调用的acodecBufferChannel,它里面调的crypto 就掉到了crypto plugin的插件去解密
❓谁真正的去处理消息呢?
mCrypto = static_cast
mBufferChannel->setCrypto(mCrypto);
MediaCodec::onQueueInputBuffer()
ACodecBufferChannel.cpp
status_t ACodecBufferChannel::queueSecureInputBuffer
{
result = mCrypto->decrypt(key, iv, mode, pattern,
source, it->mClientBuffer->offset(),
subSamples, numSubSamples, destination, errorDetailMsg);
}
在drmplugin和cryptoplugin是通过session关联到一起的:
drmplugin将license等信息置到session里面并保存;cryptoplugin通过过去session实例获取相关信息。
cryptoplugin.cpp
setMediaDrmSession
{
mSession = SessionLibrary::get()->findSession(sessionId);
}
传递下来DecryptParam keyId和iv
struct DecryptParam {
uint32_t secure { 0 };
int mode { 0 };
uint32_t skipBlocks { 0 };
uint32_t encryptBlocks { 0 };
const void *srcPtr { nullptr };
const SessionSubSample *subSamples { nullptr };
size_t numSubSamples { 0 };
void *dstPtr { nullptr };
VideoType videoType { VIDEO_NONE };
};
cryptodata是统一的,各个extractor都会提取出来,然后提取到cryptoInfo,触发format的变化;
MediacodecRender检测到format变化时,会触发drmsessionManager.acquireSession();
sessions是保存在manager本地的一个列表,不同uuid对应不同session,如果有存在的就用存在的session,没有就新建。
session.acquire
一个session里面只有一个mediadrm对象,session里面只获取一个crypto
FrameworkMediaDrm由UUID唯一创建,new一个对象。
Audio和Video都会调用父类的这个函数,证明pendingDrmSession是唯一一个,由acquireSession绑定在一起
+ onInputFormatChanged
{
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
}
+ cacquireSession 证明session是由sessionID绑定的唯一的
{
if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) {
session = existingSession;
}
if (session == null) {
session = new(mediaDrm schemeDatas)
}
session.acquire();
}
audio
onInputFormatChanged()
video
onInputFormatChanged()
father
audio render和video render共同继承了render基类。
参见安卓官网MediaCodecService变更
MediaCodecRender,java
initCodecWithFallback()
mediaCrypto.requiresSecureDecoderComponent()
是否需要安全解码器取决于mediaCrypto.requiresSecureDecoderComponent(mimeType);
Android12已经弃用OMX了,采用CCodec
MediaCodec支持两种模式编解码器,即同步synchronous、异步asynchronous,所谓同步模式是指编解码器数据的输入和输出是同步的,编解码器只有处理输出完毕才会再次接收输入数据;而异步编解码器数据的输入和输出是异步的,编解码器不会等待输出数据处理完毕才再次接收输入数据。这里,我们主要介绍下同步编解码,因为这种方式我们用得比较多。我们知道当编解码器被启动后,每个编解码器都会拥有一组输入和输出缓存区,但是这些缓存区暂时无法被使用,只有通过MediaCodec的dequeueInputBuffer/dequeueOutputBuffer方法获取输入输出缓存区授权,通过返回的ID来操作这些缓存区。
queueInputBuffer:输入流入队列
dequeueInputBuffer:从输入流队列中取数据进行编码操作
getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
releaseOutputBuffer:处理完成,释放ByteBuffer数据
关于DRM的深层次分析
feedInputBuffer
{
inputIndex = codec.dequeueInputBuffer(0);
buffer.data = getInputBuffer(inputIndex);
}
AcodecBufferChanal ——> queueSecureInputBuffer
source:
ICrypto::SourceBuffer source;
source.mSharedMemory = it->mSharedEncryptedBuffer;
source.mHeapSeqNum = mHeapSeqNum;
DestinationBuffer destination:
从当前成员mBufferInputs里面找出传入的Buffer的迭代器it,用迭代器再获取指针secureData,secureData指针获取到secureHandle,作为输出地址即nativeHandle.
BufferInfoIterator it = findClientBuffer(array, buffer)——>secureData=it->mCodecBuffer.get()——>secureData——>secureHandle——destination.mHandle
❓是谁初始化了mBufferInputs?里面都是什么
ACodec.c里面:
ACodec::allocateBuffersOnPort
kPortIndexInput一直被初始化为0 难道没有被更改过 确实没看见赋值在哪
if (secure) {
destination.mType = ICrypto::kDestinationTypeNativeHandle;
destination.mHandle = secureHandle;
}
struct DestinationBuffer {
62 DestinationType mType;
63 native_handle_t *mHandle;
64 sp<IMemory> mSharedMemory;
65 };
allocateBuffersOnPort处理 输出buffer
setInputBufferArray处理 输入buffer
先配置输出buffer是否安全,再配置输入buffer,再打包初始化为BufferInfo的列表