camera開發系列之三 相機資料採集硬編碼h264

靚仔凌霄發表於2018-08-05

章節

Camera開發系列之一-顯示攝像頭實時畫面

Camera開發系列之二-相機預覽資料回撥

Camera開發系列之三-相機資料硬編碼為h264

Camera開發系列之四-使用MediaMuxer封裝編碼後的音視訊到mp4容器

Camera開發系列之五-使用MediaExtractor製作一個簡易播放器

Camera開發系列之六-使用mina框架實現視訊推流

Camera開發系列之七-使用GLSurfaceviw繪製Camera預覽畫面

視訊的播放過程可以簡單理解為一幀一幀的畫面按照時間順序呈現出來的過程,就像在一個本子的每一頁畫上畫,然後快速翻動的感覺。

notebook

有人就說了,這樣不就簡單了,直接把camera獲取到的資料儲存成檔案,然後播放就行了。還需要編碼不是多此一舉麼?你還別說,當初我就是這麼想的,以至於被公司dalao鄙視了好久,終於知道了知識是多麼重要。

camera開發系列之三 相機資料採集硬編碼h264

為什麼要對Camera獲取到的資料編碼

首先要講一下為什麼需要將camera獲取到的yuv資料進行編碼,就拿視訊直播舉例,視訊直播非常注重實時性,實時性就是視訊影象從產生到消費完成整個過程人感覺不到延遲,只要符合這個要求的視訊業務都可以稱為實時視訊。要實時就要縮短延遲,要縮短延遲就要知道延遲是怎麼產生的,視訊從產生、編碼、傳輸到最後播放消費,各個環節都會產生延遲,總體歸納為下圖:

camera開發系列之三 相機資料採集硬編碼h264

  1. 成像延遲,一般的技術是毫無為力的,涉及到 CCD 相關的硬體,現在市面上最好的 CCD,一秒鐘 50 幀,成像延遲也在 20 毫秒左右,一般的 CCD 只有 20 ~ 25 幀左右,成像延遲 40 ~ 50 毫秒
  2. 編碼延遲,和編碼器有關係,本篇也主要圍繞這個來講。
  3. 實時互動視訊一個關鍵的環節就是網路傳輸技術,不管是早期 VoIP,還是現階段流行的視訊直播,其主要手段是通過 TCP/IP 協議來進行通訊。但是 IP 網路本來就是不可靠的傳輸網路,在這樣的網路傳輸視訊很容易造成卡頓現象和延遲。

我們知道從camera採集到的影象格式一般是YUV格式,這種格式的儲存空間非常大,如果是 1080P 解析度的影象空間:1920 *1080 * 3 /2= 3MB,就算轉換為jpg也需要近200

k大小,如果是每秒12幀也需要近 2.4MB/S的頻寬,這頻寬在公網上傳輸是無法接受的。

視訊編碼器就是為了解決這個問題的,它會根據前後影象的變化做運動檢測,通過各種壓縮把變化的傳送到對方,1080P 進行過 H.264 編碼後頻寬也就在 200KB/S ~ 300KB/S 左右。

結論:

在實際應用中,並不是每一幀都是完整的畫面,因為如果每一幀畫面都是完整的圖片,那麼一個視訊的體積就會很大,這樣對於網路傳輸或者視訊資料儲存來說成本太高,所以通常會對視訊流中的一部分畫面進行壓縮(編碼)處理。

硬編碼又是什麼

所謂的硬編碼就是用GPU對視訊幀進行編碼,相對於軟編碼來說,硬編碼的編碼效率天差地別。更高的編碼效率就意味著在相同幀率下能夠獲得更高的解析度,更佳的畫面質量。但是由於硬編碼和手機硬體平臺相關性較大,目前在部分機型上存在不相容現象,所以並不能完全拋棄軟編碼方案而是作為硬編碼的補充。

初識mediacodec

Android平臺提供了mediacodec類對視訊進行硬編碼 ,MediaCodec類可用於訪問低階媒體編解碼器,即編碼/解碼元件。它是Android低階別多媒體支援基礎設施的一部分(通常一起使用MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.)

#####1 初始化

mediacodec的初始化也非常簡單,主要是下面三個方法:

static MediaCodec createByCodecName(String name);
static MediaCodec createEncoderByType(String type);
static MediaCodec createDecoderByType(String type);
複製程式碼

如果要使用createByCodecName初始化,需要提前知道編解碼器的具體名字,第二個方法和第三個方法分別對應編碼和解碼,根據type建立,開頭以video/打頭,比如h264就是"video/avc" 。

2 配置編解碼引數

初始化之後呼叫如下方法進行配置:

void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
複製程式碼

先看看第一個引數MediaFormat是幹什麼的,官方解釋:

Encapsulates the information describing the format of media data, be it audio or video.

它是音視訊資料格式的簡單描述

The format of the media data is specified as string/value pairs.

這個格式有點特殊,是string/value鍵值對

Keys common to all audio/video formats, all keys not marked optional are mandatory:

鍵一般是音訊/視訊這樣的格式,所有鍵都是必須的

簡而言之,就是配置一些編解碼時的格式,比如幀率,位元速率,顏色空間等等。

第二個引數是指定要在其上呈現此解碼器輸出的view,編碼可以傳入null

第三個引數用於視訊加密

第四個是指定CONFIGURE_FLAG_ENCODE將元件配置為編碼器 ,如果是解碼就傳0

好吧,其實第三個是我隨便說的,我也不知道具體能幹什麼,看官方文件上的解釋也不詳細,哪位知道的觀眾姥爺可以悄悄咪咪的告訴我,我悄悄的修改。

camera開發系列之三 相機資料採集硬編碼h264

完整的程式碼如下:

MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
        //顏色空間設定為yuv420sp
 mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        //位元率,也就是位元速率 ,值越高視訊畫面更清晰畫質更高
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
        //幀率,一般設定為30幀就夠了
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
        //關鍵幀間隔
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        try {
            //初始化mediacodec
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //設定為編碼模式和編碼格式
        mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
        mediaCodec.start();
複製程式碼
3 開始編碼

首先建立一個佇列,將獲取到的視訊資料逐幀的放入:

private static int yuvqueuesize = 10;
private ArrayBlockingQueue<byte[]> YUVQueue = new ArrayBlockingQueue<>(yuvqueuesize);
public void putYUVData(byte[] buffer) {
        if (YUVQueue.size() >= 10) {
            YUVQueue.poll();
        }
        YUVQueue.add(buffer);
    }
複製程式碼

在camera資料回撥方法中呼叫:

Camera.setPreviewCallback(new Camera.PreviewCallback() {
                    @Override
                    public void onPreviewFrame(byte[] data, Camera camera) {
                         //給佇列丟資料
                         putYUVData(data);
                    }
                });
複製程式碼

其次初始化一個輸出流,在建構函式中呼叫,往裡面寫編碼後的資料:

private void createfile(){
        File file = new File(path);
        if(file.exists()){
            file.delete();
        }
        try {
            outputStream = new BufferedOutputStream(new FileOutputStream(file));
        } catch (Exception e){
            e.printStackTrace();
        }
    }
複製程式碼

然後新建一個執行緒從佇列裡取出幀資料進行編碼,需要注意的是我設定的是YUV420SP格式的顏色空間,所以這裡要將NV21轉換為NV12格式的:

public void StartEncoderThread(){
        Thread EncoderThread = new Thread(new Runnable() {
            @SuppressLint("NewApi")
            @Override
            public void run() {
                isRuning = true;
                byte[] input = null;
                long pts =  0;
                long generateIndex = 0;

                while (isRuning) {
                    if (YUVQueue.size() > 0){
                        //從緩衝佇列中取出一幀
                        input = YUVQueue.poll();
                        byte[] yuv420sp = new byte[m_width*m_height*3/2];
                        //把待編碼的視訊幀轉換為YUV420格式
                        NV21ToNV12(input,yuv420sp,m_width,m_height);
                        input = yuv420sp;
                    }
                    if (input != null) {
                        try {
                            long startMs = System.currentTimeMillis();
                            //編碼器輸入緩衝區
                            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();
                                //把轉換後的YUV420格式的視訊幀放到編碼器輸入緩衝區中
                                inputBuffer.put(input);
                                mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
                                generateIndex += 1;
                            }

                            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];
                                outputBuffer.get(outData);
                                if(bufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG){
                                    configbyte = new byte[bufferInfo.size];
                                    configbyte = outData;
                                }else if(bufferInfo.flags == BUFFER_FLAG_KEY_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 (Throwable t) {
                            t.printStackTrace();
                        }
                    } else {
                        try {
                            //這裡可以根據實際情況調整編碼速度
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        EncoderThread.start();
    }
複製程式碼

NV21轉NV21:

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];
        }
    }
複製程式碼

PTS(Presentation Time Stamp):即顯示時間戳,這個時間戳用來告訴播放器該在什麼時候顯示這一幀的資料。雖然PTS 是用於指導播放端的行為,但它們是在編碼的時候由編碼器生成的。 下面是計算pts的方法:

/**
     * 計算pts
     * @param frameIndex
     * @return
     */
    private long computePresentationTime(long frameIndex) {
        return 132 + frameIndex * 1000000 / framerate;
    }

    public boolean isEncodering(){
        return isRuning;
    }
複製程式碼

最後寫一個按鈕,開啟編碼執行緒開始編碼:

mBtnEncoder.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //啟動執行緒編碼
                StartEncoderThread();
            }
        });
複製程式碼

參考連結;

Android攝像頭採集的YUV資料旋轉與映象翻轉

Android 硬解碼MediaCodec配合SurfaceView的踏坑之旅

H.264編碼原理以及I幀B幀P幀

理解音視訊 PTS 和 DTS

專案地址:camera資料採集硬編碼H.264 歡迎start和fork

相關文章