音视频开发路线:

Android 音视频开发入门指南_Jhuster的专栏的技术博客_51CTO博客_android 音视频开发入门

demo地址:

GitHub - wygsqsj/videoPath: 音视频学习路线demo

Camera采集数据

安卓摄像头采集得数据格式是NV21,NV21格式比较奇葩,安卓的MediaCodec编码也不支持,需要转换成NV12格式即YUVI420,他们两个的YUV排列顺序就是将UV调换了过来,NV21奇数位为V,偶数为U,NV12正好反过来:

NV21:

NV12:

无论哪种格式,都是一样的存储大小,一帧画面的大小计算公式,Y、U、V 每个都占一个byte,

  • Y = 宽* 高
  • U = 宽* 高*1/4
  • V = 宽* 高 * 1/4
  • YUV =宽* 高*3/2

也就是每一帧数据的存储大小就是 :宽* 高*3/2

当前就是普通的通过Camera1采集数据,并为Camera相机设置SurfaceView的预览:

mCamera = Camera.open(0);
if (mCamera == null) {throw new RuntimeException("摄像机打开失败!");
}
try {//设置camera数据回调mCamera.setPreviewCallback(this);mCamera.setDisplayOrientation(90);if (parameters == null) {parameters = mCamera.getParameters();}//获取默认的camera配置parameters = mCamera.getParameters();//设置预览格式parameters.setPreviewFormat(ImageFormat.NV21);//配置camera参数mCamera.setParameters(parameters);//将完全初始化的SurfaceHolder传入到setPreviewDisplay(SurfaceHolder)中//没有surface的话,相机不会开启preview预览mCamera.setPreviewDisplay(surfaceview.getHolder());//NV21一帧的大小,其实就是 宽*高*3/2callbackBuffer = new byte[width * height * 3 / 2];mCamera.addCallbackBuffer(callbackBuffer);mCamera.setPreviewCallback(this);mCamera.startPreview();
} catch (IOException e) {e.printStackTrace();
}

从SurfaceView的回调中获取到yuv数据,放到一个队列中,供我们的编码线程获取:

 protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_demo6);surfaceview = findViewById(R.id.demo6Surface);surfaceview.getHolder().addCallback(this);}//摄像头获取到的yuv数据回调@Overridepublic void onPreviewFrame(byte[] data, Camera camera) {//将当前帧图像保存在队列中putYUVData(data, data.length);}@Overridepublic void surfaceCreated(SurfaceHolder holder) {initBackCamera();}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {if (null != mCamera) {mCamera.setPreviewCallback(null);mCamera.stopPreview();mCamera.release();mCamera = null;}if (encodeThread != null) {encodeThread.stopEncode();}if (encodeMuxerThread != null) {encodeMuxerThread.stopEncode();}}public void putYUVData(byte[] buffer, int length) {
//        Log.i(LOG_TAG, "获取到摄像头数据" + length);if (YUVQueue.size() >= 10) {YUVQueue.poll();}YUVQueue.add(buffer);}

旋转YUV90度得到竖屏

摄像头获取摄像头数据是以横屏时的左上角为原点获取的,也就是我们摄像头捕捉的原始数据时横屏的,这也是我们为什么在初始化Camera时要把预览画面旋转90度的原因,先来看看未旋转时的图像:

而设置旋转90度后的图像:

未旋转时即便我们时竖屏下拍照,摄像头仍旧是捕捉的横屏画面,所以得到的也是横屏的照片,这时候我们按顺时针旋转90度即可得到竖屏的数据:

横屏转竖屏就是将YUV数据顺时针旋转90度即可得到:

/*** 旋转yuv数据,将横屏数据转换为竖屏*/
private byte[] revolveYuv(byte[] yuvData) {byte[] revolveData = new byte[yuvData.length];int y_size = width * height;//uv高度int uv_height = height >> 1;//旋转y,左上角跑到右上角,左下角跑到左上角,从左下角开始遍历int k = 0;for (int i = 0; i < width; i++) {for (int j = height - 1; j > -1; j--) {revolveData[k++] = yuvData[width * j + i];}}//旋转uvfor (int i = 0; i < width; i += 2) {for (int j = uv_height - 1; j > -1; j--) {revolveData[k++] = yuvData[y_size + width * j + i];revolveData[k++] = yuvData[y_size + width * j + i + 1];}}return revolveData;
}

IO流方式对YUV数据进行编码

通过FileOutStream来将编码好的数据写入到h264文件中,此中方式写出来来的264文件播放时没有时间戳标识,而且文件比较大,此处应该还有没有照顾到的点,但是可以写出264文件且进行播放代码如下:

1.配置MediaFormat,构建编码MediaCodec,注意我们配置宽高的时候,要按竖屏的宽高来配置

//构建对应的MeidaFormat,后期我们会将摄像头数据旋转90读成为竖屏,所以此处调换一下宽高
MediaFormat mediaFormat = MediaFormat.createVideoFormat(encodeMine, height, width);
//设置yuv格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//比特率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
//描述视频格式的帧速率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
//关键帧之间的间隔,此处指定为1秒
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//构建编码h264MediaCodec
encodeCodec = MediaCodec.createEncoderByType(encodeMine);
encodeCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解码得到的byte[]数据的相关信息
//启动编码器
encodeCodec.start();

比特率:可以理解成264文件的码率,即时间单位中编码所占的字节数,越高代表画质越好,记录下来的细节越多,但同样的文件也就越大,他的单位是kbps,算法是 文件大小M/时间秒 得出来

帧率:用FPS表示,用于表示一秒有多少帧,与画面得流畅度相关,如果帧数越多,画面衔接越好,同样帧数越多文件越大

2.获取YUV数据,放到编码器中进行编码

Camera1获取得数据格式是NV21,需要转换成MediaCodec可以操作得NV12格式才可以传给编码器进行操作

//获取当前的yuv数据
if (!demo6Activity.getYUVQueue().isEmpty()) {//从猪肉工厂获取装猪的小推车,填充数据后发送到猪肉工厂进行处理ByteBuffer[] inputBuffers = encodeCodec.getInputBuffers();//所有的小推车int inputIndex = encodeCodec.dequeueInputBuffer(0);//返回当前可用的小推车标号if (inputIndex != -1) {Log.i(LOG_TAG, "找到了input 小推车" + inputIndex);ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到小推车inputBuffer.clear();//扔出去里面旧的东西//从yuv队列中取出数据byte[] yuvData = demo6Activity.getYUVQueue().poll();//摄像头默认是横着的,需要旋转90度byte[] revolveData = revolveYuv(yuvData);byte[] yuv420sp = new byte[width * height * 3 / 2];//把待编码的视频帧转换为YUV420格式NV21ToNV12(revolveData, yuv420sp, height, width);//audioExtractor没猪了,也要告知一下if (revolveData.length < 0) {Log.i(LOG_TAG, "当前yuv数据异常");} else {//拿到猪//把转换后的YUV420格式的视频帧放到编码器输入缓冲区中Log.i(LOG_TAG, "yuv数据转换成功,当前数据的数据长度为:" + yuvData.length);inputBuffer.limit(revolveData.length);inputBuffer.put(revolveData, 0, yuvData.length);//计算时间戳,配置了但是没有用,所以此处传0也可以pts = computePresentationTime(generateIndex);Log.i(LOG_TAG, "当前时间戳:" + pts);encodeCodec.queueInputBuffer(inputIndex, 0, revolveData.length, pts, 0);generateIndex += 1;}} else {Log.i(LOG_TAG, "没有可用的input 小推车");}
}//在使用Camera的时候,设置预览的数据格式为NV21,转换成nv12
private byte[] NV21ToNV12(byte[] nv21, byte[] nv12, int width, int height) {if (nv21 == null || nv12 == null) return null;int framesize = width * height;int i = 0, j = 0;System.arraycopy(nv21, 0, nv12, 0, framesize);for (i = 0; i < framesize; i++) {nv12[i] = nv21[i];}for (j = 0; j < framesize / 2; j += 2) {nv12[framesize + j - 1] = nv21[j + framesize];}for (j = 0; j < framesize / 2; j += 2) {nv12[framesize + j] = nv21[j + framesize - 1];}return nv12;
}

3.将编码好得数据写入到h264文件中

SPS和PPS等配置信息是放在I帧前面得,如果读取到这些配置信息,需要先将这些信息存储下来,等到I帧编码出来,为每个I帧前面添加上这些配置信息,这样在另一端接受时就可以从任何一个I帧前面找到这些配置信息来配置播放器

int outputIndex = encodeCodec.dequeueOutputBuffer(encodeBufferInfo, 10000);//返回当前筐的标记
switch (outputIndex) {case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:Log.i(LOG_TAG, "输出的format已更改" + encodeCodec.getOutputFormat());break;case MediaCodec.INFO_TRY_AGAIN_LATER:Log.i(LOG_TAG, "超时,没获取到");break;case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:Log.i(LOG_TAG, "输出缓冲区已更改");break;default:Log.i(LOG_TAG, "获取到编码后的数据了,当前解析后的数据长度为:" + encodeBufferInfo.size);//获取所有的筐ByteBuffer[] outputBuffers = encodeCodec.getOutputBuffers();//拿到当前装满火腿肠的筐ByteBuffer outputBuffer;if (Build.VERSION.SDK_INT >= 21) {outputBuffer = encodeCodec.getOutputBuffer(outputIndex);} else {outputBuffer = outputBuffers[outputIndex];}//将数据读取到outData中byte[] outData = new byte[encodeBufferInfo.size];outputBuffer.get(outData);//当前是初始化编解码器数据,不是媒体数据,sps、pps等初始化数据if (encodeBufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG) {Log.i(LOG_TAG, "配置信息编码完成");configByte = new byte[encodeBufferInfo.size];configByte = outData;} else if (encodeBufferInfo.flags == BUFFER_FLAG_KEY_FRAME) {//当前的数据中包含关键帧数据Log.i(LOG_TAG, "I帧编码完成");//将初始化数据和当前的关键帧数据合并后写入到h264文件中byte[] keyframe = new byte[encodeBufferInfo.size + configByte.length];System.arraycopy(configByte, 0, keyframe, 0, configByte.length);//把编码后的视频帧从编码器输出缓冲区中拷贝出来System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);fos.write(keyframe, 0, keyframe.length);} else {//写到文件中Log.i(LOG_TAG, "非I帧数据编码完成");fos.write(outData, 0, outData.length);}//把筐放回工厂里面encodeCodec.releaseOutputBuffer(outputIndex, false);break;
}if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {Log.i(LOG_TAG, "表示当前编解码已经完事了");inOutFinish = true;
}

最后把SD卡中保存得264文件通过专门的工具播放出来,此处使用的时雷霄骅大神的VideoEye

通过MediaMuter输出到h264文件

上面我们通过IO自己将编码好的264码流写入文件中,我们还可以通过MediaMuter来帮我们写入

out264File = new File(demo6Activity.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "cameraMuxer.h264");
out264File.createNewFile();
vedioMuxer = new MediaMuxer(out264File.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);......int outputIndex = encodeCodec.dequeueOutputBuffer(encodeBufferInfo, 10000);//返回当前筐的标记
switch (outputIndex) {case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:Log.i(LOG_TAG, "输出的format已更改" + encodeCodec.getOutputFormat());videoTrackIndex = vedioMuxer.addTrack(encodeCodec.getOutputFormat());vedioMuxer.start();//开始合成audiobreak;case MediaCodec.INFO_TRY_AGAIN_LATER:Log.i(LOG_TAG, "超时,没获取到");break;case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:Log.i(LOG_TAG, "输出缓冲区已更改");break;default:Log.i(LOG_TAG, "获取到编码后的数据了,当前解析后的数据长度为:" + encodeBufferInfo.size);//获取所有的筐ByteBuffer[] outputBuffers = encodeCodec.getOutputBuffers();//拿到当前装满火腿肠的筐ByteBuffer outputBuffer;if (Build.VERSION.SDK_INT >= 21) {outputBuffer = encodeCodec.getOutputBuffer(outputIndex);} else {outputBuffer = outputBuffers[outputIndex];}//将数据读取到outData中byte[] outData = new byte[encodeBufferInfo.size];outputBuffer.get(outData);//当前是初始化编解码器数据,不是媒体数据,sps、pps等初始化数据if (encodeBufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG) {configByte = new byte[encodeBufferInfo.size];configByte = outData;} else if (encodeBufferInfo.flags == BUFFER_FLAG_KEY_FRAME) {//当前的数据中包含关键帧数据//将初始化数据和当前的关键帧数据合并后写入到h264文件中byte[] keyframe = new byte[encodeBufferInfo.size + configByte.length];System.arraycopy(configByte, 0, keyframe, 0, configByte.length);//把编码后的视频帧从编码器输出缓冲区中拷贝出来System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);ByteBuffer newBuffer = ByteBuffer.allocate(keyframe.length);newBuffer.put(keyframe);videoBufferInfo.size = keyframe.length;videoBufferInfo.presentationTimeUs = (System.nanoTime() - startTime) / 1000;videoBufferInfo.offset = 0;videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;//通过MediaMuxer写入vedioMuxer.writeSampleData(videoTrackIndex, newBuffer, videoBufferInfo);} else {//写到文件中videoBufferInfo.size = encodeBufferInfo.size;videoBufferInfo.presentationTimeUs = (System.nanoTime() - startTime) / 1000;videoBufferInfo.offset = 0;videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;//通过MediaMuxer写入vedioMuxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferInfo);}//把筐放回工厂里面encodeCodec.releaseOutputBuffer(outputIndex, false);break;
}
if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {Log.i(LOG_TAG, "表示当前编解码已经完事了");inOutFinish = true;
}

通过MediaMuter输出的264文件各项数据都时非常正常的,而通过io流写出的数据则从文件大小,时间戳,比特率方面都有一些问题,暂时还没有去研究具体的原因,可以放到后面再深入研究一些

音视频6.2——相机采集数据编码成H264相关推荐

  1. 【Android音视频开发】- 实时采集视频

    前言 通过我的上一篇文章,可以知道直播大致有几个步骤:音视频采集 -> 美颜/滤镜/特效处理 -> 编码 -> 封包 -> 推流 -> 分发 -> 解码/渲染/播放 ...

  2. Android 音视频入门之音频采集、编码、播放

    今天我们学习音频的采集.编码.生成文件.转码等操作,我们生成三种格式的文件格式,pcm.wav.aac 三种格式,并且我们用 AudioStack 来播放音频,最后我们播放这个音频. 本篇文章你将学到 ...

  3. 研究Android音视频-3-在Android设备上采集音视频并使用MediaCodec编码为H.264

    原文 : https://juejin.cn/post/69601302052266311754 本文解决的问题 本文主要使用 MediaCodec 硬编码器对 Android 设备采集的音视频编码 ...

  4. Android音视频【三】硬解码播放H264

    人间观察 穷人家的孩子真的是在社会上瞎混 遥远的2020年马上就过去了,天呐!!! 前两篇介绍了下H264的知识和码流结构,本篇就拿上篇从抖音/快手抽离的h264文件实现在Android中进行解码播放 ...

  5. FFMPEG音视频同步-音频实时采集编码封装成MP4

    https://blog.csdn.net/quange_style/article/details/90083173

  6. Android IOS WebRTC 音视频开发总结(七一)-- H265/H264有何不同

    本文整理自自网络,非原创,喜欢相关文章请关注我们的微信公众号:blackerteam H.265 H.265是ITU-TVCEG继H.264之后所制定的新的视频编码标准.H.265标准围绕着现有的视频 ...

  7. linux下ffmpeg采集音视频设备

    Linux下查看音视频设备 1.视频输入设备: 命令查看:ls /sys/class/video4linux/ 执行结果如下: 如果系统存在video4linux文件夹说明已安装过视频驱动,video ...

  8. (零)音视频技术基础知识,现实项目

    前言 耽误了很久,一直想写音视频开发的教程,一方面,音视频的发展正在向各个行业扩展,从教育的远程授课,交通的人脸识别,医疗的远程就医等,音视频方向已经占据一个相当重要的位置,而音视频真正入门的文章又少 ...

  9. (强烈推荐)移动端音视频从零到上手(转载)

    移动端音视频从零到上手 原文链接 概述 随着整个互联网的崛起,数据传递的形式也在不断升级变化,总的流行趋势如下: 纯文本的短信,QQ -> 空间,微博,朋友圈的图片文字结合 -> 微信语音 ...

最新文章

  1. java 网络编程UDP
  2. java多线程解读一(基础篇)
  3. C#项目开发系统开发进度-第X组-xxx(简单)
  4. JAVA构架之并发编程的一些总结
  5. java并发集合面试题,那些经常被问的JAVA面试题(1)—— 集合部分
  6. IE下的一个安全BUG —— 可实时跟踪系统鼠标位置
  7. 基于RFM模型的用户价值的数据分析报告
  8. [wpa_supplicant]基于ubuntu的wpa_supplicant工具的安装与使用
  9. FPGA vs ASIC
  10. Alpha Fold 2
  11. win10系统怎么安装ie11
  12. ubuntu安装搜狗输入法老是没中文怎么办
  13. 更新后的哥德巴赫猜想(位运算)
  14. 【Mac】电脑维修 换电池之被坑记
  15. 什么是伪静态?网站伪静态有什么作用?
  16. 北大核心,sci和核心期刊,文章几区,文章下载
  17. ssm毕设项目基于框架的众筹管理系统f5244(java+VUE+Mybatis+Maven+Mysql+sprnig)
  18. 如何使用Python渲染动画
  19. Windows Bat批处理技巧之管理员权限运行
  20. Ndd v2.0 发布,终于等到了

热门文章

  1. 轻松上手,设计无忧,三款在线图片编辑器必不可少
  2. maven profiles勾选不了
  3. String转JSONObject,JSONObject转JSONArray,JSONArray数组转换成JSON字符串
  4. 8.磁盘存储器的管理
  5. 在校园网中进行无线路由器设置
  6. 溴PEG溴,Br-PEG-Br
  7. uni-app项目配置UrlSchemes在外部打开APP
  8. mac无法验证此app不包含恶意软件
  9. 谨慎使用达梦manger工具
  10. How to build openssl with fips module on Linux and Windows?