文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android采集摄像头的视频流数据并使用MediaCodec编码为H264格式

2022-06-06 13:09

关注

前言:

博主在写这篇文章之前可以说是在

音视频这方面
,知识积累与经验几乎为0;所以在实现这个功能上也是费了好一番功夫和精力把它给搞出来了,所以以此篇文章纪念一下。

一、首先就是需要先打开摄像头,并拿到视频的每一帧数据 1、相机权限是必须要的,API>=6.0还需要动态申请 (动态申请权限代码略过,详情见文末源码)

2、在布局上放置一个
SurfaceView
用来预览相机的画面

3、初始化
SurfaceView
,并为
SurfaceHolder
添加一个
addCallback
来获取
SurfaceView
的状态
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {
    private SurfaceHolder holder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    private void initView() {
        SurfaceView surfaceView = findViewById(R.id.sfv);
        holder = surfaceView.getHolder();
        holder.addCallback(this);
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}
二、当
SurfaceView
就绪好后就可以开启相机获取视频帧数据了
1、在
surfaceCreated
处打开相机

    private Camera camera;
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    	//打开相机
        openCamera();
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    	//关闭相机
        releaseCamera(camera);
    }
     
    private void openCamera() {
        camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
        //获取相机参数
        Camera.Parameters parameters = camera.getParameters();
        //获取相机支持的预览的大小
        Camera.Size previewSize = getCameraPreviewSize(parameters);
        int width = previewSize.width;
        int height = previewSize.height;
        //设置预览格式(也就是每一帧的视频格式)YUV420下的NV21
        parameters.setPreviewFormat(ImageFormat.NV21);
        //设置预览图像分辨率
        parameters.setPreviewSize(width, height);
        //相机旋转90度
        camera.setDisplayOrientation(90);
        //配置camera参数
        camera.setParameters(parameters);
        try {
            camera.setPreviewDisplay(holder);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //设置监听获取视频流的每一帧
        camera.setPreviewCallback(new Camera.PreviewCallback() {
            @Override
            public void onPreviewFrame(byte[] data, Camera camera) {
            }
        });
        //调用startPreview()用以更新preview的surface
        camera.startPreview();
    }
    
    private Camera.Size getCameraPreviewSize(Camera.Parameters parameters) {
        List list = parameters.getSupportedPreviewSizes();
        Camera.Size needSize = null;
        for (Camera.Size size : list) {
            if (needSize == null) {
                needSize = size;
                continue;
            }
            if (size.width >= needSize.width) {
                if (size.height > needSize.height) {
                    needSize = size;
                }
            }
        }
        return needSize;
    }
    
    public void releaseCamera(Camera camera) {
        if (camera != null) {
            camera.setPreviewCallback(null);
            camera.stopPreview();
            camera.release();
        }
    }
2、到这里不出意外的话就可以在界面上可以看到摄像头的画面了

而视频每一帧的数据就在

onPreviewFrame(byte[] data, Camera camera)
回调函数中获取

3、注意:这里获取到的视频数据是
YUV420
编码格式的原始数据,并不是我们要的H264编码格式;所以接下来就是要对这每一帧的视频数据进行转码了。对于编码格式想要了解的大家可以出门右转问下度娘^ _ ^ 三、通过MediaCodec编码成H264格式

编码步骤

将从摄像头获取到的
NV21
数据编码成
NV12
,因为
MediaCodec
不支持
NV21
格式 需要将
NV21
格式数据进行顺时针旋转
90度
,因为从摄像头拿到的画面已经被逆时针旋转了90度 使用
MediaCodec
进行编码

1、代码有点多,这里就贴上整个类代码了

public class NV21EncoderH264 {
    private int width, height;
    private int frameRate = 30;
    private MediaCodec mediaCodec;
    private EncoderListener encoderListener;
    public NV21EncoderH264(int width, int height) {
        this.width = width;
        this.height = height;
        initMediaCodec();
    }
    private void initMediaCodec() {
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
            //height和width一般都是照相机的height和width。
            //TODO 因为获取到的视频帧数据是逆时针旋转了90度的,所以这里宽高需要对调
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", height, width);
            //描述平均位速率(以位/秒为单位)的键。 关联的值是一个整数
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
            //描述视频格式的帧速率(以帧/秒为单位)的键。帧率,一般在15至30之内,太小容易造成视频卡顿。
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
            //色彩格式,具体查看相关API,不同设备支持的色彩格式不尽相同
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
            //关键帧间隔时间,单位是秒
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            //开始编码
            mediaCodec.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public void encoderH264(byte[] data) {
        //将NV21编码成NV12
        byte[] bytes = NV21ToNV12(data, width, height);
        //视频顺时针旋转90度
        byte[] nv12 = rotateNV290(bytes, width, height);
        try {
            //拿到输入缓冲区,用于传送数据进行编码
            ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
            //拿到输出缓冲区,用于取到编码后的数据
            ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
            int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
            //当输入缓冲区有效时,就是>=0
            if (inputBufferIndex >= 0) {
                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                inputBuffer.clear();
                //往输入缓冲区写入数据
                inputBuffer.put(nv12);
                //五个参数,第一个是输入缓冲区的索引,第二个数据是输入缓冲区起始索引,第三个是放入的数据大小,第四个是时间戳,保证递增就是
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, nv12.length, System.nanoTime() / 1000, 0);
            }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            //拿到输出缓冲区的索引
            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
            while (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                byte[] outData = new byte[bufferInfo.size];
                outputBuffer.get(outData);
                //outData就是输出的h264数据
                if (encoderListener != null) {
                    encoderListener.h264(outData);
                }
                mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
    
    private byte[] NV21ToNV12(byte[] nv21, int width, int height) {
        byte[] nv12 = new byte[width * height * 3 / 2];
        int frameSize = width * height;
        int i, j;
        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;
    }
    
    private byte[] rotateNV290(byte[] data, int imageWidth, int imageHeight) {
        byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
        // Rotate the Y luma
        int i = 0;
        for (int x = 0; x = 0; y--) {
                yuv[i] = data[y * imageWidth + x];
                i++;
            }
        }
        // Rotate the U and V color components
        i = imageWidth * imageHeight * 3 / 2 - 1;
        for (int x = imageWidth - 1; x > 0; x = x - 2) {
            for (int y = 0; y < imageHeight / 2; y++) {
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
                i--;
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
                i--;
            }
        }
        return yuv;
    }
    
    public void setEncoderListener(EncoderListener listener) {
        encoderListener = listener;
    }
    public interface EncoderListener {
        void h264(byte[] data);
    }
}
2、这里需要注意一点:因为我们将视频旋转了90度,所以需要将原来的宽变成高,高变成宽;所以在初始化MediaFormat的时候,需要将传入的宽高对调一下,不然画面会显示花屏
MediaCodec mediaCodec = MediaCodec.createEncoderByType("video/avc");
//height和width一般都是照相机的height和width。
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", height, width);
四、使用写好的工具类进行编码 需要在打开相机之前创建好实例
    
    private void openCamera() {
        //....省略若干代码
        Camera.Parameters parameters = camera.getParameters();
        //获取相机支持的预览的大小
        Camera.Size previewSize = getCameraPreviewSize(parameters);
        int width = previewSize.width;
        int height = previewSize.height;
        //....省略若干代码
        final NV21EncoderH264 nv21EncoderH264 = new NV21EncoderH264(width, height);
        nv21EncoderH264.setEncoderListener(this);
        //设置监听获取视频流的每一帧
        camera.setPreviewCallback(new Camera.PreviewCallback() {
            @Override
            public void onPreviewFrame(byte[] data, Camera camera) {
                nv21EncoderH264.encoderH264(data);
            }
        });
        //调用startPreview()用以更新preview的surface
        camera.startPreview();
    }
	//编码成功的回调
    @Override
    public void h264(byte[] data) {
        Log.e("TAG", data.length + "");
    }
运行的效果
在这里插入图片描述 既然已经把视频帧数据编码好了,那就可以把它写入到一个文件里拿来播放了 五、保存编码好的数据为视频文件
onCreate()
的时候创建写入的文件
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        createFile();
    }
    @Override
    public void h264(byte[] data) {
        try {
            outputStream.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void createFile() {
        File file = new File(getExternalCacheDir(), "test.h264");
        try {
            outputStream = new FileOutputStream(file);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
生成的文件
在这里插入图片描述 使用
adb pull
命令下载到电脑进行播放就可以 Demo 下载地址请戳我
作者:Code-Porter


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯