Camera 视频采集,H264 编码保存

一. 前言

上篇文章《AAC 音频编码保存和解码播放》 讲述了通过 AudioRecord 录制音频数据,并通过 AAC 编码保存为 AAC 文件。这里的 aac 既是一种编码方式,也是一种容器,因此可以直接播放。本篇文章讲讲述如何使用 Camera 采集音频音频数据,并通过 H264 编码保存为 .h264 文件,因为 .h264 不是标准的容器,所以不能直接播放,但是可以通过 ffmpeg 播放。

二. Camera 的预览和数据采集

1. Camera 的预览

Camera 的预览需要使用 SurfaceView (TextureView 也可以),SurfaceView 和 Surface , SurfaceHolder 搭配使用,它们的关系如下:

  • Surface:是内存中一段绘图缓冲区,可以独立地绘制图像。
  • SurfaceView:拥有 Surface 的 View。
  • SurfaceHolder: Surface 的持有,管理类,SurfaceView 通过 SurfaceHoler 管理 Surface。SurfaceHolder 有个接口 SurfaceHolder.Callback ,可以监听 Surface 的状态(创建,改变和销毁)。
2. Camera 的创建

Camera 的创建需要设定一些参数。

// cameraFacing 表示打开前置摄像头还是后置摄像头
Camera.open(cameraFacing);

// 设置预览输出的格式 , 这里是 NV21 所有的相机都支持, 是 YUV420 的一种
mParameters = camera.getParameters();
mParameters.setPreviewFormat(ImageFormat.NV21);

//设置预览的大小,Camera 预览的大小(分辨率)只支持内置的几种 getSupportedPreviewSizes 
Camera.Size previewSize = getBestSize(DEFAULT_WIDTH, DEFAULT_HEIGHT, mParameters.getSupportedPreviewSizes());
mParameters.setPreviewSize(previewSize.width, previewSize.height);

//如果使用截图接口,还需要设置截图大小(分辨率)
Camera.Size pictureSize = getBestSize(DEFAULT_WIDTH, DEFAULT_HEIGHT, mParameters.getSupportedPictureSizes());
mParameters.setPictureSize(pictureSize.width, pictureSize.height);

//设置支持的聚焦模式
if (supportFocus(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
    mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
private boolean supportFocus(String focus) {
    List<String> focusModes = mCamera.getParameters().getSupportedFocusModes();
    if (focusModes.contains(focus)) {
        return true;
    }
    return false;
}

//设置帧率
int defminFps = 0;
int defmaxFps = 0;
List<int[]> supportedPreviewFpsRange = mParametergetSupportedPreviewFpsRange();
for (int[] fps : supportedPreviewFpsRange) {
    if (defminFps <= fps[PREVIEW_FPS_MIN_INDEX] && defmaxFps <fps[PREVIEW_FPS_MAX_INDEX]) {
        defminFps = fps[PREVIEW_FPS_MIN_INDEX];
        defmaxFps = fps[PREVIEW_FPS_MAX_INDEX];
    }
}
mParameters.setPreviewFpsRange(defminFps, defmaxFps);


//由于相机预览默认是横屏的,还需要根据实际情况设置为竖屏或者横屏

private void setCameraDisplayOrientation(Activity activity) {
        Camera.CameraInfo info = new Camera.CameraInfo();
        Camera.getCameraInfo(mCameraFacing, info);
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }
        mDisplayOrientation = 0;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            mDisplayOrientation = (info.orientation + degrees) % 360;
            mDisplayOrientation = (360 - mDisplayOrientation) % 360;
        } else {
            mDisplayOrientation = (info.orientation - degrees + 360) % 360;
        }
        mCamera.setDisplayOrientation(mDisplayOrientation);
    }


//最后将参数传给 camera.setParameters(mParameters);
3. Camera 的预览数据回调
mCamera.setPreviewCallback(this);

//回调的接口
 @Override
    public void onPreviewFrame(byte[] bytes, Camera camera) {
        //...
    }


三. H264 编码保存

1. 找到编码器信息

由于 MediaCodec 硬编码的兼容性问题,需要判断是否有支持 “video/avc” 的编码器(avc 就是 H264 )

private MediaCodecInfo selectCodecInfo() {
        int numCodecs = MediaCodecList.getCodecCount();
        for (int i = 0; i < numCodecs; i++) {
            MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
            if (!codecInfo.isEncoder()) {
                continue;
            }
            String[] types = codecInfo.getSupportedTypes();
            for (int j = 0; j < types.length; j++) {
                if (types[j].equalsIgnoreCase(H264Encoder.VIDEO_MIME_TYPE)) {
                    return codecInfo;
                }
            }
        }
        return null;
    }
2. 创建媒体格式

创建媒体格式用于编码器的参数配置

//查询编码器支持的输入像素格式
    private int selectColorFormat(MediaCodecInfo codecInfo) {
        if (codecInfo == null) {
            return -1;
        }
        MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(H264Encoder.VIDEO_MIME_TYPE);
        int[] colorFormats = capabilities.colorFormats;
        for (int i = 0; i < colorFormats.length; i++) {
            if (isRecognizedFormat(colorFormats[i])) {
                return colorFormats[i];
            }
        }
        return -1;
    }

    private boolean isRecognizedFormat(int colorFormat) {
        switch (colorFormat) {
            // these are the formats we know how to handle for this test
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar://对应Camera预览格式I420(YV21/YUV420P)
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: //对应Camera预览格式NV12
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar://对应Camera预览格式NV21
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: {////对应Camera预览格式YV12
                return true;
            }
            default:
                return false;
        }
    }


//配置 MediaFormat
mBitRate = (mWidth * mHeight * 3 / 2) * 8 * fps;
mMediaFormat = MediaFormat.createVideoFormat(VIDEO_MIME_TYPE, mHeight, mWidth);
mMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);/todo 没有这一行会报错 configureCodec returning error -38
mMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
mMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMATmColorFormat);
mMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
Log.e("eee", mMediaCodecInfo.getName());
try {
    mMediaCodec = MediaCodec.createByCodecName(mMediaCodecInfgetName());
} catch (IOException e) {
    e.printStackTrace();
}

3. 创建编码器
//创建 编码器
try {
    mMediaCodec = MediaCodec.createByCodecName(mMediaCodecInfo.getName());
} catch (IOException e) {
    e.printStackTrace();
}
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

四. 编码器的数据输入

//在 Camera 的预览回调接口中将数据放入队列

mCameraHelper.setPreviewCallback(new CameraHelper.PreviewCallback() {
            @Override
            public void onFrame(byte[] data) {
                mH264Encoder.putFrameData(data);
            }
        });


public void putFrameData(byte[] data) {
        if (data == null || !mIsEncoding) {
            return;
        }
        try {
            mQueue.put(data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

五. 循环取出数据

//循环从 队列中取出数据进行编解码,
mExecutorService.execute(new Runnable() {
            @Override
            public void run() {
                mIsEncoding = true;
                mPresentationTimeUs = System.currentTimeMillis() * 1000;
                mMediaCodec.start();
                while (mIsEncoding) {
                    byte[] data = getFrameData();
                    if (data == null) {
                        continue;
                    }
                    encodeVideoData(data);
                }

                mMediaCodec.stop();
                mMediaCodec.release();
                IOUtil.close(mFileOutputStream);
                IOUtil.close(mBufferedOutputStream);
            }

六. 编码

1. 旋转数据方向

由于 Camera 预览的数据默认是横屏的,还需要将数据旋转 90 度 (这和 setDisplayOrientation 无关,即即使设置了 setDisplayOrientation,预览的数据还是横屏的数据),除此之外,由于 Camera 设置的输出格式 和 MediaCodec 支持的输入格式可能不同,还需要进行进一步转换。

private byte[] transferFrameData(byte[] data, byte[] yuvBuffer, byte[] rotatedYuvBuffer) {
        //Camera 传入的是 NV21
        //转换成 MediaCodec 支持的格式
        switch (mColorFormat) {
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar://对应Camera预览格式I420(YV21/YUV420P)
                YUVEngine.Nv21ToI420(data, yuvBuffer, mWidth, mHeight);
                YUVEngine.I420ClockWiseRotate90(yuvBuffer, mWidth, mHeight, rotatedYuvBuffer, mOutWidth, mOutHeight);
                Log.i("transferFrameData", "COLOR_FormatYUV420Planar");
                break;
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: //对应Camera预览格式NV12
                YUVEngine.Nv21ToNv12(data, yuvBuffer, mWidth, mHeight);
                YUVEngine.Nv12ClockWiseRotate90(yuvBuffer, mWidth, mHeight, rotatedYuvBuffer, mOutWidth, mOutHeight);
                Log.i("transferFrameData", "COLOR_FormatYUV420SemiPlanar");
                break;
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar://对应Camera预览格式NV21
                System.arraycopy(data, 0, yuvBuffer, 0, mWidth * mHeight * 3 / 2);
                YUVEngine.Nv21ClockWiseRotate90(yuvBuffer, mWidth, mHeight, rotatedYuvBuffer, mOutWidth, mOutHeight);
                Log.i("transferFrameData", "COLOR_FormatYUV420PackedSemiPlanar");
                break;
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: ////对应Camera预览格式YV12
                YUVEngine.Nv21ToYV12(data, yuvBuffer, mWidth, mHeight);
                YUVEngine.Yv12ClockWiseRotate90(yuvBuffer, mWidth, mHeight, rotatedYuvBuffer, mOutWidth, mOutHeight);
                Log.i("transferFrameData", "COLOR_FormatYUV420PackedPlanar");
                break;
        }
        return rotatedYuvBuffer;
    }

YUVEngine 是一个对 YUV 数据操作的封装类。

2. 编码
//编码的流程可以简单概括如下:
dequeueInputBuffer// 获取可用的输入缓存区 buffer 的下标 inputIndex
getInputBuffers// 根据 inputIndex 获取可用的输入缓冲区 bytebuffer 
bytebuffer.put // 放入数据
queueInputBuffer // 将数据放入输入缓冲区
dequeueOutputBuffer // 获取可用的输出缓存区 buffer 的下标 outputIndex
getOutPutBuffers // 根据 outputIndex 获取可用的输出缓冲区 bytebuffer
outputBuffer.get() // 获取数据
releaseOutputBuffer // 处理完成,释放 buffer
//其中还有一个参数 pts,Presentation Time Stamp , 用于表示一帧的显示时间,我们知道 PTS 是告诉播放器播放一帧的时间,而 DTS 是解码时间,因此在进行编码的时候就应该传入 PTS 用于解码后的播放。除此之外,一些设备如果没有设置合理的值,那么在编码的时候就会采取丢弃帧和低质量编码的方式。


ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        int inputIndex = mMediaCodec.dequeueInputBuffer(10_000);
        if (inputIndex >= 0) {
            ByteBuffer byteBuffer = inputBuffers[inputIndex];
            byteBuffer.clear();
            byteBuffer.put(mRotatedYUVBuffer);
            long pts = System.currentTimeMillis() * 1000 - mPresentationTimeUs;
            mMediaCodec.queueInputBuffer(inputIndex, 0, mRotatedYUVBuffer.length, pts, 0);
        }

        ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
        int outputIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000);
        if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            outputBuffers = mMediaCodec.getOutputBuffers();
        }
        while (outputIndex >= 0) {
            ByteBuffer byteBuffer = outputBuffers[outputIndex];
            byte[] buffer = new byte[mBufferInfo.size];
            byteBuffer.get(buffer);
            //写入 .h264 文件
            try {
                mBufferedOutputStream.write(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mMediaCodec.releaseOutputBuffer(outputIndex, false);
            outputIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000);
        }
3. 验证

最后生成的 .h264 文件不能直接在播放器播放,但是可以通过 ffplay 播放

ffplay media_codec_video.h264

github demo

![欢迎关注我的微信公众号【海盗的指针】]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342