自顶向下学习Android DRM框架

自顶向下学习Android DRM框架

前言

从最顶层的音视频播放类APP至底层DrmPlugins,逐层向下介绍在Android体系下的DRM插件的开发与使用流程。方便了解、掌握=DRM开发的关键流程。首先,本文档整理Android DRM的架构中的重要类图,并同步对标我们代码中的文件和类图。理解DRM技术的关键在于理解密钥和码流的传递路线这两条主线再进一步加深强化对整个过程的熟悉。
📌

1 Android 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 框架为受保护和不受保护的内容提供了一致的操作模式。

Android11之前的架构

在这里插入图片描述

Android11之后的架构

在这里插入图片描述

Android11之前的框架

从下图中看,从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创建出来的。

在这里插入图片描述

Android11之后的框架

https://source.android.google.cn/static/docs/core/media/images/ape_fwk_drm_2_postO.png?hl=zh-cn

Framwork的主要类

具体接口的定义可以看:frameworks/av/include/media

namefunction
MediaplayerMediaPlayer 类可用于控制音频/视频文件和流的播放MediaPlayer
MediaExtractor解复用。视频解析,就是音视频分离,解析头信息,分别获取音视频流。MediaExtractor
MediaDRM用于获取用于解密受保护媒体流的密钥,与MediaCrypto结合使用MediaDrm
MediaCrypto与MediaCodec结合使用,用于解码加密的媒体信息MediaCrypto
MediaCodec编码器/解码器组件。MediaCodec
DrmManagerClientDRM 框架的主要编程接口。应用程序必须实例化此类以通过 DRM 框架访问 DRM 代理。DrmManagerClient

2 概念

HAL

Android中的DRM HAL详解

HardWare Abstraction Layer. HAL是底层硬件和上层框架直接的接口,框架层通过HAL可以操作硬件设备,HAL的实现在用户空间。HAL 是一个抽象层,其中包含硬件供应商要实现的标准接口。借助 HAL,Android 可以忽略较低级别的驱动程序实现。借助 HAL,您可以顺利实现相关功能,而不会影响或更改更高级别的系统。Android8.0以下为旧版,等于以上为新版。

可通过调用 .hal 文件内的接口中定义的方法将数据发送到服务。具体方法有两类:

  • Blocking方法会等到服务器产生结果。
  • Oneway方法仅朝一个方向发送数据且不阻塞。如果 RPC 调用中正在传输的数据量超过实现限制,调用可能会阻塞或返回错误指示(具体行为尚不确定)。

不返回值但未声明为 oneway 的方法仍会阻塞。

HIDL

HAL interface definition language. HAL接口定义语言。HIDL定义的接口/方法,是由服务端去实现的,给客户端通过Binder去调用的。在 HIDL 接口中声明的所有方法都在一个方向上调用,要么从 HAL 发出,要么到 HAL。接口没有指定具体调用方向。需要从 HAL 发起调用的架构应该在 HAL 软件包中提供两个(或更多个)接口并从每个进程提供相应的接口。我们根据接口的调用方向来取名“客户端”或“服务器”(即 HAL 可以是一个接口的服务器,也可以是另一个接口的客户端)。

AIDL

AIDL( Android Interface definition language) Android接口定义语言。是一种android内部进程间通信IPC( Interprocess Communication)接口的描述语言。在Android平台上,每个应用程序都运行在自己的进程空间,一个进程通常不能访问另一个进程的内存空间,而在开发Android应用中,经常会在不同的进程间传递对象,因此为了实现android内部进程间通信, Android提供了AIDL接口机制,用于约束两个进程间的通信规则,实现Android设备上两个进程间通信。
使用AIDL接口,进程之间的通信信息,首先会被转换成AIDL协议消息,然后发送给对方,对方收到AIDL协议消息后再转换成相应的对象,由于进程之间的通信信息需要双方转换,所以Android采用代理类在背后实现了信息的双向转换,代理类由Android编译器生成,对开发人员来说是透明的。

Binder IPC

关于IBinder

binder inter-process communication (IPC)

Binder的目的:为了方便Android系统的快速移植、升级,提升系统稳定性,Android引入了HAL Binder的机制,把frameworkHAL进行隔离,减少了framework和HAL的耦合性,使得framework部分可以直接被覆盖、更新,而不需要重新对HAL进行编译。

我们没有直接使用真正的Binder,而是使用的代理。

RPC

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与RPC

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

transact onTransact

Parcel

A Parcel is a container for a message that can be used to transport data between processes or threads.

Parcel提供了一套将对象(Object)序列化的机制,可以将序列化之后的数据写入到内核态的的共享内存中,其他进程可以通过Parcel可以从这块共享内存中读取处字节流并反序列化成原有对象。数据结构,读或写都是从当前指针位置处开始,读完或写完指针就向后移动这段距离。所以提取数据的时候是按顺序读取数据的。

基于AIDL实现的Binder机制

AIDL原理学习链接:

Binder硬核资料 Android必须看

Binder图解翻译
在这里插入图片描述

RPC通信过程示意图

img

3 Android10中DRM架构route1细化

这里我们以老版本的DRM架构进行了解Android中DRM的重要类图。从这个过程中,可以了解到,自顶向下类的调用和继承关系以及DRM插件是从哪里开始开发的。

流程

对于播放器来说,它只和DRMExtrator和DRMSource交互。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8fv2J3tA-1688612423137)(D:\学习笔记\xmind\DRM架构.png)]

4 Android10中DRM架构route2细化

5 以android Q源码为例分析

drm进程与服务

系统中存在两个进程:
drm drmserver
media mediadrmserver
drm相关的服务:
drm.drmManager:[drm.IDrmManagerService]
media.drm:[android.media.IMediaDrmService]

IDrm

以IDrm为例,IDrm是一个Binder,其中既有服务端的接口,又有客户端接口。Bp是客户端,Bn是服务端

客户端:
BpDrm:BpInterface<IDrm>

服务端:

BnDrm:BpInterface<IDrm>

注册服务

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;

6 视频APP如何播放加密视频——Exoplayer

📌这里主要是侧重于带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 非常好!

Exoplayer: MediaCodec+MediaDRM方案
Exoplayer播放流程与模块调用

几个问题

1.MediaDRM的创建

2.MediaCodec的创建

3.MediaCrypto的创建

4.MediaCodec与MediaCrypto的关联,MediaCodec如何调用MediaCrypto进行播放,即MediaCrypto如何获取的加密码流?

5.MediaCrypto与MediaDRM如何关联,即MediaCrypto如何获取的license信息?

6.cryptoInfo有什么?从哪封装?从哪传递?

7.获取到加密码流和license的MediaCrypto是如何解密的?

MediaDrm 概览图
  • 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.

构建一个播放DRM内容的Exoplayer
VidePlayerSubWnd
  mediaDrm = FrameworkMediaDrm.newInstance(uuid);

exoplayer = ExoPlayerFactory.newSimpleInstance(mContext, renderFactory, trackSelector, drmSessionManager);
private DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager;//用mediadrm创建一个drmSessionManager

ExoPlayer调用时序图

PlayerMessage:

Defines a player message which can be sent with a PlayerMessage.Sender and received by a PlayerMessage.Target

MediaDRM怎么创建的
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工作流程

MediaCodecRenderer的主线:render()

创建crypto

创建codec

配置codec

循环倒待解密数据

MediaCodecRenderer创建流程

MediaCrypto怎么创建的——drm sessionID绑定crypto

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;
  }
MediaCodec怎么创建的——crypto配置codec

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)
       }
    }
  }
}

Render与MediaCodec MediaCrypto是怎么关联的——MediaCodecRenderer工作机制

MediaCodecRenderer的主线:render()

创建crypto

创建codec

配置codec

循环倒待解密数据

MediaCodecRenderer创建流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQI6uIcN-1688612423141)(C:\Users\j00808041\AppData\Roaming\Typora\typora-user-images\image-20230508095121422.png)]

加密码流的传递——MediaCodecRender feedInputBuffer内部

解密:

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<MediaCodec> MediaCodec::CreateByType()

把需要解密的消息发送出去,包括指针、key、iv等;

MediaCodec::queueSecureInputBuffer()

MediaCodec自己收到消息后,onQueueInputBuffer里面对cryptoInfo进行了拆解,又调用的acodecBufferChannel,它里面调的crypto 就掉到了crypto plugin的插件去解密

❓谁真正的去处理消息呢?

mCrypto = static_cast<ICrypto *>(crypto);
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);
  
}
解密信息的传递——MediaCrypto和MeidaDrm是怎么关联的?

在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 };
};
CryptoInfo和cryptodata——onInputFormatChanged()

cryptodata是统一的,各个extractor都会提取出来,然后提取到cryptoInfo,触发format的变化;

MediacodecRender检测到format变化时,会触发drmsessionManager.acquireSession();

AudioRender和VideoRender在session和crypto的使用关系

sessions是保存在manager本地的一个列表,不同uuid对应不同session,如果有存在的就用存在的session,没有就新建。

session.acquire

一个session里面只有一个mediadrm对象,session里面只获取一个crypto

FrameworkMediaDrm由UUID唯一创建,new一个对象。

AudioVideo都会调用父类的这个函数,证明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基类。

7 Exoplayer中的Codec及安全内存

参见安卓官网MediaCodecService变更

在这里插入图片描述

安全内存 创建解码器时分配安全内存的问题

MediaCodecRender,java
initCodecWithFallback()

安全解码器和非安全解码器问题
何时配置安全解码器

mediaCrypto.requiresSecureDecoderComponent()

是否需要安全解码器取决于mediaCrypto.requiresSecureDecoderComponent(mimeType);

配置的安全解码器接下来如何?
MediaCodec
架构

Android12已经弃用OMX了,采用CCodec

img

MediaCodec支持两种模式编解码器,即同步synchronous、异步asynchronous,所谓同步模式是指编解码器数据的输入和输出是同步的,编解码器只有处理输出完毕才会再次接收输入数据;而异步编解码器数据的输入和输出是异步的,编解码器不会等待输出数据处理完毕才再次接收输入数据。这里,我们主要介绍下同步编解码,因为这种方式我们用得比较多。我们知道当编解码器被启动后,每个编解码器都会拥有一组输入和输出缓存区,但是这些缓存区暂时无法被使用,只有通过MediaCodec的dequeueInputBuffer/dequeueOutputBuffer方法获取输入输出缓存区授权,通过返回的ID来操作这些缓存区

queueInputBuffer:输入流入队列

dequeueInputBuffer:从输入流队列中取数据进行编码操作

getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组

getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组

dequeueOutputBuffer:从输出队列中取出编码操作之后的数据

releaseOutputBuffer:处理完成,释放ByteBuffer数据

安全buffer
Drm安全强化

关于DRM的深层次分析

1.安全内存由谁分配
2.安全内存何时分配
3.是codec crypto谁拿到了安全内存?以什么形式拿到的安全内存?
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的列表

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值