音视频6.2——相机采集数据编码成H264

 音视频开发路线:

Android 音视频开发入门指南_Jhuster的专栏的技术博客_51CTO博客_android 音视频开发入门

demo地址:

GitHub - wygsqsj/videoPath: 音视频学习路线demo

Camera采集数据

安卓摄像头采集得数据格式是NV21,NV21格式比较奇葩,安卓的MediaCodec编码也不支持,需要转换成NV12格式即YUVI420,他们两个的YUV排列顺序就是将UV调换了过来,NV21奇数位为V,偶数为U,NV12正好反过来:

NV21:

音视频6.2——相机采集数据编码成H264_第1张图片

NV12:

音视频6.2——相机采集数据编码成H264_第2张图片

 

 无论哪种格式,都是一样的存储大小,一帧画面的大小计算公式,Y、U、V 每个都占一个byte,

  • Y = 宽* 高
  • U = 宽* 高*1/4
  • V = 宽* 高 * 1/4
  • YUV =宽* 高*3/2

也就是每一帧数据的存储大小就是 :宽* 高*3/2

当前就是普通的通过Camera1采集数据,并为Camera相机设置SurfaceView的预览:

mCamera = Camera.open(0);
if (mCamera == null) {
    throw new RuntimeException("摄像机打开失败!");
}
try {
    //设置camera数据回调
    mCamera.setPreviewCallback(this);
    mCamera.setDisplayOrientation(90);
    if (parameters == null) {
        parameters = mCamera.getParameters();
    }
    //获取默认的camera配置
    parameters = mCamera.getParameters();
   //设置预览格式
    parameters.setPreviewFormat(ImageFormat.NV21);
    //配置camera参数
    mCamera.setParameters(parameters);
    //将完全初始化的SurfaceHolder传入到setPreviewDisplay(SurfaceHolder)中
    //没有surface的话,相机不会开启preview预览
    mCamera.setPreviewDisplay(surfaceview.getHolder());
    //NV21一帧的大小,其实就是 宽*高*3/2
    callbackBuffer = new byte[width * height * 3 / 2];
    mCamera.addCallbackBuffer(callbackBuffer);
    mCamera.setPreviewCallback(this);
    mCamera.startPreview();
} catch (IOException e) {
    e.printStackTrace();
}

从SurfaceView的回调中获取到yuv数据,放到一个队列中,供我们的编码线程获取:

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo6);
        surfaceview = findViewById(R.id.demo6Surface);
        surfaceview.getHolder().addCallback(this);
    }



//摄像头获取到的yuv数据回调
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        //将当前帧图像保存在队列中
        putYUVData(data, data.length);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        initBackCamera();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (null != mCamera) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }

        if (encodeThread != null) {
            encodeThread.stopEncode();
        }

        if (encodeMuxerThread != null) {
            encodeMuxerThread.stopEncode();
        }
    }

    public void putYUVData(byte[] buffer, int length) {
//        Log.i(LOG_TAG, "获取到摄像头数据" + length);
        if (YUVQueue.size() >= 10) {
            YUVQueue.poll();
        }
        YUVQueue.add(buffer);
    }

旋转YUV90度得到竖屏

摄像头获取摄像头数据是以横屏时的左上角为原点获取的,也就是我们摄像头捕捉的原始数据时横屏的,这也是我们为什么在初始化Camera时要把预览画面旋转90度的原因,先来看看未旋转时的图像:

音视频6.2——相机采集数据编码成H264_第3张图片

而设置旋转90度后的图像:

音视频6.2——相机采集数据编码成H264_第4张图片

 未旋转时即便我们时竖屏下拍照,摄像头仍旧是捕捉的横屏画面,所以得到的也是横屏的照片,这时候我们按顺时针旋转90度即可得到竖屏的数据:

 横屏转竖屏就是将YUV数据顺时针旋转90度即可得到:

/**
 * 旋转yuv数据,将横屏数据转换为竖屏
 */
private byte[] revolveYuv(byte[] yuvData) {
    byte[] revolveData = new byte[yuvData.length];
    int y_size = width * height;
    //uv高度
    int uv_height = height >> 1;
    //旋转y,左上角跑到右上角,左下角跑到左上角,从左下角开始遍历
    int k = 0;
    for (int i = 0; i < width; i++) {
        for (int j = height - 1; j > -1; j--) {
            revolveData[k++] = yuvData[width * j + i];
        }
    }
    //旋转uv
    for (int i = 0; i < width; i += 2) {
        for (int j = uv_height - 1; j > -1; j--) {
            revolveData[k++] = yuvData[y_size + width * j + i];
            revolveData[k++] = yuvData[y_size + width * j + i + 1];
        }
    }
    return revolveData;
}

IO流方式对YUV数据进行编码

通过FileOutStream来将编码好的数据写入到h264文件中,此中方式写出来来的264文件播放时没有时间戳标识,而且文件比较大,此处应该还有没有照顾到的点,但是可以写出264文件且进行播放代码如下:

1.配置MediaFormat,构建编码MediaCodec,注意我们配置宽高的时候,要按竖屏的宽高来配置

//构建对应的MeidaFormat,后期我们会将摄像头数据旋转90读成为竖屏,所以此处调换一下宽高
MediaFormat mediaFormat = MediaFormat.createVideoFormat(encodeMine, height, width);
//设置yuv格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//比特率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
//描述视频格式的帧速率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
//关键帧之间的间隔,此处指定为1秒
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

//构建编码h264MediaCodec
encodeCodec = MediaCodec.createEncoderByType(encodeMine);
encodeCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解码得到的byte[]数据的相关信息
//启动编码器
encodeCodec.start();

比特率:可以理解成264文件的码率,即时间单位中编码所占的字节数,越高代表画质越好,记录下来的细节越多,但同样的文件也就越大,他的单位是kbps,算法是 文件大小M/时间秒 得出来

帧率:用FPS表示,用于表示一秒有多少帧,与画面得流畅度相关,如果帧数越多,画面衔接越好,同样帧数越多文件越大

2.获取YUV数据,放到编码器中进行编码

Camera1获取得数据格式是NV21,需要转换成MediaCodec可以操作得NV12格式才可以传给编码器进行操作

//获取当前的yuv数据
if (!demo6Activity.getYUVQueue().isEmpty()) {
    //从猪肉工厂获取装猪的小推车,填充数据后发送到猪肉工厂进行处理
    ByteBuffer[] inputBuffers = encodeCodec.getInputBuffers();//所有的小推车
    int inputIndex = encodeCodec.dequeueInputBuffer(0);//返回当前可用的小推车标号
    if (inputIndex != -1) {
        Log.i(LOG_TAG, "找到了input 小推车" + inputIndex);
        ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到小推车
        inputBuffer.clear();//扔出去里面旧的东西
        //从yuv队列中取出数据
        byte[] yuvData = demo6Activity.getYUVQueue().poll();
        //摄像头默认是横着的,需要旋转90度
        byte[] revolveData = revolveYuv(yuvData);
       
        byte[] yuv420sp = new byte[width * height * 3 / 2];
        //把待编码的视频帧转换为YUV420格式
        NV21ToNV12(revolveData, yuv420sp, height, width);

        //audioExtractor没猪了,也要告知一下
        if (revolveData.length < 0) {
              Log.i(LOG_TAG, "当前yuv数据异常");
        } else {//拿到猪
            //把转换后的YUV420格式的视频帧放到编码器输入缓冲区中
            Log.i(LOG_TAG, "yuv数据转换成功,当前数据的数据长度为:" + yuvData.length);
            inputBuffer.limit(revolveData.length);
            inputBuffer.put(revolveData, 0, yuvData.length);
            //计算时间戳,配置了但是没有用,所以此处传0也可以
            pts = computePresentationTime(generateIndex);
            Log.i(LOG_TAG, "当前时间戳:" + pts);
            encodeCodec.queueInputBuffer(inputIndex, 0, revolveData.length, pts, 0);
            generateIndex += 1;
        }
    } else {
        Log.i(LOG_TAG, "没有可用的input 小推车");
    }
}

//在使用Camera的时候,设置预览的数据格式为NV21,转换成nv12
private byte[] NV21ToNV12(byte[] nv21, byte[] nv12, int width, int height) {
    if (nv21 == null || nv12 == null) return null;
    int framesize = width * height;
    int i = 0, j = 0;
    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;
}

3.将编码好得数据写入到h264文件中

SPS和PPS等配置信息是放在I帧前面得,如果读取到这些配置信息,需要先将这些信息存储下来,等到I帧编码出来,为每个I帧前面添加上这些配置信息,这样在另一端接受时就可以从任何一个I帧前面找到这些配置信息来配置播放器

int outputIndex = encodeCodec.dequeueOutputBuffer(encodeBufferInfo, 10000);//返回当前筐的标记
switch (outputIndex) {
    case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
        Log.i(LOG_TAG, "输出的format已更改" + encodeCodec.getOutputFormat());
        break;
    case MediaCodec.INFO_TRY_AGAIN_LATER:
        Log.i(LOG_TAG, "超时,没获取到");
        break;
    case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
        Log.i(LOG_TAG, "输出缓冲区已更改");
        break;
    default:
        Log.i(LOG_TAG, "获取到编码后的数据了,当前解析后的数据长度为:" + encodeBufferInfo.size);
        //获取所有的筐
        ByteBuffer[] outputBuffers = encodeCodec.getOutputBuffers();
        //拿到当前装满火腿肠的筐
        ByteBuffer outputBuffer;
        if (Build.VERSION.SDK_INT >= 21) {
            outputBuffer = encodeCodec.getOutputBuffer(outputIndex);
        } else {
            outputBuffer = outputBuffers[outputIndex];
        }
        //将数据读取到outData中
        byte[] outData = new byte[encodeBufferInfo.size];
        outputBuffer.get(outData);
        //当前是初始化编解码器数据,不是媒体数据,sps、pps等初始化数据
        if (encodeBufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG) {
            Log.i(LOG_TAG, "配置信息编码完成");
            configByte = new byte[encodeBufferInfo.size];
            configByte = outData;
        } else if (encodeBufferInfo.flags == BUFFER_FLAG_KEY_FRAME) {//当前的数据中包含关键帧数据
            Log.i(LOG_TAG, "I帧编码完成");
            //将初始化数据和当前的关键帧数据合并后写入到h264文件中
            byte[] keyframe = new byte[encodeBufferInfo.size + configByte.length];
            System.arraycopy(configByte, 0, keyframe, 0, configByte.length);
            //把编码后的视频帧从编码器输出缓冲区中拷贝出来
            System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);
            fos.write(keyframe, 0, keyframe.length);
        } else {
            //写到文件中
            Log.i(LOG_TAG, "非I帧数据编码完成");
            fos.write(outData, 0, outData.length);
        }
        //把筐放回工厂里面
        encodeCodec.releaseOutputBuffer(outputIndex, false);
        break;
}

if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    Log.i(LOG_TAG, "表示当前编解码已经完事了");
    inOutFinish = true;
}

最后把SD卡中保存得264文件通过专门的工具播放出来,此处使用的时雷霄骅大神的VideoEye

音视频6.2——相机采集数据编码成H264_第5张图片

通过MediaMuter输出到h264文件

上面我们通过IO自己将编码好的264码流写入文件中,我们还可以通过MediaMuter来帮我们写入

out264File = new File(demo6Activity.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "cameraMuxer.h264");
out264File.createNewFile();
vedioMuxer = new MediaMuxer(out264File.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

......

int outputIndex = encodeCodec.dequeueOutputBuffer(encodeBufferInfo, 10000);//返回当前筐的标记
switch (outputIndex) {
    case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
        Log.i(LOG_TAG, "输出的format已更改" + encodeCodec.getOutputFormat());
        videoTrackIndex = vedioMuxer.addTrack(encodeCodec.getOutputFormat());
        vedioMuxer.start();//开始合成audio
        break;
    case MediaCodec.INFO_TRY_AGAIN_LATER:
        Log.i(LOG_TAG, "超时,没获取到");
        break;
    case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
        Log.i(LOG_TAG, "输出缓冲区已更改");
        break;
   default:
        Log.i(LOG_TAG, "获取到编码后的数据了,当前解析后的数据长度为:" + encodeBufferInfo.size);
        //获取所有的筐
        ByteBuffer[] outputBuffers = encodeCodec.getOutputBuffers();
        //拿到当前装满火腿肠的筐
        ByteBuffer outputBuffer;
        if (Build.VERSION.SDK_INT >= 21) {
            outputBuffer = encodeCodec.getOutputBuffer(outputIndex);
        } else {
            outputBuffer = outputBuffers[outputIndex];
        }
        //将数据读取到outData中
        byte[] outData = new byte[encodeBufferInfo.size];
        outputBuffer.get(outData);
        //当前是初始化编解码器数据,不是媒体数据,sps、pps等初始化数据
        if (encodeBufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG) {
            configByte = new byte[encodeBufferInfo.size];
            configByte = outData;
        } else if (encodeBufferInfo.flags == BUFFER_FLAG_KEY_FRAME) {//当前的数据中包含关键帧数据
            //将初始化数据和当前的关键帧数据合并后写入到h264文件中
            byte[] keyframe = new byte[encodeBufferInfo.size + configByte.length];
            System.arraycopy(configByte, 0, keyframe, 0, configByte.length);
            //把编码后的视频帧从编码器输出缓冲区中拷贝出来
            System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);

            ByteBuffer newBuffer = ByteBuffer.allocate(keyframe.length);
            newBuffer.put(keyframe);

            videoBufferInfo.size = keyframe.length;
            videoBufferInfo.presentationTimeUs = (System.nanoTime() - startTime) / 1000;
            videoBufferInfo.offset = 0;
            videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
            //通过MediaMuxer写入
            vedioMuxer.writeSampleData(videoTrackIndex, newBuffer, videoBufferInfo);
        } else {
            //写到文件中
            videoBufferInfo.size = encodeBufferInfo.size;
            videoBufferInfo.presentationTimeUs = (System.nanoTime() - startTime) / 1000;
            videoBufferInfo.offset = 0;
            videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
            //通过MediaMuxer写入
            vedioMuxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferInfo);
  }
        //把筐放回工厂里面
        encodeCodec.releaseOutputBuffer(outputIndex, false);
        break;
}
if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    Log.i(LOG_TAG, "表示当前编解码已经完事了");
    inOutFinish = true;
}
     

通过MediaMuter输出的264文件各项数据都时非常正常的,而通过io流写出的数据则从文件大小,时间戳,比特率方面都有一些问题,暂时还没有去研究具体的原因,可以放到后面再深入研究一些

音视频6.2——相机采集数据编码成H264_第6张图片

你可能感兴趣的:(音视频,安卓,音视频,android,java)