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

Laurie ·
更新时间:2024-11-10
· 621 次阅读

前言:

博主在写这篇文章之前可以说是在音视频这方面,知识积累与经验几乎为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(); } } /** * 将NV21编码成H264 */ 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(); } } /** * 因为从MediaCodec不支持NV21的数据编码,所以需要先讲NV21的数据转码为NV12 */ 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; } /** * 此处为顺时针旋转旋转90度 * * @param data 旋转前的数据 * @param imageWidth 旋转前数据的宽 * @param imageHeight 旋转前数据的高 * @return 旋转后的数据 */ 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



数据 mediacodec 摄像 h2 摄像头 Android

需要 登录 后方可回复, 如果你还没有账号请 注册新账号