1. 程式人生 > >Android視訊錄製--螢幕錄製

Android視訊錄製--螢幕錄製

上一篇介紹了MediaProjection,這個類可以用來實現安卓螢幕資料的採集,也就是手機一幀幀的截圖,並輸出成byte流的格式。
有興趣的同學可以看這篇:
Android視訊錄製--MediaProjection

但其實只用MediaProjection,並無法生成一個視訊,因為我們得到的只是流,還需要把流編碼成視訊格式。MediaProjection官方的demo裡,也僅僅是把輸出內容放到了surfaceview裡面,在app內部展示。

這次我們就講一下,如何把MediaProjection輸出的流轉化成為視訊。
其實這個過程,我在另外一篇部落格裡面也講過:
android視訊直播-直播流程概述

簡單說一下,一個視訊的生成,最少要有以下兩步:
1. 視訊的採集,比如攝像頭,比如我們講的MediaProjection,這一步最終的輸出,通常是一個流
2. 視訊的編碼壓縮,這一步是對第一步中獲取到的流做處理,編碼可能採用硬編碼,比如h264,也可能採用軟編碼,自己寫編碼邏輯,最終生成的是一個解碼器(也就是我們通常說的播放器)可以解碼(播放)的視訊檔案(比如mp4)

所以MediaProjection其實幫我們實現了第一步,也就是視訊的採集,我們還需要自己來實現視訊的編碼。
所幸Google給我們提供了另外一個類MediaCodec來實現視訊的硬編碼,而不需要我們自己寫太多的邏輯。
廢話不多說,直接上程式碼,首先,我們需要在開始編碼之前,先做一下準備,定義我們要編碼的格式等資訊:

//MediaFormat這個類是用來定義視訊格式相關資訊的
//video/avc,這裡的avc是高階視訊編碼Advanced Video Coding
//mWidth和mHeight是視訊的尺寸,這個尺寸不能超過視訊採集時採集到的尺寸,否則會直接crash
MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);

//COLOR_FormatSurface這裡表明資料將是一個graphicbuffer元資料
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,  MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);  

//設定位元速率,通常位元速率越高,視訊越清晰,但是對應的視訊也越大,這個值我預設設定成了2000000,也就是通常所說的2M,這已經不低了,如果你不想錄制這麼清晰的,你可以設定成500000,也就是500k         
format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate); //設定幀率,通常這個值越高,視訊會顯得越流暢,一般預設我設定成30,你最低可以設定成24,不要低於這個值,低於24會明顯示卡頓 format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); //IFRAME_INTERVAL是指的幀間隔,這是個很有意思的值,它指的是,關鍵幀的間隔時間。通常情況下,你設定成多少問題都不大。 //比如你設定成10,那就是10秒一個關鍵幀。但是,如果你有需求要做視訊的預覽,那你最好設定成1 //因為如果你設定成10,那你會發現,10秒內的預覽都是一個截圖 format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); //建立一個MediaCodec的例項 MediaCodec mEncoder = MediaCodec.createEncoderByType("video/avc"); //定義這個例項的格式,也就是上面我們定義的format,其他引數不用過於關注 mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //這一步非常關鍵,它設定的,是MediaCodec的編碼源,也就是說,我要告訴mEncoder,你給我解碼哪些流。 //很出乎大家的意料,MediaCodec並沒有要求我們傳一個流檔案進去,而是要求我們指定一個surface //而這個surface,其實就是我們在上一講MediaProjection中用來展示螢幕採集資料的surface mSurface = mEncoder.createInputSurface(); mEncoder.start();

關於上面的mSurface,定義和使用的程式碼:

Surface mSurface;
mMediaProjection.createVirtualDisplay(TAG + "-display",
                    mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                    mSurface, null, null);

可以看到,通過上面mSurface的串聯,我們把mMediaProjection的輸出內容放到了mSurface裡面,而mSurface正是mEncoder的輸入源,這樣就完成了對mMediaProjection輸出內容的編碼,也就是螢幕採集資料的編碼。

現在我們搞定編碼的輸入源(mSurface)問題了,下一步我們需要把編碼後的內容輸出到一個檔案中去:

public class AvcEncoder {

private MediaCodec mediaCodec;
private BufferedOutputStream outputStream;

public AvcEncoder() { 
    File f = new File(Environment.getExternalStorageDirectory(), "Download/video_encoded.264");
    touch (f);
    try {
        outputStream = new BufferedOutputStream(new FileOutputStream(f));
        Log.i("AvcEncoder", "outputStream initialized");
    } catch (Exception e){ 
        e.printStackTrace();
    }

    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 320, 240);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();
}

public void close() {
    try {
        mediaCodec.stop();
        mediaCodec.release();
        outputStream.flush();
        outputStream.close();
    } catch (Exception e){ 
        e.printStackTrace();
    }
}

// called from Camera.setPreviewCallbackWithBuffer(...) in other class
public void offerEncoder(byte[] input) {
    try {
        ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
        int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(input);
            mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
        while (outputBufferIndex >= 0) {
            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
            byte[] outData = new byte[bufferInfo.size];
            outputBuffer.get(outData);
            outputStream.write(outData, 0, outData.length);
            Log.i("AvcEncoder", outData.length + " bytes written");

            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);

        }
    } catch (Throwable t) {
        t.printStackTrace();
    }

}