当前位置:首页 > CN2资讯 > 正文内容

android encodeFrame 编码视频数据 旋转了90度 android视频编码解码

1天前CN2资讯


人间观察
我该如何去表达呢

前面介绍了H265的一些知识,本篇实现利用camera采集进行H265硬编码,利用WebSocket来传输H265裸流,接收到H265的码流后进行H265解码渲染到surfaceview上,从而实现简易的视频通话。

主要有:摄像头如何处理,如何拿到摄像头的yuv数据,yuv数据怎么处理,实现Android H265硬编码和硬解码,vps,sps,pps怎么处理以及如何在网络上传输。

1 .这里用哪种协议不是本文的重点,本文采用java封装好websocket协议的组件,在真实项目中音视频通话可能不用websocket协议,更多的可能是webrtc。

2.没有涉及到音频的编解码和发送传输,音频会后续出系列介绍

3.本篇也是用kotlin来实现,为什么用kotlin?因为工作中没有用到,我想自己练习下。。。

效果图

实现方案

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

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:///ta893115871/H265WithCameraWebSocket


      你可能想看:

      扫描二维码推送至手机访问。

      版权声明:本文由皇冠云发布,如需转载请注明出处。

      本文链接:https://www.idchg.com/info/19965.html

      分享给朋友:

      “android encodeFrame 编码视频数据 旋转了90度 android视频编码解码” 的相关文章

      如何通过 NameCheap 注册 $0.99 便宜域名并选择合适后缀

      在如今的网络世界,获取一个合适的域名可以说是非常关键的。对我来说,域名不仅是一个网站的门牌,更是品牌的第一印象。最近,NameCheap 推出了一个令人兴奋的优惠活动,注册域名低至 $0.99 每年,这绝对是个让人心动的机会。想到能够以这样的低价拥有一个域名,真的是让我忍不住想赶紧注册。 相信大家对...

      全面了解扩容:定义、分类及最佳实践

      扩容的定义与重要性 扩容这个词听起来似乎很简单,但它其实蕴含了很多技术细节和实际应用。简单来说,扩容就是对已有系统或设备的能力进行增强,尤其是在存储或处理能力上。想象一下,当你的业务正在快速增长,客户数量激增,原本的系统可能会面临压力,这时扩容就显得尤为重要。通过扩容,我可以在需要的时候增加更多的存...

      探索韩国VPS服务:选择高性能低延迟的虚拟专用服务器

      在数字化迅猛发展的今天,韩国的VPS(虚拟专用服务器)越来越受到用户的青睐。许多企业和个人用户都开始关注这个区域,特别是那些需要稳定网站和应用程序的人。这篇文章将为你深入探讨韩国VPS的市场需求和背景,以及它在不同场景中的适用性。 首先,韩国VPS市场的兴起与其优越的网络基础设施密不可分。韩国位于东...

      甲骨文云账号如何注销:详尽步骤与注意事项

      甲骨文云账号注销流程 注销甲骨文云账号的流程其实并不复杂,但有几个关键步骤需要认真对待。整个过程主要分为几个部分,包括登录甲骨文云控制台、发起注销请求、查看注销请求状态,以及最后的等待和确认删除。 1.1 甲骨文云控制台的登录 进入甲骨文云控制台的第一步,就是要登录到你的账号。打开浏览器,访问甲骨文...

      VPS论坛:虚拟主机爱好者的交流与学习平台

      VPS论坛概述 VPS论坛是一个专为VPS主机爱好者提供交流与分享的平台。在这里,像我这样对VPS感兴趣的人们,可以参与关于虚拟专用服务器的各种讨论。VPS实际上属于一个相对小众的领域,因此知名的VPS论坛数量较少,但它们所承载的信息和交流却是丰富多彩的。这些论坛不仅是获取信息的重要来源,更是与其他...

      连接测试地址的全面解析与故障排查技巧

      在网络管理和故障排查的过程中,连接测试地址扮演了重要角色。这些特定的IP地址并不指向任何实际的网络设备,因此在进行网络测试时,它们能够确保不会对现有的网络结构造成影响。简单来说,连接测试地址允许我们在不干扰现有设备的情况下,检查和验证网络的状态。 说到连接测试地址,我想起两个常见的:127.0.0....