音视频小记 - 实现 MediaCodec 播放 scrcpy 视频流

我们今天要实现的是安卓通过scrcpy-server显示另一台安卓的屏幕
要实现的是从自定义协议(scrcpy-server)中播放视频,所以直接用现成的视频播放器是不现实的。

前言

在动手写前,从各个博客补了一些知识,几乎每一篇在实现 MediaCodec 解码播放的时候都会说,这个通常和 MediaExtracter 配合使用,然后具体解码就是借助 MediaExtracter 解码到 MediaCodec 中,找了很久没找到纯 MediaCodec 解码播放视频的。

包括搜了一些 stackoverflow ,搜到的是以下代码。https://stackoverflow.com/questions/24622816/android-using-mediaextractor-and-mediacodec-from-a-socket-streaming-mpeg
setDataSource 不再指向路径,而是指向 socket 得到的 fd(文件描述符),看起来很合理的代码。
代码一跑就挂了😕,输出如下。

所以大致得出结论,用 MediaExtracter 估计是行不通了。

问题分析

最后在这两篇博客找到一些有用的信息。mediaext中的readSimpleData显然是有内部的防止粘包的逻辑的,包括advice方法跳到下一帧,就说明这样的方式解码,它就根据视频的协议,例如h264等,知道多长的位置是这一帧的结尾,所以使用MediaCodec+MediaExtracter的时候,我们不用考虑会出现数据流粘包的问题。

粘包解决

解决粘包还是得从协议本身上来,最后在 scrcpt/
-server,发现服务端在发送一帧数据包前,会先发送pts 信息,和包长度,这是很重要的数据。

所以在客户端进行解码这部分的视频流的时候,先取出12个字节,其中8个字节是long类型,为pts时间戳,4个字节是数据包长度,意味着这一帧的长度,只有将这个长度的内容完整读取,才能正常渲染,如果某一次因为缓冲没有读取完,那只有等待下次继续读取,直到读取到数据的尾部。
最后送显,这部分代码在各种各样的轮子中都能看到。

趟了一个非常蠢的坑,将getDecoderByType写成了getEncoderByType,还定位了非常久,crash 的堆栈会跟其它的问题混淆,例如给定size的width或者height为奇数,也是一模一样的crash 堆栈,离谱!

整体流程

服务端

1.启动一台手机的 scrcpy-server
2.转发这台手机的 scrcpy 端口到 PC的:10000端口上
3.等待另一台手机通过 $PC_IP:10000 获取数据流

客户端部分

连接视频流套接字

用上面的$PC_IP:10000

连接控制事件套接字

构造 MediaCodec

MediaFormat 构造并插值

开始解码

我们不再借助 MediaExtracter 往 MediaCodec 喂数据,现在通过从 socket 中读取每帧的数据交给 MediaCodec,这是与普通的视频播放器的差异点。

看下最后实现的效果。

作者

梦魇兽

发布于

2021-12-24

更新于

2023-03-11

许可协议

评论