首页 > 编程知识 正文

Android合并音视频,安卓音视频开发教程

时间:2023-05-06 00:06:46 阅读:109537 作者:1842

人类观察

我该怎么表达

前面介绍了一些关于H265的知识,本文利用camera采集进行H265硬编码,利用WebSocket传输H265裸流,接收到H265的码流后进行H265解码

主要包括:处理摄像机、获取摄像机yuv数据、处理yuv数据、实现Android H265的硬编码和硬解码、vps、sps、pps的处理方法和网络传输方法。

1 .这里使用什么协议不是本文的重点。 本文用java封装websocket协议的组件,在实际项目中音视频通话可能不用websocket协议。 很多可能是webrtc。

2 .没有涉及音频的编解码和传输。 音频将在系列中介绍

3 .本篇也是用kotlin实现的,为什么要用kotlin呢? 因为工作上没有用,所以想自己练习。

效果图

实现方案

Camera的YUV数据收集简单地说是Camera,本篇是带着Camera相机进行数据收集。 当然,也可以用camera2来实现。 camera2提供了更丰富的API (但我觉得真的很难使用。 拍照,获取原始YUV数据并写数百行代码)。 然后谷歌在jetpack上提供了。 各种照相机花两天研究就能完成,但现在的研究什么都没有。 主要介绍编解码器和yuv数据的处理,这些基本上保持不变,不像上层摄像机的api。

camera主要是打开camera设置预览屏幕的大小和回调的数据格式。 (默认为NV21格式的yuv数据,NV21格式的数据基本上所有摄像头都支持,因此Android默认采用该格式。 设置回调的数据大小。 一般设定为容易处理的是1帧的yuv数据的大小。 即yuv的数据大小=width * height width * height的1/4=width * height * 3/2。

本地代码如下所示。

fun startPreview () )//临时后置摄像头、 编解码器和数据传输camera=camera.open (camera.camera info.camera _ facing _ back ) val parameters 3360 camera.parameters=camers ' preview format : ' parameters.preview format (setpreviewsize ) parameters ) camera.setparameters ) camera //只是预览旋转了一下。 数据未旋转camera.setdisplayorientation (90 )//使相机回调一帧数据大小的buffer=bytearray (width * height *3/2) onPreviewFrame回调的数据大小为buffer.length camera.addcallbackbuffer (buffer ) camera.setpreviewcallbackwithbuffer ) this

如果打开并设置yuv数据回调,则会从onPreviewFrame回调中进行回调。

overridefunonpreviewframe (data : bytearray?camera: Camera? (//照相机的原始数据yuv camera! addcallbackbuffer(data ) ) YUV数据处理YUV相关知识请参考前篇。

1 .摄像机里出来的是NV21的数据,H265编码器需要的是NV12,所以在变换下,也就是y需要不变地进行UV交换。

funnv 21to nv12 (NV 21: bytearray ) : bytearray (valsize=nv21.sizevalnv 12=bytearray (size ) valy _ len=size * 2

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/hevcmediaCodec = 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的使用,很简单。

private const val URL = “ws://172.24.92.58:$PORT” 是另一台手机的ip地址 ,如果跑demo的话,自己改一下哦

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.get(bytes) // 发送 h265DecodeListener?.onDecode(bytes) } } 源码


https://github.com/ta893115871/H265WithCameraWebSocket

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。