1. 程式人生 > >Android-音視訊(7):使用Camera錄製視訊,並存檔案

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)

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<byte[]> yuv420queue = new ArrayBlockingQueue<byte[]>(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 <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];
        }
    }

    public void stopEncoder() {
        isRunning = false;
    }

    public void putDate(byte[] bytes) {
        if (yuv420queue.size() >= 10){
            yuv420queue.poll();
        }
        yuv420queue.add(bytes);
    }



}