Android音视频【七】H265硬编解码&视频通话
人间观察
我该如何去表达呢
前面介绍了H265
的一些知识,本篇实现利用camera
采集进行H265硬编码,利用WebSocket
来传输H265裸流,接收到H265的码流后进行H265解码渲染到surfaceview上,从而实现简易的视频通话。
主要有:摄像头如何处理,如何拿到摄像头的yuv数据,yuv数据怎么处理,实现Android H265硬编码和硬解码,vps,sps,pps怎么处理以及如何在网络上传输。
1 .这里用哪种协议不是本文的重点,本文采用java封装好websocket协议的组件,在真实项目中音视频通话可能不用websocket协议,更多的可能是webrtc。
2.没有涉及到音频的编解码和发送传输,音频会后续出系列介绍
3.本篇也是用kotlin来实现,为什么用kotlin?因为工作中没有用到,我想自己练习下。。。
效果图
h265视频通话预览图.png实现方案
h265硬编解码视频通话.pngCamera的YUV数据采集
简单说下camera,本篇拿camera摄像头来进行数据的采集,当然你也可以用camera2来实现,camera2是提供了更丰富的API(但是我想说真难用,拍个照,获取原始yuv数据写几百行代码),然后Google在jetpack中提供了camerax,camerax的api还是比较简单的。各种camera 花两天研究下就会了,现学现用都没啥,我们主要是介绍编解码和yuv数据的处理,这些基本都是不变的,不像上层camera的api一样。
在camera中主要就是打开camera设置预览画面大小和回调的数据格式(默认是NV21格式的yuv数据,NV21格式的数据基本上所有的摄像头都支持,所以Android默认采用这个)。设置预览回调的数据大小,一般为了方便处理设置的就是一帧yuv数据的大小,也就是y+u+v的数据大小=width * height + width * height的1/4 +width * height的1/4=width * height * 3 / 2。
局部代码如下:
fun startPreview() {
// 临时用后置摄像头,重点是编解码和数据的传输
camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK)
val parameters: Camera.Parameters = camera.parameters
// 摄像头默认NV21
Log.e(TAG, "previewFormat:" + parameters.previewFormat)
setPreviewSize(parameters)
camera.setParameters(parameters)
camera.setPreviewDisplay(holder)
// 由于硬件安装是横着的,如果是后置摄像头&&正常竖屏的情况下需要旋转90度
// 只是预览旋转了,数据没有旋转
camera.setDisplayOrientation(90)
// 让摄像头回调一帧的数据大小
buffer = ByteArray(width * height * 3 / 2)
// onPreviewFrame回调的数据大小就是buffer.length
camera.addCallbackBuffer(buffer)
camera.setPreviewCallbackWithBuffer(this)
camera.startPreview()
}
摄像头的预览旋转问题,如果是后置摄像头&&正常竖屏拿着,这时候你会发现预览出来的画面是横着的,所以需要旋转90度。当然前后摄像头和人为的旋转手机本身也需要做对应的旋转才行。
开启预览和设置yuv数据回调后,就会在onPreviewFrame回调中回调出来。
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
// 摄像头的原始数据yuv
camera!!.addCallbackBuffer(data)
}
YUV数据处理
关于YUV的数据的知识可以参考前一篇。
1.因为摄像头出来的是NV21
的数据,H265编码器需要的是NV12
,所以需要转换下,也就是Y不变UV交换一下。
fun nv21toNv12(nv21: ByteArray): ByteArray {
val size = nv21.size
val nv12 = ByteArray(size)
val y_len = size * 2 / 3
// Y
System.arraycopy(nv21, 0, nv12, 0, y_len)
var i = y_len
// nv12和nv21是奇偶交替
while (i < size - 1) {
nv12[i] = nv21[i + 1]
nv12[i + 1] = nv21[i]
i += 2
}
return nv12
}
2.上文提到了camera摄像头的预览需要旋转,只是预览画面进行旋转了,yuv的数据并没有旋转,所以yuv数据也需要旋转。
fun dataTo90(data: ByteArray, output: ByteArray, width: Int, height: Int) {
val y_len = width * height
// uv数据高为y数据高的一半
val uvHeight = height shr 1 // kotlin 的shr 1 就是右移1位 height >> 1
var k = 0
for (j in 0 until width) {
for (i in height - 1 downTo 0) {
output[k++] = data[width * i + j]
}
}
// uv
var j = 0
while (j < width) {
for (i in uvHeight - 1 downTo 0) {
output[k++] = data[y_len + width * i + j]
output[k++] = data[y_len + width * i + j + 1]
}
j += 2
}
}
H265硬编码
这个和H264的使用方法一样,唯一的区别就是创建MediaCodec
的时候指定是H265编码器。即MediaFormat.MIMETYPE_VIDEO_HEVC
(它的值是video/hevc
)
// H265编码器 video/hevc
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC)
具体的编码流程和H264的一样,没啥区别,这里就不多介绍了,可以参考前前面文章H264的编解码的介绍。Android音视频【四】H264硬编码
唯一要特别注意的是指定编码器的参数的时候,视频的宽和高的时候需要对调。因为后置摄像头旋转了90度,yuv数据也旋转了90度,也就是宽和高对调了。
WebSocket通信
WebSocket依赖添加如下
implementation "org.java-websocket:Java-WebSocket:1.4.0"
使用方法很简单,就是API的使用,内部实现感兴趣的可以研究下。
- WebSocketServer端
// 创建WebSocketServer
private val webSocketServer: WebSocketServer = object :
WebSocketServer(InetSocketAddress(PORT)) {
// ...省略其它代码
// 接收数据
override fun onMessage(conn: WebSocket, message: ByteBuffer) {
super.onMessage(conn, message)
if (h265ReceiveListener != null) {
val buf = ByteArray(message.remaining())
message[buf]
Log.d(TAG, "onMessage:" + buf.size)
h265ReceiveListener?.onReceive(buf)
}
}
}
// 发送数据
override fun sendData(bytes: ByteArray?) {
if (webSocket?.isOpen == true) {
webSocket?.send(bytes)
}
}
// 建立连接
override fun start() {
webSocketServer.start()
}
- WebSocketClient端
private inner class MyWebSocketClient(serverUri: URI) : WebSocketClient(serverUri) {
// 接收数据
override fun onMessage(bytes: ByteBuffer) {
if (h265ReceiveListener != null) {
val buf = ByteArray(bytes.remaining())
bytes.get(buf)
Log.i(TAG, "onMessage:" + buf.size)
h265ReceiveListener?.onReceive(buf)
}
}
}
发送数据和建立连接
// 发送数据
override fun sendData(bytes: ByteArray?) {
if (myWebSocketClient?.isOpen == true) {
myWebSocketClient?.send(bytes)
}
}
// 建立连接
private const val URL = "ws://172.24.92.58:$PORT"
override fun start() {
try {
val url = URI(URL)
myWebSocketClient = MyWebSocketClient(url)
myWebSocketClient?.connect()
} catch (e: Exception) {
e.printStackTrace()
}
}
这里就不多介绍了,都是API的使用,很简单。
H265硬解码
这个和H264的使用方法一样,这里就不多介绍了,可以参考前前面文章H264的编解码的介绍。唯一的区别就是创建MediaCodec
的时候指定是H265解码器。
// H265解码器
mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC)
怎么渲染到surface呢,在创建完解码器后进行配置阶段指定即可。
// 渲染到surface上
mediaCodec?.configure(mediaFormat, surface, null, 0)
mediaCodec?.start()
然后在解码完数据的时候,指定是否将h265解码后的数据渲染到configure配置阶段的surface上,true渲染,falsse不渲染。
// true渲染到surface上
mediaCodec!!.releaseOutputBuffer(outputBufferIndex, true)
VPS,SPS,PPS网络传输
Android中的硬编码器MediaCodec
首帧编码出来的是SPS,PPS等数据,在H265数据流中多了 VPS
。随后编码出来的是I帧,P帧,B帧
后续也不会回调出来VPS,SPS,PPS
等数据了。我们想一个问题就是:在网络传输怎么处理VPS,SPS,PPS
呢?,其实不止这个例子,所有的网络发送H264/H265
数据的时候都需要处理这个问题。
VPS(视频参数集),SPS(序列参数集),PPS(图像参数集)
- 是
VPS 、SPS、PPS
包含了在解码端(播放端)所用需要的profile,level,图像的宽和高。 - 发送端(直播端/主播)已经直播一小时了,有的用户播放端(用户端)才进入直播间,如果后续没有了
VPS 、SPS、PPS
那么解码怎么解码怎么渲染呢?对吧。
所以处理方法就是:缓存VPS,SPS,PPS
的数据,然后在发送每个关键帧(I帧)
前先发送VPS、SPS、PPS
的数据即可。这样后续进来的用户等下一个关键帧(I帧)
就会立刻看到画面了。
关键代码如下:
private fun dealFrame(byteBuffer: ByteBuffer) {
// H265的nalu的分割符的下一个字节的类型
var offset = 4
if (byteBuffer[2].toInt() == 0x1) {
offset = 3
}
// VPS,SPS,PPS... H265的nalu头是2个字节,中间的6位bit是nalu类型
// 0x7E的二进制的后8位是 0111 1110
// java版本
// int naluType = (byteBuffer.get(offset) & 0x7E) >> 1;
val naluType = byteBuffer[offset].and(0x7E).toInt().shr(1)
// 保存下VPS,SPS,PPS的数据
if (NAL_VPS == naluType) {
vps_sps_pps_buf = ByteArray(info.size)
byteBuffer.get(vps_sps_pps_buf!!)
} else if (NAL_I == naluType) {
// 因为是网络传输,所以在每个i帧之前先发送VPS,SPS,PPS
val bytes = ByteArray(info.size)
byteBuffer.get(bytes)
val newBuf = ByteArray(info.size + vps_sps_pps_buf!!.size)
System.arraycopy(vps_sps_pps_buf!!, 0, newBuf, 0, vps_sps_pps_buf!!.size)
System.arraycopy(bytes, 0, newBuf, vps_sps_pps_buf!!.size, bytes.size)
// 发送
h265DecodeListener?.onDecode(bytes)
} else {
// 其它bp帧数据
val bytes = ByteArray(info.size)
byteBuffer[bytes]
// 发送
h265DecodeListener?.onDecode(bytes)
}
}