Android-音视频(7):使用Camera录制视频,并存文件

1.MediaCodec的作用


因为这里会用到,所以先介绍这个的用法。

MediaCodec类用于使用一些基本的多媒体编解码操作。

主要的API如下:

  • getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 
  • queueInputBuffer:输入流入队列 
  • dequeueInputBuffer:从输入流队列中取数据进行编码操作 
  • getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组 
  • dequeueOutputBuffer:从输出队列中取出编码操作之后的数据 
  • releaseOutputBuffer:处理完成,释放ByteBuffer数据 

在编码压缩的过程中,有几个关键的标志如下:

  • BUFFER_FLAG_CODEC_CONFIG   这表明标记为这样的缓冲区包含编解码器初始化/编解码器特定数据,而不是媒体数据   常数:2
  • BUFFER_FLAG_END_OF_STREAM   这意味着流的结束,即在此之后没有缓冲区可用   常数:4
  • BUFFER_FLAG_KEY_FRAME   这表示标记为这样的(编码的)缓冲区包含关键帧的数据   常数:1
  • CONFIGURE_FLAG_ENCODE   如果要将此编解码器用作编码器,则传递此标志   常数:1

 

2.本次代码的主要流程:


  • 1.设置Camera参数,并开启Camera录制
  • 2.收集Camera数据,先将NV21的数据格式转换为NV12,然后用MediaCodec编码为H.264(AVC)并存储到mp4文件。

 

3.解释一些地方


  • 1.AVC是什么?

AVC是一种编码。实际上是 H.264 协议的别名。自从H.264协议中增加了SVC的部分之后,人们习惯将不包含SVC的H.264协议那一部分称为 AVC,而将SVC这一部分单独称为SVC。

  • 2.ArrayBlockingQueue 作用?

ArrayBlockingQueue是一个阻塞式的队列,线程安全,底层以数组的形式保存数据,其所含的对象是以FIFO(先入先出)顺序排序的。

  • 3.YUV420 数据格式到底是什么?

YUV420是一类数据格式的总称。不仅仅只是下面给出的 :

YUV420有平面格式(Planar),即Y、U、V是分开存储的,其中Y为 width*height,而U、V合占Y的一半,该种格式每个像素占12比特。根据U、V的顺序,分出2种格式,U前V后即YUV420P,也叫 I420V前U后,叫YV12(YV表示Y后面跟着V,12表示12bit)。

还有一种半平面格式(Semi-planar),即Y单独占一块地 方,其后U、V紧挨着排在一起,根据U、V的顺序,又有2种,( U前V后叫NV12,在国内好像很多人叫它为YUV420SP格式);V前U后叫 NV21

I420: YYYYYYYY UU VV    =>YUV420P

YV12: YYYYYYYY VV UU    =>

NV12: YYYYYYYY UVUV     =>YUV420SP

NV21: YYYYYYYY VUVU     =>

所以由此得出:YUV420 数据在内存中的长度是 width * hight * 3 / 2 (Y占1、UV占0.5)

我们之后会用到 NV21格式向 NV12格式转换,原因在Android-音视频(5):用 Camera API 采集视频数据并用SurfaceView显示 中有说。

 

4.代码如下:


MainActivity.java

public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {
    //预览用SurfaceView,视频采集用Camera类,视频压缩为H.264(avc)
    Camera camera;
    SurfaceView surfaceView;
    SurfaceHolder surfaceHolder;
    Button stopEncoder;

    int width = 1280;
    int height = 720;
    int framerate = 30; //一秒30帧
    H264Encoder encoder; //自定义的编码操作类


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main5);
        stopEncoder = (Button) findViewById(R.id.stopEncoder);
        surfaceView = (SurfaceView) findViewById(R.id.surface_view);
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(this);

        if (supportH264Codec()){ //查询手机是否支持AVC编码
            Log.e("TAG" , "support H264 hard codec");
        }else {
            Log.e("TAG" , "not support H264 hard codec");
        }

        stopEncoder.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.i("TAG", "surfaceDestroyed Medthod");
                //停止预览,并释放资源
                if (camera != null){
                    camera.setPreviewCallback(null);
                    camera.stopPreview();
                    camera = null;
                }
                if (encoder != null){
                    //停止编码
                    encoder.stopEncoder();
                }
            }
        });

    }

    private boolean supportH264Codec() {
        // 遍历支持的编码格式信息,并查询有没有支持H.264(avc)的编码
        if (Build.VERSION.SDK_INT >= 18){
            //计算可用的编解码器数量
            int number = MediaCodecList.getCodecCount();
            for (int i=number-1 ; i >0 ; i--){
                //获得指定的编解码器信息
                MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
                //得到支持的类型
                String[] types = codecInfo.getSupportedTypes();
                //查询有没有支持H.264(avc)的编码
                for (int j = 0 ; j < types.length ; j++){
                    if (types[j].equalsIgnoreCase("video/avc")){
                        return true;
                    }
                }
            }
        }
        return false;
    }


    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        Log.i("TAG", "surfaceCreated Medthod");
        camera = Camera.open(); //开启相机
        camera.setDisplayOrientation(90);
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewFormat(ImageFormat.NV21); //设置数据格式
        parameters.setPreviewSize(1280,720);
        try{
            camera.setParameters(parameters);
            camera.setPreviewDisplay(surfaceHolder);
            camera.setPreviewCallback(this);
            camera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
        //编码初始化
        encoder = new H264Encoder(width,height,framerate);
        encoder.startEncoder(); //开始编码
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
        Log.i("TAG", "surfaceChanged Medthod");
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        /*Log.i("TAG", "surfaceDestroyed Medthod");
        //停止预览,并释放资源
        if (camera != null){
            camera.setPreviewCallback(null);
            camera.stopPreview();
            camera = null;
        }
        if (encoder != null){
            //停止编码
            encoder.stopEncoder();
        }*/
    }

    @Override
    public void onPreviewFrame(byte[] bytes, Camera camera) {
        //返回相机预览的视频数据,并给H264Encoder编码压缩为H.264(avc)的文件test.mp4
        //这里面的Bytes的数据就是NV21格式的数据
        if (encoder != null){
            encoder.putDate(bytes); //将一帧的数据传过去处理
        }
    }

}

H264Encoder.java

public class H264Encoder {

    private final static int TIMEOUT_USEC = 12000; //超时时间
    private MediaCodec mediaCodec; //核心
    public boolean isRunning = false; //flag
    private int width ,height , framerate;
    public byte[] configbyte;
    private BufferedOutputStream outputStream;
    
    //存储camera返回的视频数据yuv(NV21)
    public ArrayBlockingQueue yuv420queue = new ArrayBlockingQueue(10);

    public H264Encoder(int width, int height, int framerate) {
        this.width = width;
        this.height = height;
        this.framerate = framerate;
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc",width,height);
//设置编码器的数据格式
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); //NV12(YUV420SP)的数据格式
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,width*height*5); //比特率 bite/s
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
//创建编码器
            mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
            mediaCodec.start();
            createfile();//准备存储视频录制数据的文件
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void createfile() {
        String path = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator+"test.mp4";
        File file = new File(path);
        if (file.exists()){
            file.delete();
        }
        try {
            outputStream = new BufferedOutputStream(new FileOutputStream(file));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    //核心
    public void startEncoder() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                isRunning = true;
                byte[] input = null;
                long pts = 0;
                long generateIndex = 0 ;

                while (isRunning){
                    if (yuv420queue.size() > 0){
//得到一帧数据
                        input = yuv420queue.poll();

                        //YUV420 数据在内存中的长度是 width * hight * 3 / 2 (Y占UV占0.5)
                        byte[] yuv420sp = new byte[width*height*3/2];
                        //必须要转换格式,否则视频录得内容播放出来颜色有偏差
                        NV21TONV12(input,yuv420sp,width,height);
                        input = yuv420sp;
                    }
                    if (input != null){
                        try {
                            ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
                            ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
                            int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
                            if (inputBufferIndex >= 0){
//当前帧的时间戳
                                pts = computePresentationTime(generateIndex);
//得到编码的输入缓冲区
                                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                                inputBuffer.clear();
//向缓冲区添加数据
                                inputBuffer.put(input);
//缓冲区数据入编码器
                                mediaCodec.queueInputBuffer(inputBufferIndex,0,input.length,pts,0);
                                generateIndex += 1;
                            }
//定义一个BufferInfo保存outputBufferIndex的帧信息
                            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
                            while (outputBufferIndex >= 0){
//得到输出缓冲区
                                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                                byte[] outData = new byte[bufferInfo.size];
//将数据写入outData
                                outputBuffer.get(outData);
                                if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG){
//关键帧信息或初始化的信息
                                    configbyte = new byte[bufferInfo.size];
                                    configbyte = outData;
                                } else if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_SYNC_FRAME){
//关键帧
                                    byte[] keyframe = new byte[bufferInfo.size + configbyte.length];
                                    System.arraycopy(configbyte,0,keyframe,0,configbyte.length);
                                    System.arraycopy(outData,0,keyframe,configbyte.length,outData.length);
                                    outputStream.write(keyframe,0,keyframe.length);
                                }else {
                                    outputStream.write(outData,0,outData.length);
                                }
//释放输出缓冲区,进行下一次编码操作
                                mediaCodec.releaseOutputBuffer(outputBufferIndex,false);
                                outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }else {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                //编解码完成,停止编解码,并释放资源
                mediaCodec.stop();;
                mediaCodec.release();
                //关闭数据流
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private long computePresentationTime(long frameIndex) {
        return frameIndex * 1000000 / framerate;
    }


    private void NV21TONV12(byte[] nv21, byte[] nv12, int width, int height) {
        if (nv21 == null || nv12 == null) return;
        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 = 10){
            yuv420queue.poll();
        }
        yuv420queue.add(bytes);
    }



}

5.出现的问题


  • 1.播放时发现时间戳不对或没有时间戳,网上找的解释如下,不知道对不对?。

简单来说由于输出的是纯H264(AVC)的文件,是没有时间戳概念的,就是一堆流文件,所以如果手机配置低的话,由于编码时间过长(大于两次帧刷新间隔的话),会导致很多帧数据丢掉,所以最终的流文件播放出来就会短。

  • 2.如果是用前置摄像头录制,预览时正常,播放时图像位置倒过来了,如何解决?

前置摄像头:  camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);

解决办法:

不是camera.setDisplayOrientation(90)的问题,这个不管是前置还是后置,预览正常都需要加上这句话。

手机相机录出来的数据本身就是横着的,要作的处理是,将前置相机回调的图像数据(NV21)旋转270°之后再写入到编码器进行编码,这样输出的H264流就是角度正常的

旋转方法如下:

private byte[] rotateYUV420Degree270(byte[] data, int imageWidth, int imageHeight){
        byte[] yuv =new byte[imageWidth*imageHeight*3/2];
        // Rotate the Y luma
        int i =0;
        for(int x = imageWidth-1;x >=0;x--){
            for(int y =0;y < imageHeight;y++){
                yuv[i]= data[y*imageWidth+x]; 		            i++;
            }   }// Rotate the U and V color components  	i = imageWidth*imageHeight;
        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-1)]; 		         i++; 		       yuv[i]= data[(imageWidth*imageHeight)+(y*imageWidth)+x]; 		            i++;
            }
        }
        return yuv;
    }

使用位置如下:自己在上面的代码找对应的位置

@Override
    public void onPreviewFrame(byte[] bytes, Camera camera) {
        //返回相机预览的视频数据,并给H264Encoder编码,压缩成H.264(avc)格式的文件test.mp4
        //这里面的Bytes的数据就是NV21格式的数据(YUV)
        if (encoder != null) {
            encoder.putDate(rotateYUV420Degree270(bytes,1280,720));
        }
    }

由于旋转了270度后,视频数据的宽高正好对换,所以要在H264Encoder类中中修改初始化MediaCodec时的宽高。

public H264Encoder(int width, int height, int framerate) {
        this.width = width;
        this.height = height;
        this.framerate = framerate;
//在这一行注意将宽高对换,不然视频播放错误
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc",height,width);
        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,30);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
            mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
            mediaCodec.start();
            createfile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

注意:

不管前置还是后置摄像头,录制的视频文件播放时图像都是倒着的,所以都需要上面的解决棒法解决。

 

 

你可能感兴趣的:(Android-音视频,Android音视频学习)