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,謝謝各位看官!
有興趣的朋友還可以關注下公衆號,聽我給你們講段子。