camera2对摄像头编码h264

MediaCodec编码摄像头数据

前置:保存的一些成员变量

    // 摄像头开启的 handler
    private Handler cameraHandler;
    // Camera session 会话 handler
    private Handler sessionHandler;
    //这里是个Context都行
    private AppCompatActivity mActivity;
    // 这个摄像头所有需要显示的 Surface,以TextureView创建的Surface在一开始就已经加进去了,如果用SurfaceView的话,注意管理好SurfaceView因为onstop,onStart创建和销毁的Surface
    private List<Surface> mViewSurfaces;
    private CameraManager cameraManager;

    private String mCameraId;
    private CameraDevice mDevice;
    //Camera 配置信息
    private CameraCharacteristics mCharacteristics;
    private CameraCaptureSession mSession;

    // 编码输入的Surface,由MediaCodec创建
    private Surface mStreamSurface;
    // 摄像头选择的输出大小
    private Size mSize;

    //编码器
    private MediaCodec mMediaCodec;

一、选择要打开的摄像头,并保存对应配置参数。

  1. 获取打开的摄像头 ID,以及获取对应参数配置.
        try {
            String[] cameraIdList = cameraManager.getCameraIdList();
            // 遍历摄像头,获取第一个可用的摄像头,多个摄像头暂不播放
            for (String id : cameraIdList) {
                CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(id);
                // 获取该摄像头的说明数据
                int[] ints = cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
                if (ints != null) {
                    Arrays.stream(ints).filter(value -> value == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE).close();
                    if (ints.length == 0) {
                        Log.d(TAG, "the camera has not basic supports.");
                        continue;
                    }
                    mCameraId = id;
                    mCharacteristics = cameraCharacteristics;
                    // 由于目前无具体需求,使用第一个可连接的摄像头进行显示,选完就返回,有需要的获取配置参数自行判定
                    break;
                }
            }
        } catch (CameraAccessException e) {
            throw new RuntimeException(e);
        }
说明:CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE 只是为了确定这个 Camera可用而已。
  1. 获取摄像头的所有的输出尺寸,选一个合适的
        //获取配置参数
        StreamConfigurationMap configurationMap = mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        //拿到对应 ImageFormat.YUV_420_888 所有可输出的尺寸,一般比较多,1280x720,1920x1080等等一大堆,传参有点重要下面说明会说.
        Stream<Size> stream = Arrays.stream(configurationMap.getOutputSizes(ImageFormat.YUV_420_888));
        //这里我选了宽高相乘最大的
        Optional<Size> max = stream
                .max(Comparator.comparingInt(item -> item.getWidth() * item.getHeight()));
        stream.close();
        //把输出尺寸保存起来,后面创建编码器要用,其实就是视频的分辨率
        max.ifPresent(value -> mSize = value);
1. 比较重要的参数configurationMap.getOutputSizes(ImageFormat.YUV_420_888)传参为什么是ImageFormat.YUV_420_888说明 比较重要的是,这个值不是随便设置的,是通过获取设备参数之后选的,只要是设备参数里有的都能用,但是没有的值,用了可是会出错的。

获取Camera图像格式的方法:int[] outputFormats = configurationMap.getOutputFormats();

遍历打印一下,选一个就行,但是尽量按照参数说明搭配需求去选。

二、创建编码器

  1. 通过步骤一拿到的输出尺寸,配置编码格式.(只设置了必要的几个参数,其他的自选加入)
        //创建编码格式
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mSize.getWidth(), mSize.getHeight());
        //设置码率
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, (int) (mSize.getHeight() * mSize.getWidth() * 0.2));
        //设置帧率60
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
        //设置颜色格式,因为这里用的自动编码,就不用yuv420自己手动编了,直接用surface
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        //设置关键帧产生速度 1,单位是(帧/秒)
        mediaFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, 1f);
        //摄像头编码角度会出现90度的偏差,设置正向旋转90显示正常效果,你要喜欢歪头90看其实也行
        mediaFormat.setInteger(MediaFormat.KEY_ROTATION, 90);

        Log.d(TAG, "MediaCodec format: " + mediaFormat);

说明:
(1) 编码格式创建中,video/avc 表示编码为 h264 编码,编码其实就是为了降低视频大小.

(2) 码率设置中,后面的乘以0.2只是为了降低码率,不然太清晰了。。。正常的大多数是乘以5,这样看着清晰多,当然大小也会增加.

(3) 帧率设置为24以上就行,不然就是PPT。

(4)格式设置为自动编码,手动的话可以设置MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible,具体看需求。自动编码只能获得编码后的h264,手动可以做个ImageReader,从里面读取可输出为图片的byte[],就可以添加什么特效啊,贴图这些,这个Demo不需要这些所以就走自动模式了。就是会比较慢,更好的方式没去看。

(5) 关键帧产生速度SDK25之后可以设为float类型,关键帧就是 i 帧,设置之后只是说尽量按这个速度产生,肯定不会一模一样的,0或者负数就是希望每帧都是关键帧

  1. 创建编码器
        try {
            //按照“video/avc”创建编码器,硬编码是基于硬件的,厂商不同会有不一样的硬编码实现,编码是Encode,别用成Decode解码器
            mMediaCodec = MediaCodec.createEncoderByType("video/avc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //配置编码器,MediaCodec状态机就不说了,这里只提使用。参数4也是说明是编码器,和创建时不一致也会报错
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //创建输入Surface,和MediaFormat设置的KEY_COLOR_FORMAT参数是有联系的,format那里设错了就不能用自动挡的surface,且createInputSurface()方法必须在configure()之后,start()之前调用
        mStreamSurface = mMediaCodec.createInputSurface();
        //开启编码
        mMediaCodec.start();

说明:

(1) MediaCodec.createEncoderByType(“video/avc”),创建编码器,MediaCodec.createDecoderByType(“video/avc”)是创建解码器的。byName那个的话想用得先获取硬件已实现的编码器的名字才能调,虽然不麻烦,但是懒得敲,而且因为硬件厂商实现不同,兼容性还得考虑。


(2) configure()很容易就报错,参数4编解码值是不一样的,以及MediaFormat的设置,如果和摄像头、手机不支持也会报错,所以前面写的那些代码就是为了拿的,不然随便写一个就行了。


(3) MediaCodec.createInputSuface()方法必须在 configure()配置编码之后,start()开始编码之前调用,点进实现也有说明,出现其实也挺好解决,你也可以用mMediaCodec.setInputSurface(创建的常显Surface)去用自己的Surface,没有常显Surface的话可以静态方法MediaCodec.createPersistentInputSurface()创建一个常显Surface.


(4) 在 start()开启编码之后,MediaFormat参数格式其实是可以改的,只是需要用Bundle,通过key-value的方式设值,然后mMediaCodec.setParameters(bundle)去动态设置。

三、操作摄像头

  1. 打开摄像头预览

摄像头 ID之前已经拿过了,所以直接open就行了。

cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
            @Override
            public void onOpened(@NonNull CameraDevice camera) {
                Log.d(TAG, "onOpened, camera: " + camera);
                mDevice = camera;

                // Camera 打开之后,把创建的解码器 Surface 加入显示队列中
                addSurface(mStreamSurface);

                //创建请求
                CaptureRequest request = createCaptureRequest(mDevice, mViewSurfaces);
                //创建输出配置项
                List<OutputConfiguration> outputConfigurations = createOutputConfig(mViewSurfaces);

                try {
                    SessionConfiguration config = new SessionConfiguration(SessionConfiguration.SESSION_REGULAR,
                            outputConfigurations,
                            new ThreadPoolExecutor(3, 5, 15, TimeUnit.SECONDS, new ArrayBlockingQueue<>(25)),
                            new CameraCaptureSession.StateCallback() {
                                @Override
                                public void onConfigured(@NonNull CameraCaptureSession session) {
                                    mSession = session;
                                    try {
                                        //到这个回调,摄像头就可以正常预览了,编码器也会输出编码数据到输出队列了
                                        mSession.setRepeatingRequest(request, null, sessionHandler);
                                    } catch (CameraAccessException e) {
                                        throw new RuntimeException(e);
                                    }
                                }

                                @Override
                                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                                    Log.e(TAG, "onConfigureFailed, camera: " + session);
                                    if (mSession != null) mSession.close();
                                    if (mDevice != null) mDevice.close();
                                    mDevice = null;
                                }
                            });
                    mDevice.createCaptureSession(config);

                } catch (CameraAccessException e) {
                    error();
                }
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice camera) {
                Log.e(TAG, "onDisconnected, camera: " + camera);
            }

            @Override
            public void onError(@NonNull CameraDevice camera, int error) {
                Log.e(TAG, "onError, camera: " + camera + " ,error : " + error);
            }
        }, cameraHandler);
  1. 创建CaptureRequest请求的函数(因为涉及到多个Surface,就单独创建了)
    private CaptureRequest createCaptureRequest(CameraDevice cameraDevice, List<Surface> surfaces) {
        CaptureRequest.Builder captureRequest;
        try {
            //简单预览模式创建,有要求可以根据api去选video这些
            captureRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            //聚焦模式,不设置也可以
            captureRequest.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
        } catch (CameraAccessException e) {
            throw new RuntimeException(e);
        }
        if (surfaces != null) {
            //把每一个需要输出摄像头数据的Surface都加入
            for (Surface surface : surfaces) {
                if (surface == null) {
                    continue;
                }
                captureRequest.addTarget(surface);
            }
        }
        return captureRequest.build();
    }
  1. 创建List的函数,Surface比较多,用的比较新的api去创建的session,老的api已经被标注遗弃了,所以单独列个方法。
    private List<OutputConfiguration> createOutputConfig(List<Surface> surfaces) {
        List<OutputConfiguration> configs = new ArrayList<>(surfaces.size());
        for (Surface surface : surfaces) {
            if (surface == null) {
                continue;
            }
            //SDK32,config.enableSurfaceSharing()开启分享后addSurface()添加新的输出面会有问题,所以就没以一个Config去添加所有Surface
            OutputConfiguration config = new OutputConfiguration(surface);
            //就只创建,啥也不做,有要求可以自己加
            configs.add(config);
        }
        return configs;
    }

四、获取编码器的编码数据

  1. 这部分网上挺多的,也不难.
    //这个是保存录像数据的
    FileOutputStream outputStream;
    //每个关键帧的数据前面都需要添加spsPps的数据,不然无法解码播放
    byte[] spsPps = null;
    
    boolean codecing = true;
    //编码输出信息的对象,赋值由MediaCodec做
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

            //当前可用的数据位置
    int outputBufferIndex;
    byte[] h264 = new byte[mCameraAdapter.getSize().getWidth() * mCameraAdapter.getSize().getHeight()];
    //死循环获取,也可以对MediaCodec设置callback获取
    while (codecing) {
        //获取未来一段时间可用的输出缓冲区,如果存在可用,返回值大于等于0,bufferInfo也会被赋值,最大等待时间是100000微秒,其实就是100毫秒
        outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
        Log.i(TAG, "dequeue output buffer outputBufferIndex=" + outputBufferIndex);
    
        //如果当前输出缓冲区没有可用的,返回负值,不同值含义不一样,有需要做判定即可
        if (outputBufferIndex < 0) {                
            continue;
        }
        ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
        Log.i(TAG, "Streaming   bufferInfo,flags: " + bufferInfo.flags
                + ", size: " + bufferInfo.size
                + ", presentationTimeUs: " + bufferInfo.presentationTimeUs
                + ", offset: " + bufferInfo.offset);
        //调整数据位置,从offset开始。这样我们一会儿读取就不用传offset偏差值了。 
        outputBuffer.position(bufferInfo.offset);
        //改完位置,那肯定要改极限位置吧,不然你数据不就少了数据末尾长度为offset的这一小部分?这两步不做也可以,get的时候传offset也一样
        outputBuffer.limit(bufferInfo.offset + bufferInfo.size);

        //现在的spsPps还是空的,这是不能做写入文件,保存之类操作的
        if (spsPps == null) {
            //创建写入的文件,保存录像
            try {
                File file = new File(mContext.getDataDir().getAbsolutePath() + "camera.h264");
                boolean newFile = file.createNewFile();
                outputStream = new FileOutputStream(file, true);
            } catch (Exception ignored) {
            }
            // sps pps获取并保存,还有其他方式可以获取,不过不一定有效,最好检验一下
            ByteBuffer sps = mMediaCodec.getOutputFormat().getByteBuffer("csd-0");
            ByteBuffer pps = mMediaCodec.getOutputFormat().getByteBuffer("csd-1");
            byte[] spsBites = new byte[sps.remaining()];
            byte[] ppsBites = new byte[pps.remaining()];
            sps.get(spsBites);
            pps.get(ppsBites);
            spsPps = new byte[spsBites.length + ppsBites.length];
            System.arraycopy(spsBites, 0, spsPps, 0, spsBites.length);
            System.arraycopy(ppsBites, 0, spsPps, spsBites.length, ppsBites.length);

        } else {
            //现在已经有spsPps数据了,可以开始了
            try {

                if (bufferInfo.size > h264.length) {
                    h264 = new byte[bufferInfo.size];
                }
                h264 = new byte[bufferInfo.size];
                //获取编码数据
                outputBuffer.get(h264, 0, bufferInfo.size);

                //数据校验一下,如果是关键帧需要在头部加入spsPps,校验完的trans,就是编码好的h264编码数据了
                byte[] trans = trans(h264, 0, bufferInfo.size, bufferInfo.flags);


                //保存录像数据到文件中.暂定2G大小
                if (outputStream.getChannel().size() < (2L << 30)) {
                    outputStream.write(trans);
                }

                //结束标志到达
                if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                    pusher.stop();
                    isStreaming = false;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //释放这个缓冲位置的数据,不释放就一直在,MediaCodec数据满了可不行
                mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            }
        }
    }
说明: (1) 获取编码的 index,大于等于零才是有效的,不然不能用。 (2) 保存h264编码数据之前,需要先获取spsPps,不然关键帧之前没有spsPps,解码播放端也不加的话就没法看了。 (3) 获取数据注意offset,真正有效的数据是 bytes\[offset\] 到 bytes\[offset+size\]这个位置,至于你是get()的时候传offset拿,还是possition()limit()后传0拿,就看你心情了。 (4) index的输出缓冲区用完之后,需要释放. (5) 这个录像保存只是做个样子,如果真是录像,建议用 MediaMuxer 来做,直接判定 index 大于等于零后调 api 传 buffinfo 就行,简单方便。
  1. 这是校验是不是关键帧的,很简单明了
    private byte[] trans(byte[] h264, int offset, int length, int flags) {
        //关键帧判定
        if ((flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
            // 每个关键帧前面需要添加spsPps
            byte[] data = new byte[spsPps.length + length];
            System.arraycopy(spsPps, 0, data, 0, spsPps.length);
            System.arraycopy(h264, offset, data, spsPps.length, length);

            return data;
        } else {
            return h264;
        }
    }

五、播放保存的 h264 文件.

连上设备,我这边设置的文件位置是 Context.getDataDir().getAbsolutePath()/camera.h264,真实目录是 /data/data/包名/camera.h264,保存出来用vlc,或者ffmpeg命令ffplay播放就行了,亲测:windows自带的播放器无法解码h264(笑)。

  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您好!对于在 Android 上使用 Camera2 API 和 FFmpeg 进行视频编码,您可以按照以下步骤操作: 1. 首先,您需要在您的 Android 项目中集成 FFmpeg。您可以通过在项目的 build.gradle 文件中添加 FFmpeg 库的引用来完成此操作。例如: ```groovy implementation 'com.writingminds:FFmpegAndroid:0.3.2' ``` 2. 接下来,您需要设置并打开 Camera2 API。您可以参考 Android 官方文档来了解如何使用 Camera2 API 进行摄像头操作。 3. 在获取Camera2 的帧数据后,您可以将其编码为视频文件。为此,您可以使用 FFmpeg 提供的命令行接口。以下是一个示例代码片段,演示如何将 Camera2 的帧数据编码为 H.264 格式的视频文件: ```java // 设置 FFmpeg 命令行参数 String[] ffmpegCmd = new String[] { "-y", // 覆盖输出文件 "-f", "rawvideo", "-vcodec", "rawvideo", "-pix_fmt", "nv21", // 请根据摄像头输出格式进行更改 "-s", previewSize.getWidth() + "x" + previewSize.getHeight(), // 请根据预览尺寸进行更改 "-i", "-", // 使用输入管道获取帧数据 "-c:v", "libx264", "-preset", "ultrafast", // 编码速度,可根据需求进行更改 "-tune", "zerolatency", // 实时性优化,可根据需求进行更改 "-crf", "23", // 视频质量,可根据需求进行更改 "-f", "mp4", outputFile.getAbsolutePath() }; // 创建 FFmpeg 进程并启动编码 Process ffmpegProcess = FFmpeg.getInstance(context).execute(ffmpegCmd, new ExecuteBinaryResponseHandler() { @Override public void onSuccess(String message) { // 编码成功 } @Override public void onFailure(String message) { // 编码失败 } }); // 将 Camera2 的帧数据写入 FFmpeg 的输入管道 OutputStream ffmpegInputStream = ffmpegProcess.getOutputStream(); ffmpegInputStream.write(frameData); ffmpegInputStream.flush(); ffmpegInputStream.close(); ``` 请注意,上述代码仅为示例,您需要根据您的项目需求进行适当的修改。 希望这能对您有所帮助!如有任何进一步的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值