音视频学习系列第(四)篇---视频的采集预览
音视频系列
如何进行视频的采集和预览
视频的采集需要用到Camera这个API,谷歌在5.0引入了camrea2,为了适配所有的机型,我将分别介绍camera和camera2
预览可以使用SurfaceView和TextureView
<uses-permission android:name="android.permission.CAMERA" />
采集
camera的基本使用
1.打开摄像头
mCamera=Camera.open(mCameraId);
mCameraId为int型,1代表前置摄像头,0代表后置摄像头
2.设置预览的媒介
mCamera.setPreviewDisplay(mHolder);
mCamera.setPreviewTexture(mTexture);
前者是SurfaceView预览,后者是TextureView预览
3.设置参数
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21); //预览数据格式
parameters.setPictureFormat(ImageFormat.JPEG); //设置拍照图片格式
parameters.setPreviewSize(mSize.width, mSize.height); //预览视频尺寸
parameters.setPictureSize(nSize.width, nSize.height); //拍照图片尺寸
4.设置预览方向和拍摄图片的方向
//设置拍照后图片的方向,否则方向不对if (mCameraId == 0) {parameters.setRotation(90); //后置 } else { parameters.setRotation(270); //前置 } mCamera.setParameters(parameters); mCamera.setDisplayOrientation(90); //预览角度默认0度,手机左侧为0
5.开启预览
mCamera.setPreviewCallback(this); //接收每一帧的数据mCamera.startPreview();
camera2的基本使用
camera2相较于camera发生了很大的变化
先上两张网上找来的图
图一说明
Android Device相当于我们的app
Camera Device相当于手机上的相机
app想要使用相机,就需要在app和相机之间建立一个连接
连接建立完成之后app就可以向相机发起数据请求
相机响应请求并向app返回数据
app得到数据之后可以通过各种surface处理数据
图二说明
图二介绍了连接过程中用到的关键类
CameraManager
负责管理所有的摄像头,如打开相机
CameraDevice.StateCallback
相机设备状态的回调,可以判断相机是否打开
CaptureRequest.Builder
通过Builder.build()创建一个CaptureRequest,如预览,拍照,录像等等
通过addTarget(Surface outputTarget)为对应的每一个CaptureRequest提供数据处理的地方,即数据着陆点
CameraCaptureSession.StateCallback
通过 mCameraDevice.createCaptureSession创建一次会话,到了这里app和相机才算了建立起了连接
camera2流程梳理
预览
1.通过CameraManager打开相机
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
mCameraManager.openCamera(String.valueOf(mCameraId),new CameraStateCallback(), mCameraHandler);
2.在CameraDevice.StateCallback的onOpened回调中拿到相机对象,同时创建一个预览的请求
@Override
public void onOpened(@NonNull CameraDevice camera) { mCameraDevice = camera; startPreview(); } CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); Surface surface = getSurface(); builder.addTarget(surface); mCaptureRequest = builder.build();
3.通过相机对象建立一个连接
mCameraDevice.createCaptureSession(Arrays.asList(surface, mPictureReader.getSurface()), new CameraSessionCallback(), mCameraHandler);
4.在连接建立成功的回调中发送之前创建的预览这个请求
public void onConfigured(@NonNull CameraCaptureSession session) { try { mCameraCaptureSession = session; //预览 mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, null); } catch (CameraAccessException e) { e.printStackTrace(); } }
至此整个预览就完成了
拍照
拍照相当于一个CaptureRequest,如果我们已经完成了预览的流程,对于拍照只需要新创建一个请求,然后通过之前创建的Session发送这个请求即可。
在camera2中一次会话可以包含多个请求,因此我们只需要创建一次会话即可
public void takePicture(String path) { mPicturePath = path; try { CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); builder.addTarget(mPictureReader.getSurface()); //拍照 mCameraCaptureSession.capture(builder.build(), null, null); } catch (CameraAccessException e) { e.printStackTrace(); } }
mPictureReader的写法
//2代表ImageReader中最多可以获取两帧图像流
mPictureReader = ImageReader.newInstance(mCaptureSize.getWidth(), mCaptureSize.getHeight(),ImageFormat.JPEG, 2);
mPictureReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {@Override public void onImageAvailable(ImageReader reader) { //将这帧数据转成字节数组,类似于Camera1的PreviewCallback回调的预览帧数据 savePicture(reader); } }, mCameraHandler);
由于我们设置的图片是JPEG格式,则在onImageAvailable回调中,直接可以将字节数组保存为jpg格式的图片
private void savePicture(ImageReader reader) { Image image = reader.acquireLatestImage(); ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] data = new byte[buffer.remaining()]; buffer.get(data); try { FileOutputStream fos = new FileOutputStream(mPicturePath); fos.write(data); fos.close(); Toast.makeText(mContext,"图片保存至:"+mPicturePath,Toast.LENGTH_SHORT).show(); } catch (FileNotFoundException e) { e.printStackTrace(); Log.d(TAG, "Camera2 FileNotFoundException"); } catch (IOException e) { e.printStackTrace(); Log.d(TAG, "Camera2 IOException"); } finally { image.close(); } }
摄像
摄像也相当于一个请求,我们同样可以创建一个摄像的请求,然后通过之前创建的session将这个请求发送出去
摄像和预览一样都是通过在CameraCaptureSession.StateCallback的回调中调用setRepeatingRequest方法
两者的区别
预览是将数据展示在SurfaceView或者TextureView上
摄像是将数据保存成一个视频文件
摄像数据的保存
通过ImageReader拿到原始的NV21数据
ImageReader.newInstance(width,height,ImageFormat.NV21, 1);
注意Camera2已经不支持NV21格式了,通过源码可以看出当使用该格式时,会抛异常
protected ImageReader(int width, int height, int format, int maxImages, long usage) { mWidth = width; mHeight = height; mFormat = format; mMaxImages = maxImages; if (width < 1 || height < 1) { throw new IllegalArgumentException( "The image dimensions must be positive"); } if (mMaxImages < 1) { throw new IllegalArgumentException( "Maximum outstanding image count must be at least 1"); } if (format == ImageFormat.NV21) { throw new IllegalArgumentException( "NV21 format is not supported"); } mNumPlanes = ImageUtils.getNumPlanesForFormat(mFormat); nativeInit(new WeakReference<>(this), width, height, format, maxImages, usage); mSurface = nativeGetSurface(); mIsReaderValid = true; // Estimate the native buffer allocation size and register it so it gets accounted for // during GC. Note that this doesn't include the buffers required by the buffer queue // itself and the buffers requested by the producer. // Only include memory for 1 buffer, since actually accounting for the memory used is // complex, and 1 buffer is enough for the VM to treat the ImageReader as being of some // size. mEstimatedNativeAllocBytes = ImageUtils.getEstimatedNativeAllocBytes( width, height, format, /*buffer count*/ 1); VMRuntime.getRuntime().registerNativeAllocation(mEstimatedNativeAllocBytes); }
解决办法,改为YUV_420_888格式
mPreviewReader = ImageReader.newInstance(1280, 720,ImageFormat.YUV_420_888, 1);
然后将YUV_420_888格式转化为NV21即可,具体转换方式可以百度
参考链接
通过MediaRecorder直接存储为经过编码压缩的文件
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
两者区别
前者更灵活,拿到原始数据之后可以做一些自己的处理
后者更简单,无需我们在编码和压缩了
封装
前面已经介绍了camera和camera2的用法,通过代码的练习,相信大家已经了解了两种camera的基本用法了
那么问题来了,我们怎样用这两种camera了
由于camera2是5.0中的api,为了兼容旧版手机,我们又不得不继续使用camera,但两者的用法却完全不一样
为此我封装了一个SofarCamera类,为camera和camera2的提供了统一的调用方法,只需要一个参数的区分,用户就可以自由切换要使用的是旧的camera还是5.0的camera2
同时在封装过程中,我将数据和UI界面分离开,读者可以自己设计UI界面,用的时候只需要传递SurfaceView的holder或者TextureView的texture即可
protected SurfaceHolder mHolder; //SurfaceView预览
protected SurfaceTexture mTexture; //TextureView预览
对外统一调用类SofarCamera
public class SofarCamera { public static final int CAMERA_FRONT=1; //前置摄像头 public static final int CAMERA_BACK=0; //后置摄像头 public static final int CAMERA1=1; //旧api public static final int CAMERA2=2; //新api private Context context; private int cameraId; private int cameraApi; private SurfaceHolder holder; private SurfaceTexture texture; private BaseCamera baseCamera; private SofarCamera(Builder builder){ this.context=builder.context; this.cameraId=builder.cameraId; this.cameraApi=builder.cameraApi; this.holder=builder.holder; this.texture=builder.texture; initCamera(); } private void initCamera(){ if(cameraApi==CAMERA1){ baseCamera=new Camera1(); }else if(cameraApi==CAMERA2){ baseCamera=new Camera2(); } baseCamera.setContext(context); baseCamera.setCameraId(cameraId); baseCamera.setDisplay(holder); baseCamera.setDisplay(texture); } public void openCamera(){ baseCamera.openCamera(); } public void destroyCamera(){ baseCamera.destroyCamera(); } public void switchCamera(){ if(cameraId==CAMERA_FRONT){ cameraId=CAMERA_BACK; }else { cameraId=CAMERA_FRONT; } destroyCamera(); initCamera(); openCamera(); } public void takePicture(String path){ baseCamera.takePicture(path); } public Builder newBuilder() { return new Builder(this); } public static final class Builder{ private Context context; private int cameraId; private int cameraApi; private SurfaceHolder holder; private SurfaceTexture texture; public Builder(){ } public Builder(SofarCamera camera){ this.cameraId=camera.cameraId; this.cameraApi=camera.cameraApi; this.holder=camera.holder; this.texture=camera.texture; } public Builder context(Context context){ this.context=context; return this; } public Builder cameraId(int cameraId){ this.cameraId=cameraId; return this; } public Builder cameraApi(@CameraApi int cameraApi){ this.cameraApi=cameraApi; return this; } public Builder holder(SurfaceHolder holder){ this.holder=holder; return this; } public Builder texture(SurfaceTexture texture){ this.texture=texture; return this; } public SofarCamera build() { return new SofarCamera(this); } } @IntDef({CAMERA1,CAMERA2}) @Retention(RetentionPolicy.SOURCE) public @interface CameraApi{ } }
Camera的抽象
对于Camera1和Camera2只需要继承该类,并实现必要的方法即可
public abstract class BaseCamera { private static final String TAG = "BaseCamera"; protected Context mContext; protected int mCameraId = 1; //1前置 0后置 protected SurfaceHolder mHolder; //SurfaceView预览 protected SurfaceTexture mTexture; //TextureView预览 protected String mPicturePath; //拍照后的图片存储路径 //SurfaceView预览 public void setDisplay(SurfaceHolder holder) { mHolder = holder; } //TextureView预览 public void setDisplay(SurfaceTexture texture) { mTexture = texture; } //设置前置或后置摄像头 public void setCameraId(int cameraId) { if (cameraId != 1 && cameraId != 0) { Log.d(TAG, "error cameraId:" + cameraId + " BaseCamera cameraId only support 0 or 1 "); return; } mCameraId = cameraId; } public void setContext(Context context) { mContext = context; } public abstract void openCamera(); public abstract void destroyCamera(); public abstract void takePicture(String path); }
调用方法
使用camera
mSofarCamera = new SofarCamera.Builder().context(this).cameraApi(SofarCamera.CAMERA1).cameraId(SofarCamera.CAMERA_BACK).holder(holder).build();
使用camera2
mSofarCamera = new SofarCamera.Builder().context(this).cameraApi(SofarCamera.CAMERA2).cameraId(SofarCamera.CAMERA_BACK).holder(holder).build();
最后的话
由于篇幅有限,Camera1和Camera2的代码我就不贴了
读者可以阅读前面的使用介绍或者一些其他资料,自己来实现它
我的实现已经放置在github上
https://github.com/hustersf/SofarMusic
Camera的封装放置在libplayer下的video包下
方法调用放置在app/demo/media/video下
转载于:https://www.cnblogs.com/Free-Thinker/p/10489381.html
音视频学习系列第(四)篇---视频的采集预览相关推荐
- AQS与CLH相关论文学习系列(四)- AQS的设计思路
本文是AQS与CLH相关论文学习系列第四篇. 系列其他文章链接如下 AQS与CLH相关论文学习系列(一)- 排队式自旋锁思想启蒙 AQS与CLH相关论文学习系列(二)- MCS 锁 AQS与CLH相关 ...
- Android音视频学习系列(五) — 掌握音频基础知识并使用AudioTrack、OpenSL ES渲染PCM数据
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(七) — 从0~1开发一款Android端播放器(支持多协议网络拉流本地文件)
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(八) — 基于Nginx搭建(rtmp、http)直播服务器
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(九) — Android端实现rtmp推流
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(六) — 掌握视频基础知识并使用OpenGL ES 2.0渲染YUV数据
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- Android音视频学习系列(十) — 基于FFmpeg + OpenSL ES实现音频万能播放器
系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...
- 互联网早报:抖音内测“学习”入口:加码知识视频内容创作
行业热点 1.抖音内测"学习"入口:加码知识视频内容创作: 2.百度上线"基木鱼电商"App,打造百度商家后台管理平台: 3.快播终结破产程序:破产财产分配完毕 ...
- UR机器人装箱姿态_UR10 RG2机械臂手臂+RealsenseZR300 机器人手眼标定 系列第四篇
UR10 RG2机械臂手臂+RealsenseZR300 机器人手眼标定 系列第四篇 发布时间:2018-09-18 17:43, 浏览次数:1180 , 标签: UR RG RealsenseZR ...
- 深入理解javascript作用域系列第四篇——块作用域
前面的话 尽管函数作用域是最常见的作用域单元,也是现行大多数javascript最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀.简洁的 ...
最新文章
- bos开发时,测试卡在登录界面解决
- 谈谈CListCtrl如何调整行高
- 【控制】《多智能体系统一致性协同演化控制理论与技术》纪良浩老师-第5章-多智能体系统双阶脉冲一致性
- $(MAKE) -C $(KERNELDIR) M=`pwd` modules
- Apache Flink 零基础入门(二):使用docker快速搭建Flink
- 猴子吃桃问题(南阳ACM324)
- nssl1143,jzoj3493-三角形【排序,数学,几何】
- Git—代码管理、提交及冲突解决流程的思考
- Installshield关于.NET安装时需要重启动的处理办法,以及延伸出的重启后继续安装的安装包的一点想法...
- 启动jar包报错: 找不到或无法加载主类
- MATLAB websave批量下载(URL)
- 9 个出色的 JavaScript 库推荐【云图智联】
- 「ZigBee模块」协议栈-串口透传,打造无线串口模块
- AE使用Keylight抠出人物身体教程-AE 人物抠像中文视频教程
- php 判断爬虫程序,php判断搜索引擎蜘蛛爬虫还是人为访问代码
- 安检机出彩色图及三点一线校准问题
- app审核被拒:App Tracking Transparency permission request when reviewed on iOS 15.0
- Win10系统里的软件有小盾牌有啥影响吗
- 学习笔记(01):C++基础入门21 精讲-01_C++基础课程的安排和需要持之以恒的学习态度...
- 《HFSS电磁仿真设计从入门到精通》一第1章 HFSS概述
热门文章
- 2020-11-16梦笔记
- 液晶显示器模糊的照片
- 反思:前一段时间的开发中,忽略了对象概念
- 订阅机票时要注意的几个教训
- 管理感悟:代码缺的不是注释,而是自解释
- java 原型模式的应用_java中原型模式详解和使用方法
- html画布arc,绘制弧线将线性渐变html5画布(Draw arc will linear gradient html5 canvas)
- 服务器修改文件后撮,xp系统的dns服务器修改办法.doc
- 编译安装mysql 不动了_编译安装MySQL5.6失败的相关问题解决方案
- php中文网是什么需要框架,框架是什么?