一. WideVine简介
在收购Widevine之前,Android没有系统的数字版权保护机制,2010年12月,Google不惜重金将视频数字版权管理软件公司Widevine招安,弥补了Android在这方面的短板。Android从3.0开始就支持Widevine。现在Widevine已经成为GMS(Google Mobile Service)中必备的内容,所有想要得到GMS的手机厂商,都需要根据GMS的要求搭载Widevine。
WideVine的安全级别
WideVine提供了三种安全级别。
迪士尼等版权厂商对不同的视频清晰度,有着不同的安全级别要求。用户可以通过License中配置级别,从而满足版权厂商的要求。
二. ExoPlayer实现 WideVine播放
ExoPlayer 是谷歌开发的在Android平台上使用的开源播放器。它通过MediaCodec + MediaDrm 实现了对WideVine播放的支持。封装的接口也是比较容易使用,想播放WideVine视频,只需要实现接口 MediaDrmCallback
:
MediaDrmCallback drmCallback =
new WideVineDrmCallback(ExternPlayerExo.this, mDataSourceFactory);
mediaDrm = FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID);
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, drmCallback, null, false);
....
mExoPlayer = ExoPlayerFactory.newSimpleInstance(
mContext, new DefaultRenderersFactory(mContext), mTrackSelector, drmSessionManager);
MediaDrmCallback 需要实现两个接口:public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request)
: 请求Provisionpublic byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request)
: 请求license。
整个过程比较简单,如果需要快速开发出WideVine功能,可以使用ExoPlayer。但是会有几个问题:
- 需要额外引入一个ExoPlayer 的库。
- 出现问题无法修改。
所以最好还是自研WideVine的播放。
三. 自研WideVine播放
想要播放WideVine,会涉及到播放器的很多流程,大体涉及的流程如下:
1. 解析m3u8索引文件。
解析出m3u8中KEY的信息,包括:KEYFORMAT,URI等信息。
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,AAAATHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACwSEOmcQnb1u7mkYV47U5xyFMYiEDI61IXNX0iWvdFsX6j2bNQ4AUjzxombBg==",KEYID=0xE99C4276F5BBB9A4615E3B539C7214C6,IV=0xBBA0A21E523D460BA3B0F154C507E7C7,KEYFORMATVERSIONS="1",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"
KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" ,值是widevine的uuid, 这是一个WideVine加密的视频。
URI 里面是初始化CDM所需要的信息。
2. 初始化CDM模块
在Android中CDM是通过MediaDrm
类实现的。
mediaDrm = new MediaDrm(WIDEVINE_UUID);
mediaDrm.setOnEventListener(new MediaDrm.OnEventListener() {
@Override
public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
//监听drm信息,通过event,分别请求Provision或者请求license。
}
});
sessionId = mediaDrm.openSession();
请求license的initData,就是url中的值。
3. demux时,读取packet中的解密信息
播放器一般都是使用ffmpeg去做demux操作,出来一个packet。解密信息就包含在packet的ENCRYPT_INFO字段中:
int encryption_info_size;
const uint8_t *new_encryption_info = av_packet_get_side_data(mpkt, AV_PKT_DATA_ENCRYPTION_INFO, &encryption_info_size);
if (encryption_info_size <= 0 || new_encryption_info == nullptr) {
return false;
}
mAVEncryptionInfo = av_encryption_info_get_side_data(new_encryption_info, encryption_info_size);
AVEcnrypttionInfo的结构如下:
/**
* This describes encryption info for a packet. This contains frame-specific
* info for how to decrypt the packet before passing it to the decoder.
*
* The size of this struct is not part of the public ABI.
*/
typedef struct AVEncryptionInfo {
/** The fourcc encryption scheme, in big-endian byte order. */
uint32_t scheme;
/**
* Only used for pattern encryption. This is the number of 16-byte blocks
* that are encrypted.
*/
uint32_t crypt_byte_block;
/**
* Only used for pattern encryption. This is the number of 16-byte blocks
* that are clear.
*/
uint32_t skip_byte_block;
/**
* The ID of the key used to encrypt the packet. This should always be
* 16 bytes long, but may be changed in the future.
*/
uint8_t *key_id;
uint32_t key_id_size;
/**
* The initialization vector. This may have been zero-filled to be the
* correct block size. This should always be 16 bytes long, but may be
* changed in the future.
*/
uint8_t *iv;
uint32_t iv_size;
/**
* An array of subsample encryption info specifying how parts of the sample
* are encrypted. If there are no subsamples, then the whole sample is
* encrypted.
*/
AVSubsampleEncryptionInfo *subsamples;
uint32_t subsample_count;
} AVEncryptionInfo;
4. 创建硬解码器MediaCodec
由于是需要解密的,所以创建MediaCodec的时候,需要MediaCrypto信息去解密。
UUID drmUUID = UUID.fromString(uuid);
mediaCrypto = new MediaCrypto(drmUUID, sessionId);
同时,MediaCodec对流的格式有不同的要求:
- 对于video, 必须是用00000001/000001分隔的数据。(而对于用CENC加密的数据,则必须要是00000001分隔的,因为CENC就是用4个字节表示大小的,MediaCodec内部会做转换。)
- 对于Audio aac, 必须要指明是不是ADTS格式。如果不对的话,会出现解码错误。
boolean needSecureDecoder = false;
if (mediaCrypto != null) {
needSecureDecoder = !forceInsecureDecoder && mediaCrypto.requiresSecureDecoderComponent(mMime);
}
String codecName = getDecoderName(videoFormat, needSecureDecoder);
mMediaCodec = MediaCodec.createByCodecName(codecName);
if (surface instanceof Surface) {
mMediaCodec.configure(videoFormat, (Surface) surface, mediaCrypto, 0);
} else { //audio 没有surface
mMediaCodec.configure(videoFormat, null, mediaCrypto, 0);
}
5. 送入解码器去解码
解码加密数据时,需要使用queueSecureInputBuffer
, 而EOS的时候,则需要使用queueInputBuffer
方法。
if (secure && buffer != null) {
MediaCodec.CryptoInfo crypInfo = createCryptoInfo((EncryptionInfo) encryptionInfo); //创建解密信息
synchronized (queLock) {
mMediaCodec.queueSecureInputBuffer(index, 0, crypInfo, pts, flags);
}
} else {
if ((flags & BUFFER_FLAG_END_OF_STREAM) == BUFFER_FLAG_END_OF_STREAM) {
mMediaCodec.queueInputBuffer(index, 0, 0, 0, flags);
} else {
mMediaCodec.queueInputBuffer(index, 0, inputBuffer.limit(), pts, flags);
}
}
6. 渲染音频和视频
对于音频,可以从MediaCodec的buffer中直接读取出pcm数据。(根据这个猜测:Android 手机的音频安全级别都是L3的。)
对于视频,如果是L3的级别,也可以从buffer中读取出yuv数据。
对于L1的安全级别,则需要在mMediaCodec.configure的时候,传入最后渲染的surfaceView(必须是SurfaceView)。这样在mediaCodec releaseBuffer的时候,就直接渲染到view上了。程序无法直接接触到解码后的数据,保证了安全性。