android 錄屏方案 VFR和CFR

      android 5.0錄屏的例子網上滿天飛,我這裏主要是總結一下,如何以VFR和CFR的方式來錄屏。寫這篇文章主要是因爲我在做這個功能的過程中,在網上找了很久也沒有找到一個固定幀率錄屏的例子,後面還是在全球最大同性交友網站github找到了解決方案。好了,閒話不多說,下面來說下具體的解決方案。

     首先了解一下VFR和CFR的概念:

              VFR(可變幀率)
     VFR 模式是一個非常好用的模式,使用這個模式,可以錄製這個視頻最低的 FPS 幀數,比如(您設置的FPS 是 60,但是您錄製的這個視頻,在某一個時間段這個畫面都不會動,那麼選擇這個模式就可以記錄60幀數以下的幀數,從而節省資源損耗,錄製的體積也變小)

             CFR(恆定幀率)
      VFR 比 CFR 好用,但是一些視頻編輯軟件,卻不支持 VFR,比如:Adobe Premiere 就不支持VFR,所以如果您選擇的是用 Adobe Premiere 作爲後期製作軟件,那麼您必須要選擇 CFR 這個幀率模式。如果您選擇的FPS 爲60 ,幀率模式選擇的是CFR,那麼您錄製的視頻就是一個持續擁有FPS 爲60的視頻文件。 
        

      我從網上找了很多例子,都是可變幀率的,一個類搞定:

package com.jxd.jxdcamerapro.screen;

import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.projection.MediaProjection;
import android.util.Log;
import android.view.Surface;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 非固定幀率錄製
 */
public class ScreenRecorder extends Thread {
    private static final String TAG = "ScreenRecorder";

    private int mWidth;
    private int mHeight;
    private int mBitRate;
    private int mDpi;
    private String mDstPath;
    private MediaProjection mMediaProjection;
    // parameters for the encoder
    private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
    private static final int FRAME_RATE = 8; // 30 fps
    private static final int IFRAME_INTERVAL = 10; // 10 seconds between I-frames
    private static final int TIMEOUT_US = 10000;

    private MediaCodec mEncoder;
    private Surface mSurface;
    private MediaMuxer mMuxer;
    private boolean mMuxerStarted = false;
    private int mVideoTrackIndex = -1;
    private AtomicBoolean mQuit = new AtomicBoolean(false);
    private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
    private VirtualDisplay mVirtualDisplay;

    public ScreenRecorder(int width, int height, int bitrate, int dpi, MediaProjection mp, String dstPath) {
        super(TAG);
        mWidth = width;
        mHeight = height;
        mBitRate = bitrate;
        mDpi = dpi;
        mMediaProjection = mp;
        mDstPath = dstPath;
    }

    /**
     * stop task
     */
    public final void quit() {
        mQuit.set(true);
    }

    @Override
    public void run() {
        try {
            try {
                prepareEncoder();
                mMuxer = new MediaMuxer(mDstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG + "-display",
                    mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                    mSurface, null, null);
            Log.d(TAG, "created virtual display: " + mVirtualDisplay);
            recordVirtualDisplay();

        } finally {
            release();
        }
    }

    private void recordVirtualDisplay() {
        while (!mQuit.get()) {
            int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
            Log.i(TAG, "dequeue output buffer index=" + index);
            if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                resetOutputFormat();

            } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
                Log.d(TAG, "retrieving buffers time out!");
                try {
                    // wait 10ms
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
            } else if (index >= 0) {

                if (!mMuxerStarted) {
                    throw new IllegalStateException("MediaMuxer dose not call addTrack(format) ");
                }
                encodeToVideoTrack(index);

                mEncoder.releaseOutputBuffer(index, false);
            }
        }
    }

    private void encodeToVideoTrack(int index) {
        ByteBuffer encodedData = mEncoder.getOutputBuffer(index);

        if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
            // The codec config data was pulled out and fed to the muxer when we got
            // the INFO_OUTPUT_FORMAT_CHANGED status.
            // Ignore it.
            Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
            mBufferInfo.size = 0;
        }
        if (mBufferInfo.size == 0) {
            Log.d(TAG, "info.size == 0, drop it.");
            encodedData = null;
        } else {
            Log.d(TAG, "got buffer, info: size=" + mBufferInfo.size
                    + ", presentationTimeUs=" + mBufferInfo.presentationTimeUs
                    + ", offset=" + mBufferInfo.offset);
        }
        if (encodedData != null) {
            encodedData.position(mBufferInfo.offset);
            encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
            mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
            Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer...");
        }
    }

    private void resetOutputFormat() {
        // should happen before receiving buffers, and should only happen once
        if (mMuxerStarted) {
            throw new IllegalStateException("output format already changed!");
        }
        MediaFormat newFormat = mEncoder.getOutputFormat();

        Log.i(TAG, "output format changed.\n new format: " + newFormat.toString());
        mVideoTrackIndex = mMuxer.addTrack(newFormat);
        mMuxer.start();
        mMuxerStarted = true;
        Log.i(TAG, "started media muxer, videoIndex=" + mVideoTrackIndex);
    }

    private void prepareEncoder() throws IOException {

        //MediaFormat這個類是用來定義視頻格式相關信息的
        //video/avc,這裏的avc是高級視頻編碼Advanced Video Coding
        //mWidth和mHeight是視頻的尺寸,這個尺寸不能超過視頻採集時採集到的尺寸,否則會直接crash
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
        //COLOR_FormatSurface這裏表明數據將是一個graphicbuffer元數據
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        //設置碼率,通常碼率越高,視頻越清晰,但是對應的視頻也越大,這個值我默認設置成了2000000,也就是通常所說的2M
        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);

        Log.d(TAG, "created video format: " + format);
        //創建一個MediaCodec的實例
        mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        //定義這個實例的格式,也就是上面我們定義的format,其他參數不用過於關注
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //這一步非常關鍵,它設置的,是MediaCodec的編碼源,也就是說,我要告訴mEncoder,你給我解碼哪些流。
        //很出乎大家的意料,MediaCodec並沒有要求我們傳一個流文件進去,而是要求我們指定一個surface
        //而這個surface,其實就是我們在上一講MediaProjection中用來展示屏幕採集數據的surface
        mSurface = mEncoder.createInputSurface();
        Log.d(TAG, "created input surface: " + mSurface);
        mEncoder.start();
    }

    private void release() {
        if (mEncoder != null) {
            mEncoder.stop();
            mEncoder.release();
            mEncoder = null;
        }
        if (mVirtualDisplay != null) {
            mVirtualDisplay.release();
        }
        if (mMediaProjection != null) {
            mMediaProjection.stop();
        }
        if (mMuxer != null) {
            mMuxer.stop();
            mMuxer.release();
            mMuxer = null;
        }
    }
}

然後就是它的調用:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void startScreenRecord() {
         mediaProjectionManager = (MediaProjectionManager)
                getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        if (mediaProjectionManager != null){
            Intent intent = mediaProjectionManager.createScreenCaptureIntent();
            PackageManager packageManager = getPackageManager();
            if (packageManager.resolveActivity(intent,PackageManager.MATCH_DEFAULT_ONLY) != null){
                //存在錄屏授權的Activity
                startActivityForResult(intent,START_RECORD_CODE);
            }else {
                toastShort("無法錄製");
            }
        }
    }
  @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(bCFRMode){
            startCFRRecording(resultCode,data);
            return;
        }

        MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
        if (mediaProjection == null) {
            Log.e("@@", "media projection is null");
            return;
        }
        if (requestCode == START_RECORD_CODE && resultCode == Activity.RESULT_OK){
            try {
                final int width = 360;
                final int height = 640;
                File file = new File(Environment.getExternalStorageDirectory() + "/"
                        + "1ScreenRecorder" + "/ScreenRecorder-" + width + "x" + height + "-"
                        + ".mp4");
                File dirs = new File(file.getParent());
                if (!dirs.exists())
                    dirs.mkdirs();
                try {
                    file.createNewFile();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
//        File file = new File(Environment.getExternalStorageDirectory(),
//                "record-" + width + "x" + height + "-" + System.currentTimeMillis() + ".mp4");
                final int bitrate = 1024*512;
                mRecorder = new ScreenRecorder(width, height, bitrate, 1, mediaProjection, file.getAbsolutePath());
                mRecorder.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            toastShort("拒絕錄屏");
        }
    }

然後就是固定幀率錄製的方案:主要就是採用了opengl的方式來獲取屏幕內容,由於這一塊內容稍微多一點,不適合全部貼出來,所以麻煩各位移步那啥hub,https://github.com/jingxiongdi/JXDCameraPro,謝謝各位看官!

   有興趣的朋友還可以關注下公衆號,聽我給你們講段子。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章