本文講述使用Android API MediaProjection 錄制手機(jī)屏幕
一、實(shí)現(xiàn)效果
這個(gè)Demo主要是實(shí)現(xiàn)Android手機(jī)屏幕錄制的功能愕把,可以實(shí)現(xiàn)視頻、音頻的錄制,可以選取錄制視頻的效果,是否開啟音頻錄制弟晚。截圖如下:
點(diǎn)擊START按鈕開始屏幕錄制,這里還可以選擇標(biāo)清或高清視頻逾苫,是否開啟音頻錄制等卿城;點(diǎn)擊STOP按鈕結(jié)束錄制。
二铅搓、代碼分析
整個(gè)Demo比較簡(jiǎn)單瑟押,只有兩個(gè)類:一個(gè)是應(yīng)用程序入口MainActivity,一個(gè)是具體實(shí)現(xiàn)錄制功能的ScreenRecordService星掰。
在MainActivity中多望,點(diǎn)擊START按鈕,系統(tǒng)向用戶請(qǐng)求屏幕錄制的相關(guān)權(quán)限蹋偏,這里獲取權(quán)限其實(shí)是調(diào)用 mediaProjectionManager.createScreenCaptureIntent()獲得一個(gè)intent便斥,通過 startActivityForResult(intent) 請(qǐng)求權(quán)限。在onActivityResult() 中響應(yīng)用戶動(dòng)作威始,獲得允許則開始屏幕錄制。代碼如下像街,新建MainActivity繼承Activity黎棠,向其中加入以下代碼:
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private TextView mTextView;
private static final String RECORD_STATUS = "record_status";
private static final int REQUEST_CODE = 1000;
private int mScreenWidth;
private int mScreenHeight;
private int mScreenDensity;
/** 是否已經(jīng)開啟視頻錄制 */
private boolean isStarted = false;
/** 是否為標(biāo)清視頻 */
private boolean isVideoSd = true;
/** 是否開啟音頻錄制 */
private boolean isAudio = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i(TAG, "onCreate");
if(savedInstanceState != null) {
isStarted = savedInstanceState.getBoolean(RECORD_STATUS);
}
getView() ;
getScreenBaseInfo();
}
private void getView() {
mTextView = (TextView) findViewById(R.id.button_control);
if(isStarted) {
statusIsStarted();
} else {
statusIsStoped();
}
mTextView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if(isStarted) {
stopScreenRecording();
statusIsStoped();
Log.i(TAG, "Stoped screen recording");
} else {
startScreenRecording();
}
}
});
RadioGroup radioGroup = (RadioGroup) findViewById(R.id.radio_group);
radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
// TODO Auto-generated method stub
switch (checkedId) {
case R.id.sd_button:
isVideoSd = true;
break;
case R.id.hd_button:
isVideoSd = false;
break;
default:
break;
}
}
});
CheckBox audioBox = (CheckBox) findViewById(R.id.audio_check_box);
audioBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// TODO Auto-generated method stub
isAudio = isChecked;
}
});
}
/**
* 開啟屏幕錄制時(shí)的UI狀態(tài)
*/
private void statusIsStarted() {
mTextView.setText(R.string.stop_recording);
mTextView.setBackgroundDrawable(getResources().getDrawable(R.drawable.selector_red_bg));
}
/**
* 結(jié)束屏幕錄制后的UI狀態(tài)
*/
private void statusIsStoped() {
mTextView.setText(R.string.start_recording);
mTextView.setBackgroundDrawable(getResources().getDrawable(R.drawable.selector_green_bg));
}
/**
* 獲取屏幕相關(guān)數(shù)據(jù)
*/
private void getScreenBaseInfo() {
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mScreenWidth = metrics.widthPixels;
mScreenHeight = metrics.heightPixels;
mScreenDensity = metrics.densityDpi;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
outState.putBoolean(RECORD_STATUS, isStarted);
}
/**
* 獲取屏幕錄制的權(quán)限
*/
private void startScreenRecording() {
// TODO Auto-generated method stub
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(permissionIntent, REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// TODO Auto-generated method stub
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == REQUEST_CODE) {
if(resultCode == RESULT_OK) {
// 獲得權(quán)限晋渺,啟動(dòng)Service開始錄制
Intent service = new Intent(this, ScreenRecordService.class);
service.putExtra("code", resultCode);
service.putExtra("data", data);
service.putExtra("audio", isAudio);
service.putExtra("width", mScreenWidth);
service.putExtra("height", mScreenHeight);
service.putExtra("density", mScreenDensity);
service.putExtra("quality", isVideoSd);
startService(service);
// 已經(jīng)開始屏幕錄制,修改UI狀態(tài)
isStarted = !isStarted;
statusIsStarted();
simulateHome(); // this.finish(); // 可以直接關(guān)閉Activity
Log.i(TAG, "Started screen recording");
} else {
Toast.makeText(this, R.string.user_cancelled, Toast.LENGTH_LONG).show();
Log.i(TAG, "User cancelled");
}
}
}
/**
* 關(guān)閉屏幕錄制脓斩,即停止錄制Service
*/
private void stopScreenRecording() {
// TODO Auto-generated method stub
Intent service = new Intent(this, ScreenRecordService.class);
stopService(service);
isStarted = !isStarted;
}
/**
* 模擬HOME鍵返回桌面的功能
*/
private void simulateHome() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_HOME);
this.startActivity(intent);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 在這里將BACK鍵模擬了HOME鍵的返回桌面功能(并無必要)
if(keyCode == KeyEvent.KEYCODE_BACK) {
simulateHome();
return true;
}
return super.onKeyDown(keyCode, event);
}
}
在ScreenRecordService中木西,第一步需要獲得MediaProjectionManager的實(shí)例,通過它才可以獲得MediaProjection的實(shí)例随静。然后在createMediaRecorder()方法中創(chuàng)建MediaRecorder的實(shí)例八千,并且完成對(duì)它的配置。錄制的視頻的質(zhì)量就是在這里配置完成的燎猛,主要是setVideoEncodingBitRate(), setVideoEncodingBitRate()這兩個(gè)方法控制恋捆。在配置完成后一定要記得mediaRecorder.prepare(); 并且這行代碼必須在創(chuàng)建VirtualDisplay的實(shí)例之前調(diào)用,否則不能正常獲取到 Surface的實(shí)例重绷。新建類ScreenRecordService繼承Service沸停,在其中加入以下代碼:
public class ScreenRecordService extends Service {
private static final String TAG = "ScreenRecordingService";
private int mScreenWidth;
private int mScreenHeight;
private int mScreenDensity;
private int mResultCode;
private Intent mResultData;
/** 是否為標(biāo)清視頻 */
private boolean isVideoSd;
/** 是否開啟音頻錄制 */
private boolean isAudio;
private MediaProjection mMediaProjection;
private MediaRecorder mMediaRecorder;
private VirtualDisplay mVirtualDisplay;
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
Log.i(TAG, "Service onCreate() is called");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// TODO Auto-generated method stub
Log.i(TAG, "Service onStartCommand() is called");
mResultCode = intent.getIntExtra("code", -1);
mResultData = intent.getParcelableExtra("data");
mScreenWidth = intent.getIntExtra("width", 720);
mScreenHeight = intent.getIntExtra("height", 1280);
mScreenDensity = intent.getIntExtra("density", 1);
isVideoSd = intent.getBooleanExtra("quality", true);
isAudio = intent.getBooleanExtra("audio", true);
mMediaProjection = createMediaProjection();
mMediaRecorder = createMediaRecorder();
mVirtualDisplay = createVirtualDisplay(); // 必須在mediaRecorder.prepare() 之后調(diào)用,否則報(bào)錯(cuò)"fail to get surface"
mMediaRecorder.start();
return Service.START_NOT_STICKY;
}
private MediaProjection createMediaProjection() {
Log.i(TAG, "Create MediaProjection");
return ((MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE)).getMediaProjection(mResultCode, mResultData);
}
private MediaRecorder createMediaRecorder() {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
Date curDate = new Date(System.currentTimeMillis());
String curTime = formatter.format(curDate).replace(" ", "");
String videoQuality = "HD";
if(isVideoSd) videoQuality = "SD";
Log.i(TAG, "Create MediaRecorder");
MediaRecorder mediaRecorder = new MediaRecorder();
if(isAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) + "/" + videoQuality + curTime + ".mp4");
mediaRecorder.setVideoSize(mScreenWidth, mScreenHeight); //after setVideoSource(), setOutFormat()
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); //after setOutputFormat()
if(isAudio) mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); //after setOutputFormat()
int bitRate;
if(isVideoSd) {
mediaRecorder.setVideoEncodingBitRate(mScreenWidth * mScreenHeight);
mediaRecorder.setVideoFrameRate(30);
bitRate = mScreenWidth * mScreenHeight / 1000;
} else {
mediaRecorder.setVideoEncodingBitRate(5 * mScreenWidth * mScreenHeight);
mediaRecorder.setVideoFrameRate(60); //after setVideoSource(), setOutFormat()
bitRate = 5 * mScreenWidth * mScreenHeight / 1000;
}
try {
mediaRecorder.prepare();
} catch (IllegalStateException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Log.i(TAG, "Audio: " + isAudio + ", SD video: " + isVideoSd + ", BitRate: " + bitRate + "kbps");
return mediaRecorder;
}
private VirtualDisplay createVirtualDisplay() {
Log.i(TAG, "Create VirtualDisplay");
return mMediaProjection.createVirtualDisplay(TAG, mScreenWidth, mScreenHeight, mScreenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null);
}
@Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
Log.i(TAG, "Service onDestroy");
if(mVirtualDisplay != null) {
mVirtualDisplay.release();
mVirtualDisplay = null;
}
if(mMediaRecorder != null) {
mMediaRecorder.setOnErrorListener(null);
mMediaProjection.stop();
mMediaRecorder.reset();
}
if(mMediaProjection != null) {
mMediaProjection.stop();
mMediaProjection = null;
}
}
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
}
由上基本就可以實(shí)現(xiàn)屏幕錄制的功能昭卓,但是MediaProjection是在API 21中加入的愤钾,所以只能在21以上的手機(jī)上使用。在低Android版本的手機(jī)上也可以在不root的情況下實(shí)現(xiàn)截屏候醒、屏幕錄制等功能能颁,但是那都只有應(yīng)用程序本身占用的屏幕范圍,不包括狀態(tài)欄倒淫。最后記得在manifest.xml中加入以下權(quán)限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
如果你的項(xiàng)目中并不需要錄制音頻劲装,則 "android.permission.RECORD_AUDIO" 這句可以不要。這里錄制的音頻只能是來自麥克風(fēng)的聲音昌简,并不能直接錄制手機(jī)發(fā)出的聲音占业,比如電話錄音等。
三纯赎、補(bǔ)充與總結(jié)
屏幕錄制的步驟大概可以總結(jié)為:1)通過MediaProjectionManager取得向用戶申請(qǐng)權(quán)限的intent谦疾,在onActivityResult()完成對(duì)用戶動(dòng)作的響應(yīng);2)用戶允許后開始錄制犬金,可以直接寫在一個(gè)Activity里念恍,但是像這樣另外寫在Service里更為妥當(dāng),錄制的代碼也可以單獨(dú)抽出來寫成一個(gè)ScreenRecorder的類晚顷;3)獲取MediaProjection的實(shí)例峰伙,獲取及配置MediaRecorder的實(shí)例,并記得MediaRecorder需要prepare()该默;4)獲取VirtualDisplay的實(shí)例瞳氓,它也是MediaProjection, MediaRecorder完成交互的地方,錄制的屏幕內(nèi)容其實(shí)就是mediaRecorder.getSurface() 獲得的 surface 上的內(nèi)容栓袖。
如果不使用MediaProjection + MediaRecorder組合匣摘,也可以使用MediaProjection + MediaCodec + MediaMuxer組合實(shí)現(xiàn)相同的功能店诗。其中各個(gè)類的作用簡(jiǎn)要總結(jié)如下:
MediaMuxer:將音頻和視頻進(jìn)行混合生成多媒體文件,輸出mp4格式音榜;
MediaCodec:將音視頻進(jìn)行壓縮編碼庞瘸,并可以對(duì)Surface內(nèi)容進(jìn)行編碼,實(shí)現(xiàn)屏幕錄像功能赠叼;
MediaExtrator:將音視頻分路擦囊,和MediaCodec正好反過程;
MediaFormat:用于描述多媒體數(shù)據(jù)的格式嘴办;
MediaRecoder:用于錄像并壓縮編碼瞬场,相較于MediaCodec更適合屏幕錄像;
MediaPlayer:用于播放壓縮編碼后的音視頻文件户辞;
AudioRecord:用于錄制PCM數(shù)據(jù)泌类;
AudioTrack:用于播放PCM數(shù)據(jù); PCM即原始音頻采樣數(shù)據(jù)