camera開發系列之三 相機數據採集硬編碼h264

章節

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

Camera開發系列之二-相機預覽數據回調java

Camera開發系列之三-相機數據硬編碼爲h264git

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

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

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

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

視頻的播放過程能夠簡單理解爲一幀一幀的畫面按照時間順序呈現出來的過程,就像在一個本子的每一頁畫上畫,而後快速翻動的感受。函數

notebook

有人就說了,這樣不就簡單了,直接把camera獲取到的數據保存成文件,而後播放就好了。還須要編碼不是畫蛇添足麼?你還別說,當初我就是這麼想的,以致於被公司dalao鄙視了很久,終於知道了知識是多麼重要。post

爲何要對Camera獲取到的數據編碼

首先要講一下爲何須要將camera獲取到的yuv數據進行編碼,就拿視頻直播舉例,視頻直播很是注重實時性,實時性就是視頻圖像從產生到消費完成整個過程人感受不到延遲,只要符合這個要求的視頻業務均可以稱爲實時視頻。要實時就要縮短延遲,要縮短延遲就要知道延遲是怎麼產生的,視頻從產生、編碼、傳輸到最後播放消費,各個環節都會產生延遲,整體概括爲下圖: 編碼

  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

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

完整的代碼以下:

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

相關文章
相關標籤/搜索