Android AVDemo(7):视频采集,视频系列来了丨音视频工程示例

vx 搜索『gjzkeyframe』 关注『关键帧Keyframe』来及时获得最新的音视频技术文章。

塞尚《从埃斯塔克眺望马赛湾》 .jpeg

这个公众号会路线图 式的遍历分享音视频技术:音视频基础(完成) → 音视频工具(完成) → 音视频工程示例(进行中) → 音视频工业实战(准备)。

iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染过程,并借助音视频工具来分析和理解对应的音视频数据。

在音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。

这里是 Android 第七篇:Android 视频采集 Demo。这个 Demo 里包含以下内容:

  • 1)实现两个视频采集模块,分别为 CameraCamera2
  • 2)实现视频采集逻辑并将采集的视频图像渲染进行预览;
  • 3)详尽的代码注释,帮你理解代码逻辑和原理。

在本文中,我们将详解一下 Demo 的具体实现和源码。读完本文内容相信就能帮你掌握相关知识。

如果你想获得全部源码和参与音视频技术讨论,可以知识星球搜索『关键帧的音视频开发圈』加入我们,当然也可以跳过直接看后续的内容。

1、视频采集模块 Camera

首先,实现一个 KFVideoCaptureConfig 类用于定义视频采集参数的配置。

KFVideoCaptureConfig.java

public class KFVideoCaptureConfig {
    ///< 摄像头方向。
    public Integer cameraFacing = CameraCharacteristics.LENS_FACING_FRONT;
    ///< 分辨率。
    public Size resolution = new Size(1080,1920);
    ///< 帧率。
    public Integer fps = 30;
}

这里的参数包括了:分辨率、摄像头方向、帧率这几个参数。

接下来,我们实现一个 KFIVideoCapture 类来实现视频采集接口。

KFIVideoCapture.java

public interface KFIVideoCapture {
    ///< 视频采集初始化。
    public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
    ///< 释放采集实例。
    public void release();

    ///< 开始采集。
    public void startRunning();
    ///< 关闭采集。
    public void stopRunning();
    ///< 是否正在采集。
    public boolean isRunning();
    ///< 获取 OpenGL 上下文。
    public EGLContext getEGLContext();
    ///< 切换摄像头。
    public void switchCamera();
}

上面是 KFIVideoCapture 的接口设计,主要包含 初始化开始采集停止采集切换摄像头等接口。

下面是数据回调接口 KFVideoCaptureListener

KFVideoCaptureListener.java

public interface KFVideoCaptureListener {
    ///< 摄像机打开。
    void cameraOnOpened();
    ///< 摄像机关闭。
    void cameraOnClosed();
    ///< 摄像机出错。
    void cameraOnError(int error,String errorMsg);
    ///< 数据回调给外层。
    void onFrameAvailable(KFFrame frame);
}

提供了相机打开回调相机关闭回调、以及相机出错回调的接口。外层可以根据 相机打开回调优先将 CPU 等资源分配给相机,打开成功后执行 UI 等其它布局,提升用户体验。

接下来,我们实现一个 KFVideoCaptureV1 类来实现视频采集。

KFVideoCaptureV1.java

public class KFVideoCaptureV1 implements KFIVideoCapture {
    public static final int KFVideoCaptureV1CameraDisableError = -3000;
    private static final String TAG = "KFVideoCaptureV1";

    private KFVideoCaptureListener mListener = null; ///< 回调。
    private KFVideoCaptureConfig mConfig = null; ///< 配置。
    private WeakReference mContext = null;
    private boolean mCameraIsRunning = false; ///< 是否正在采集。

    private HandlerThread mCameraThread = null; ///< 采集线程。
    private Handler mCameraHandler = null;

    private KFGLContext mGLContext = null; ///< GL 特效上下文。
    private KFSurfaceTexture mSurfaceTexture = null; ///< Surface 纹理。
    private HandlerThread mRenderThread = null; ///< 渲染线程。
    private Handler mRenderHandler = null;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主线程。

    private Camera.CameraInfo mFrontCameraInfo = null; ///< 前置摄像头信息。
    private int mFrontCameraId = -1;
    private Camera.CameraInfo mBackCameraInfo = null; ///< 后置摄像头信息。
    private int mBackCameraId = -1;
    private Camera mCamera = null; ///< 当前摄像头实例(前置或者后置)。

    public KFVideoCaptureV1() {

    }

    @Override
    public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext) {
        mListener = listener;
        mConfig = config;
        mContext = new WeakReference(context);

        ///< 采集线程。
        mCameraThread = new HandlerThread("KFCameraThread");
        mCameraThread.start();
        mCameraHandler = new Handler((mCameraThread.getLooper()));

        ///< 渲染线程。
        mRenderThread = new HandlerThread("KFCameraRenderThread");
        mRenderThread.start();
        mRenderHandler = new Handler((mRenderThread.getLooper()));

        ///< OpenGL 上下文。
        mGLContext = new KFGLContext(eglShareContext);
    }

    @Override
    public EGLContext getEGLContext() {
        return mGLContext.getContext();
    }

    @Override
    public boolean isRunning() {
        return mCameraIsRunning;
    }

    @Override
    public void release() {
        mCameraHandler.post(() -> {
            ///< 停止视频采集 清晰视频采集实例、OpenGL 上下文、线程等。
            _stopRunning();
            mGLContext.bind();
            if (mSurfaceTexture != null) {
                mSurfaceTexture.release();
                mSurfaceTexture = null;
            }
            mGLContext.unbind();
            mGLContext.release();
            mGLContext = null;

            if (mCamera != null) {
                mCamera.release();
                mCamera = null;
            }

            mCameraThread.quit();
            mRenderThread.quit();
        });
    }

    @Override
    public void startRunning() {
        mCameraHandler.post(() -> {
            ///< 检测视频采集权限。
            if (ActivityCompat.checkSelfPermission(mContext.get(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions((Activity) mContext.get(), new String[] {Manifest.permission.CAMERA}, 1);
            }
            ///< 检测相机是否可用。
            if (!_checkCameraService()) {
                _callBackError(KFVideoCaptureV1CameraDisableError, "相机不可用");
                return;
            }

            ///< 开启视频采集。
            _startRunning();
        });
    }

    @Override
    public void stopRunning() {
        mCameraHandler.post(() -> {
            _stopRunning();
        });
    }

    @Override
    public void switchCamera() {
        mCameraHandler.post(() -> {
            ///< 切换摄像头,先关闭相机调整方向再打开相机。
            _stopRunning();
            mConfig.cameraFacing = mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT;
            _startRunning();
        });
    }

    private void _startRunning() {
        ///< 获取前后台摄像机信息。
        if (mFrontCameraInfo == null || mBackCameraInfo == null) {
            _initCameraInfo();
        }
        
        try {
            ///< 根据前后台摄像头 id 打开相机实例。
            mCamera = Camera.open(_getCurrentCameraId());
            if (mCamera != null) {
                ///< 设置相机各分辨率、帧率、方向。
                Camera.Parameters parameters = mCamera.getParameters();
                Size previewSize = _getOptimalSize(mConfig.resolution.getWidth(), mConfig.resolution.getHeight());
                mConfig.resolution = new Size(previewSize.getHeight(),previewSize.getWidth());
                parameters.setPreviewSize(previewSize.getWidth(),previewSize.getHeight());
                Range selectFpsRange = _chooseFpsRange();
                if (selectFpsRange.getUpper() > 0) {
                    parameters.setPreviewFpsRange(selectFpsRange.getLower(),selectFpsRange.getUpper());
                }
                mCamera.setParameters(parameters);
                mCamera.setDisplayOrientation(_getDisplayOrientation());
                ///< 创建 Surface 纹理。
                if (mSurfaceTexture == null) {
                    mGLContext.bind();
                    mSurfaceTexture = new KFSurfaceTexture(mSurfaceTextureListener);
                    mGLContext.unbind();
                }
                ///< 设置 SurfaceTexture 给 Camera,这样 Camera 自动将数据渲染到 SurfaceTexture。
                mCamera.setPreviewTexture(mSurfaceTexture.getSurfaceTexture());
                ///< 开启预览。
                mCamera.startPreview();
                mCameraIsRunning = true;
                if (mListener != null) {
                    mMainHandler.post(()->{
                        ///< 回调相机打开。
                        mListener.cameraOnOpened();
                    });
                }
            }
        } catch (RuntimeException | IOException e) {
            e.printStackTrace();
        }
    }

    private void _stopRunning() {
        if (mCamera != null) {
            ///< 关闭相机采集。
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
            mCameraIsRunning = false;
            if (mListener != null) {
                mMainHandler.post(()->{
                    ///< 回调相机关闭。
                    mListener.cameraOnClosed();
                });
            }
        }
    }

    private int _getCurrentCameraId() {
        ///< 获取当前摄像机 id。
        if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT) {
            return mFrontCameraId;
        } else if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_BACK) {
            return mBackCameraId;
        } else {
            throw new RuntimeException("No available camera id found.");
        }
    }

    private int _getDisplayOrientation() {
        ///< 获取摄像机需要旋转的方向。
        int orientation = 0;
        if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT) {
            orientation = (_getCurrentCameraInfo().orientation) % 360;
            orientation = (360 - orientation) % 360;
        } else {
            orientation = (_getCurrentCameraInfo().orientation + 360) % 360;
        }
        return orientation;
    }

    private Camera.CameraInfo _getCurrentCameraInfo() {
        ///< 获取当前摄像机描述信息。
        if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT) {
            return mFrontCameraInfo;
        } else if (mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_BACK) {
            return mBackCameraInfo;
        } else {
            throw new RuntimeException("No available camera id found.");
        }
    }

    private Size _getOptimalSize(int width, int height) {
        ///< 根据外层输入分辨率查找对应最合适的分辨率。
        List sizeMap = mCamera.getParameters().getSupportedPreviewSizes();
        List sizeList = new ArrayList<>();
        for (Camera.Size option:sizeMap) {
            if (width > height) {
                if (option.width >= width && option.height >= height) {
                    sizeList.add(new Size(option.width,option.height));
                }
            } else {
                if (option.width >= height && option.height >= width) {
                    sizeList.add(new Size(option.width,option.height));
                }
            }
        }
        if (sizeList.size() > 0) {
            return Collections.min(sizeList, new Comparator() {
                @Override
                public int compare(Size o1, Size o2) {
                    return Long.signum(o1.getWidth() * o1.getHeight() - o2.getWidth() * o2.getHeight());
                }
            });
        }

        return new Size(0,0);
    }

    private Range _chooseFpsRange() {
        ///< 根据外层设置帧率查找最合适的帧率。
        List fpsRange = mCamera.getParameters().getSupportedPreviewFpsRange();
        for (int[] range : fpsRange) {
            if (range.length == 2 && range[1] >= mConfig.fps*1000 && range[0] <= mConfig.fps*1000) {
                return new Range<>(range[0],range[1]);///< 仅支持列表中一项,不能像 camera2 一样指定
            }
        }

        return new Range(0,0);
    }

    private void _initCameraInfo() {
        ///< 获取前置后置摄像头描述信息与 id。
        int numberOfCameras = Camera.getNumberOfCameras();
        for (int cameraId = 0; cameraId < numberOfCameras; cameraId++) {
            Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
            Camera.getCameraInfo(cameraId, cameraInfo);
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                // 后置摄像头信息。
                mBackCameraId = cameraId;
                mBackCameraInfo = cameraInfo;
            } else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                // 前置摄像头信息。
                mFrontCameraId = cameraId;
                mFrontCameraInfo = cameraInfo;
            }
        }
    }

    private boolean _checkCameraService() {
        ///< 检测相机是否可用。
        DevicePolicyManager dpm = (DevicePolicyManager)mContext.get().getSystemService(Context.DEVICE_POLICY_SERVICE);
        if (dpm.getCameraDisabled(null)) {
            return false;
        }
        return true;
    }

    private void _callBackError(int error, String errorMsg) {
        ///< 错误回调。
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.cameraOnError(error,TAG + errorMsg);
            });
        }
    }

    private KFSurfaceTextureListener mSurfaceTextureListener = new KFSurfaceTextureListener() {
        @Override
        ///< SurfaceTexture 数据回调。
        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
            mRenderHandler.post(()->{
                long timestamp = System.nanoTime();
                mGLContext.bind();
                ///< 刷新纹理数据至 SurfaceTexture。
                mSurfaceTexture.getSurfaceTexture().updateTexImage();
                if (mListener != null) {
                    ///< 拼装好纹理数据返回给外层。
                    KFTextureFrame frame = new KFTextureFrame(mSurfaceTexture.getSurfaceTextureId(),mConfig.resolution,timestamp,true);
                    mSurfaceTexture.getSurfaceTexture().getTransformMatrix(frame.textureMatrix);
                    mListener.onFrameAvailable(frame);
                }
                mGLContext.unbind();
            });
        }
    };
}

上面是 KFVideoCaptureV1 的实现,实现了 KFIVideoCapture 接口,结合下面这张图可以让我们更好地理解这些代码:

[图片上传失败...(image-d481a6-1654655472589)]

可以看到在实现采集时,我们是用 mCamera 来管理相关接口,通过它控制视频采集开始、结束、设置参数、设置输出目标等。重点介绍一下设置输出目标 setPreviewTexture,采集器内部自己创建一个 SurfaceTexture[1],将其提供给采集器,这样的优点是后续处理特效、编码等其它操作比较灵活。

从代码上可以看到主要有这几个部分:

  • 1)初始化接口 setup
    • 初始化采集线程、渲染线程,子线程处理的好处是有效避免主线程卡顿。
    • 初始化 OpenGL 上下文,将数据输出到自定义纹理上 mSurfaceTexture
  • 2)创建采集设备与开启预览 startRunning
    • 检测视频采集权限 checkSelfPermission
    • 检测摄像头是否可用,_checkCameraService
    • 开启预览 _startRunning,首先获取前后台摄像机信息 mFrontCameraInfomFrontCameraIdmBackCameraInfomBackCameraId,通过 CameraId 打开摄像头,后续依次设置分辨率、帧率、方向、输出目标等参数。设置好后通过 startPreview 开启预览,数据则会自动同步到 mSurfaceTexture
  • 3)数据输出回调在 KFSurfaceTextureListeneronFrameAvailable
    • 通过 mSurfaceTextureupdateTexImage 将数据转换为自定义纹理。
    • 将纹理数据与纹理矩阵封装为 KFTextureFrame 通过回调 onFrameAvailable 输出给外层。
  • 4)实现切换摄像头的功能。
    • switchCamera 中实现,一共分三步,停止之前摄像头、修改摄像头标记位、开启新的摄像头。
  • 5)停止视频采集 stopRunning
    • 设置回调为空 setPreviewCallback
    • 停止预览 stopPreview
    • 释放摄像头 mCamera.release
  • 6)清理摄像机实例 release

更具体细节见上述代码及其注释。

2、视频采集模块 Camera2

接口类 KFIVideoCapture 与配置类 KFVideoCaptureConfig 与上面一致,这里不再介绍,我们直接分析 KFVideoCaptureV2,我们实现 2 套采集是因为 Camera2 功能更加强大(例如可以获取每帧的信息)以及性能更加高效,但它兼容性还不是很好,所以可以根据黑白名单或者跑分等策略选择合适的采集器。

KFVideoCaptureV2.java

public class KFVideoCaptureV2 implements KFIVideoCapture {
    public static final int KFVideoCaptureV2CameraDisableError = -3000;
    private static final String TAG = "KFVideoCaptureV2";
    private KFVideoCaptureListener mListener = null; ///< 回调。
    private KFVideoCaptureConfig mConfig = null; ///< 采集配置。
    private WeakReference mContext = null;

    private CameraManager mCameraManager = null; ///< 相机系统服务,用于管理和连接相机设备。
    private String mCameraId; ///<摄像头 id。
    private CameraDevice mCameraDevice = null; ///< 相机设备类。
    private HandlerThread mCameraThread = null; ///< 采集线程。
    private Handler mCameraHandler = null;
    private CaptureRequest.Builder mCaptureRequestBuilder = null; ///< CaptureRequest 的构造器,使用 Builder 模式,设置更加方便。
    private CaptureRequest mCaptureRequest = null; ///< 相机捕获图像的设置请求,包含传感器,镜头,闪光灯等。
    private CameraCaptureSession mCameraCaptureSession = null; ///< 请求抓取相机图像帧的会话,会话的建立主要会建立起一个通道,源端是相机,另一端是 Target。
    private boolean mCameraIsRunning = false;
    private Range[] mFpsRange;

    private KFGLContext mGLContext = null;
    private KFSurfaceTexture mSurfaceTexture = null;
    private Surface mSurface = null;
    private HandlerThread mRenderThread = null;
    private Handler mRenderHandler = null;
    private Handler mMainHandler = new Handler(Looper.getMainLooper());

    public KFVideoCaptureV2() {

    }

    @Override
    public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext) {
        mListener = listener;
        mConfig = config;
        mContext = new WeakReference(context);

        ///< 相机采集线程。
        mCameraThread = new HandlerThread("KFCameraThread");
        mCameraThread.start();
        mCameraHandler = new Handler((mCameraThread.getLooper()));

        ///< 渲染线程。
        mRenderThread = new HandlerThread("KFCameraRenderThread");
        mRenderThread.start();
        mRenderHandler = new Handler((mRenderThread.getLooper()));

        mGLContext = new KFGLContext(eglShareContext);
    }

    @Override
    public EGLContext getEGLContext() {
        return mGLContext.getContext();
    }

    @Override
    public boolean isRunning() {
        return mCameraIsRunning;
    }

    @Override
    public void startRunning() {
        ///< 开启预览。
        mCameraHandler.post(() -> {
            _startRunning();
        });
    }

    @Override
    public void stopRunning() {
        ///< 停止预览。
        mCameraHandler.post(() -> {
            _stopRunning();
        });
    }

    @Override
    public void release() {
        mCameraHandler.post(() -> {
            ///< 关闭采集、释放 SurfaceTexture、OpenGL 上下文、线程等。
            _stopRunning();
            mGLContext.bind();
            if (mSurfaceTexture != null) {
                mSurfaceTexture.release();
                mSurfaceTexture = null;
            }

            mGLContext.unbind();
            mGLContext.release();
            mGLContext = null;

            if (mSurface != null) {
                mSurface.release();
                mSurface = null;
            }

            mCameraThread.quit();
            mRenderThread.quit();
        });
    }

    @Override
    public void switchCamera() {
        ///< 切换摄像头。
        mCameraHandler.post(() -> {
            _stopRunning();
            mConfig.cameraFacing = mConfig.cameraFacing == CameraCharacteristics.LENS_FACING_FRONT ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT;
            _startRunning();
        });
    }

    private void _startRunning() {
        ///< 获取相机系统服务。
        if (mCameraManager == null) {
            mCameraManager = (CameraManager) mContext.get().getSystemService(Context.CAMERA_SERVICE);
        }
        ///< 根据外层摄像头方向查找摄像头 id。
        boolean selectSuccess = _chooseCamera();
        if (selectSuccess) {
            try {
                ///< 检测采集权限。
                if (ActivityCompat.checkSelfPermission(mContext.get(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions((Activity) mContext.get(), new String[] {Manifest.permission.CAMERA}, 1);
                }

                ///< 检测相机是否可用。
                if (!_checkCameraService()) {
                    _callBackError(KFVideoCaptureV2CameraDisableError,"相机不可用");
                    return;
                }

                ///< 打开相机设备。
                mCameraManager.openCamera(mCameraId, mStateCallback, mCameraHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private void _stopRunning() {
        ///< 停止采集。
        if (mCameraCaptureSession != null) {
            mCameraCaptureSession.close();
            mCameraCaptureSession = null;
        }

        if (mCameraDevice != null) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }

    private KFSurfaceTextureListener mSurfaceTextureListener = new KFSurfaceTextureListener() {
        @Override
        //< SurfaceTexture 数据回调。
        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
            mRenderHandler.post(() -> {
                long timestamp = System.nanoTime();
                mGLContext.bind();
                ///< 刷新纹理数据至 SurfaceTexture。
                mSurfaceTexture.getSurfaceTexture().updateTexImage();
                if (mListener != null) {
                    ///< 拼装好纹理数据返回给外层。
                    KFTextureFrame frame = new KFTextureFrame(mSurfaceTexture.getSurfaceTextureId(),mConfig.resolution,timestamp,true);
                    mSurfaceTexture.getSurfaceTexture().getTransformMatrix(frame.textureMatrix);
                    mListener.onFrameAvailable(frame);
                }
                mGLContext.unbind();
            });
        }
    };

    private CameraCaptureSession.StateCallback mCaputreSessionCallback = new CameraCaptureSession.StateCallback() {
        @Override
        ///< 创建会话回调。
        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
            ///< 创建CaptureRequest。
            mCaptureRequest = mCaptureRequestBuilder.build();
            mCameraCaptureSession = cameraCaptureSession;
            try {
                ///< 通过连续重复的 Capture 实现预览功能,每次 Capture 会把预览画面显示到对应的 Surface 上。
                mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        ///< 创建会话出错回调。
        public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
            _callBackError(1005,"onConfigureFailed");
        }
    };

    private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
        @Override
        ///< 相机打开回调。
        public void onOpened(@NonNull CameraDevice camera) {
            mCameraDevice = camera;
            try {
                ///< 通过相机设备创建构造器。
                mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                Range selectFpsRange = _chooseFpsRange();
                ///< 设置帧率。
                if (selectFpsRange.getUpper() > 0) {
                    mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,selectFpsRange);
                }
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }

            if (mListener != null) {
                mMainHandler.post(()->{
                    mListener.cameraOnOpened();
                });
            }
            mCameraIsRunning = true;

            if (mSurfaceTexture == null) {
                mGLContext.bind();
                mSurfaceTexture = new KFSurfaceTexture(mSurfaceTextureListener);
                mGLContext.unbind();
                mSurface = new Surface(mSurfaceTexture.getSurfaceTexture());
            }

            if (mSurface != null) {
                ///< 设置目标输出 Surface。
                mSurfaceTexture.getSurfaceTexture().setDefaultBufferSize(mConfig.resolution.getHeight(),mConfig.resolution.getWidth());
                mCaptureRequestBuilder.addTarget(mSurface);
                try {
                    ///< 创建通道会话。
                    mCameraDevice.createCaptureSession(Arrays.asList(mSurface), mCaputreSessionCallback, mCameraHandler);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            ///< 相机断开连接回调。
            camera.close();
            mCameraDevice = null;
            mCameraIsRunning = false;
        }

        @Override
        public void onClosed(@NonNull CameraDevice camera) {
            ///< 相机关闭回调。
            camera.close();
            mCameraDevice = null;
            if (mListener != null) {
                mMainHandler.post(()->{
                    mListener.cameraOnClosed();
                });
            }
            mCameraIsRunning = false;
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            ///< 相机出错回调。
            camera.close();
            mCameraDevice = null;
            _callBackError(error,"Camera onError");
            mCameraIsRunning = false;
        }
    };

    private boolean _chooseCamera() {
        try {
            ///< 根据外层配置方向选择合适的设备 id 与 FPS 区间。
            final String[] ids = mCameraManager.getCameraIdList();
            for (String cameraId : ids) {
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (facing == mConfig.cameraFacing) {
                    mCameraId = cameraId;
                    mFpsRange = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
                    StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                    if (map != null) {
                        Size previewSize = _getOptimalSize(map.getOutputSizes(SurfaceTexture.class), mConfig.resolution.getWidth(), mConfig.resolution.getHeight());
                        // Range[] fpsRanges = map.getHighSpeedVideoFpsRangesFor(previewSize); ///< high fps range
                        mConfig.resolution = new Size(previewSize.getHeight(),previewSize.getWidth());
                    }
                    return true;
                }
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        return false;
    }

    private Size _getOptimalSize(Size[] sizeMap, int width, int height) {
        ///< 根据外层配置分辨率寻找合适的分辨率。
        List sizeList = new ArrayList<>();
        for (Size option : sizeMap) {
            if (width > height) {
                if (option.getWidth() >= width && option.getHeight() >= height) {
                    sizeList.add(option);
                }
            } else {
                if (option.getWidth() >= height && option.getHeight() >= width) {
                    sizeList.add(option);
                }
            }
        }
        if (sizeList.size() > 0) {
            return Collections.min(sizeList, new Comparator() {
                @Override
                public int compare(Size o1, Size o2) {
                    return Long.signum(o1.getWidth() * o1.getHeight() - o2.getWidth() * o2.getHeight());
                }
            });
        }
        return sizeMap[0];
    }

    private boolean _checkCameraService() {
        ///< 检测相机是否可用。
        DevicePolicyManager dpm = (DevicePolicyManager)mContext.get().getSystemService(Context.DEVICE_POLICY_SERVICE);
        if (dpm.getCameraDisabled(null)) {
            return false;
        }
        return true;
    }

    private void _callBackError(int error, String errorMsg) {
        ///< 错误回调。
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.cameraOnError(error,TAG + errorMsg);
            });
        }
    }

    private Range _chooseFpsRange() {
        ///< 根据外层配置的帧率寻找合适的帧率。
        for (Range range : mFpsRange) {
            if (range.getUpper() >= mConfig.fps && range.getLower() <= mConfig.fps) {
                return new Range<>(range.getLower(),mConfig.fps);
            }
        }

        return new Range(0,0);
    }
}

上面是 KFVideoCaptureV2 的实现,实现了 KFIVideoCapture 接口,结合下面这张图可以让我们更好地理解这些代码:

[图片上传失败...(image-ba0eb6-1654655472591)]

从代码上可以看到与 Camera 区别如下:

  • 1)开启预览 _startRunning,流程如下。
    • 获取相机系统服务 mCameraManager,根据外层配置方向选择合适的设备id mCameraId
    • 打开相机设备 openCamera,通过回调 mStateCallback 监控相机状态。
    • 相机设备打开成功会执行 onOpened,创建 CaptureRequest 构造器 mCaptureRequestBuilder,通过构造器设置参数帧率,连接输出对象 mSurfacemSurface 根据 mSurfaceTexture 生成。通过 mSurfacemCaputreSessionCallback 回调创建图像帧会话 createCaptureSession
    • 图像帧会话打开成功会执行 onConfigured,通过连续重复的 Capture 实现预览功能,每次 Capture 会把预览画面显示到对应的 Surface 上。

更具体细节见上述代码及其注释。

3、采集视频并实时展示

我们在一个 MainActivity 中来实现视频采集并实时预览的逻辑。

MainActivity.java


public class MainActivity extends AppCompatActivity {

    private KFIVideoCapture mCapture; ///< 相机采集。
    private KFVideoCaptureConfig mCaptureConfig; ///< 相机采集配置。
    private KFRenderView mRenderView; ///< 渲染视图。
    private KFGLContext mGLContext; ///< OpenGL 上下文。


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ///< 检测采集相关权限。
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) this,
                    new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1);
        }

        ///< OpenGL 上下文。
        mGLContext = new KFGLContext(null);
        ///< 渲染视图。
        mRenderView = new KFRenderView(this,mGLContext.getContext());
        WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE);
        Rect outRect = new Rect();
        windowManager.getDefaultDisplay().getRectSize(outRect);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height());
        addContentView(mRenderView,params);

        ///< 采集配置:摄像头方向、分辨率、帧率。
        mCaptureConfig = new KFVideoCaptureConfig();
        mCaptureConfig.cameraFacing = LENS_FACING_FRONT;
        mCaptureConfig.resolution = new Size(720,1280);
        mCaptureConfig.fps = 30;
        boolean useCamera2 = false;
        if (useCamera2) {
            mCapture = new KFVideoCaptureV2();
        } else {
            mCapture = new KFVideoCaptureV1();
        }
        mCapture.setup(this,mCaptureConfig,mVideoCaptureListener,mGLContext.getContext());
        mCapture.startRunning();
    }

    private KFVideoCaptureListener mVideoCaptureListener = new KFVideoCaptureListener() {
        @Override
        ///< 相机打开回调。
        public void cameraOnOpened(){}

        @Override
        ///< 相机关闭回调。
        public void cameraOnClosed() {
        }

        @Override
        ///< 相机出错回调。
        public void cameraOnError(int error,String errorMsg) {

        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        ///< 相机数据回调。
        public void onFrameAvailable(KFFrame frame) {
            mRenderView.render((KFTextureFrame) frame);
        }
    };
}

上面是 MainActivity 的实现,主要分为以下几个部分:

  • 1)创建 OpenGL 上下文。
    • 创建上下文 mGLContext,这样好处是采集与预览可以共享,提高扩展性。
  • 2)创建采集实例。
    • 这里需要注意的是,我们通过开关 useCamera2 选择 CameraCamera2
    • 参数配置 mCaptureConfig,可自定义摄像头方向、帧率、分辨率。
  • 3)采集数据回调 onFrameAvailable,将数据输入给渲染视图进行预览,预览后续会介绍,如果希望将数据存储可以借助 ImageReader[2]。

更具体细节见上述代码及其注释。

参考资料

[1]SurfaceTexture: https://developer.android.com/reference/android/graphics/SurfaceTexture

[2]ImageReader: https://developer.android.com/reference/android/media/ImageReader

- 完 -

推荐阅读

《Android AVDemo(6):音频渲染》

《Android AVDemo(5):音频解码》

《Android AVDemo(4):音频解封装》

《Android AVDemo(3):音频封装》

《Android AVDemo(2):音频编码》

《Android AVDemo(1):音频采集》

《iOS AVDemo(7):视频采集》

《iOS 音频处理框架及重点 API 合集》

《iOS AVDemo(6):音频渲染》

《iOS AVDemo(5):音频解码》

《iOS AVDemo(4):音频解封装》

《iOS AVDemo(3):音频封装》

《iOS AVDemo(2):音频编码》

《iOS AVDemo(1):音频采集》

你可能感兴趣的:(Android AVDemo(7):视频采集,视频系列来了丨音视频工程示例)