点击播放按钮后就开始实现我们的屏幕录制功能,点击结束按钮后,停止录屏功能,并生成音频文件。
我们应该知道,屏幕录制需要申请的权限有读写权限、录屏权限。
framework\base\packages\SystemUI\AndroidManifest.xml
在上一篇的悬浮窗中我们添加了一个播放按钮,对其设置按键监听并启动我们的权限申请专用Activity。
framework\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\FloatScreenRecordedWindow.java
//录屏开启
View.OnClickListener lRecordStartButtonClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
floatScreenRecordedWindowHandler.sendEmptyMessage(START_RECORDED);
}
};
在消息的处理中,我们只调用了一个方法。
case START_RECORDED://开启录制
startScreenRecord();
break;
private void startScreenRecord() {
Intent intent = new Intent(mContext, ScreenRecordActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
在startScreenRecord()方法中我们去启动权限申请的ScreenRecordActivity.java这个类。
framework\base\packages\SystemUI\src\com\android\systemui\ScreenRecordActivity.java
package com.android.systemui;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
public class ScreenRecordActivity extends Activity {
private int mScreenWidth;
private int mScreenHeight;
private int mScreenDensity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window window = getWindow();
window.setGravity(Gravity.LEFT | Gravity.TOP);
WindowManager.LayoutParams params = window.getAttributes();
params.x = 0;
params.y = 0;
params.width = 1; //不让当前权限申请界面过于显目,只点亮1个像素点。
params.height = 1;
window.setAttributes(params);
startRecord();
}
private void startRecord() {
getScreenBaseInfo();
MediaProjectionManager mediaProjectionManager =
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(permissionIntent, 1000);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1000) {
if (resultCode == RESULT_OK) {
//获得录屏权限,启动Service进行录制
Intent intent = new Intent(this, ScreenRecordService.class);
intent.putExtra("resultCode", resultCode);
intent.putExtra("resultData", data);
intent.putExtra("mScreenWidth", mScreenWidth);
intent.putExtra("mScreenHeight", mScreenHeight);
intent.putExtra("mScreenDensity", mScreenDensity);
startService(intent);
}
}
finish();
}
/**
* 获取屏幕基本信息
*/
private void getScreenBaseInfo() {
Display display = getWindowManager().getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
mScreenWidth = metrics.widthPixels;
mScreenHeight = metrics.heightPixels;
mScreenDensity = metrics.densityDpi;
}
}
关于onCreate()中点亮1像素点的原因在于,点击播放开启录屏后,浮现的该弹窗申请页面过于显目,就不让画面过多显示,整体启动也稍微好看点。
屏幕录制的权限申请在Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent(); 我们进入到源码看看。
framework\base\media\java\android\media\projection\MediaProjectionManager.java
/**
* Returns an Intent that must passed to startActivityForResult()
* in order to start screen capture. The activity will prompt
* the user whether to allow screen capture. The result of this
* activity should be passed to getMediaProjection.
*/
public Intent createScreenCaptureIntent() {
Intent i = new Intent();
final ComponentName mediaProjectionPermissionDialogComponent =
ComponentName.unflattenFromString(mContext.getResources().getString(
com.android.internal.R.string
.config_mediaProjectionPermissionDialogComponent));
i.setComponent(mediaProjectionPermissionDialogComponent);
return i;
}
可以看到google对其的解释是通过startActivityForResult()返回intent结果。而这也是我们为什么必须通过activity去申请录屏权限的原因。
在ScreenRecordActivity类的onActivityResult()方法内通过返回结果来startService()。
framework\base\packages\SystemUI\ScreenRecordService.java
package com.android.systemui.screenrecord;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.media.MediaScannerConnection;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Environment;
import android.os.IBinder;
import android.provider.MediaStore;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
import com.android.systemui.R;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import android.os.Environment;
public class ScreenRecordService extends Service {
private int mScreenWidth;
private int mScreenHeight;
private int mScreenDensity;
/**
* 录屏文件存放路径
*/
public static final String SCREEN_RECORD_FILE_PATH = Environment.getExternalStorageDirectory() + "/Pictures/Screenshots/";
private int mResultCode;
private Intent mResultData = null;
private MediaProjection mMediaProjection = null;
private MediaRecorder mMediaRecorder = null;
private VirtualDisplay mVirtualDisplay = null;
private String filePathName = null;
public ScreenRecordService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
mResultCode = intent.getIntExtra("resultCode", -1);
mResultData = intent.getParcelableExtra("resultData");
mScreenWidth = intent.getIntExtra("mScreenWidth", 0);
mScreenHeight = intent.getIntExtra("mScreenHeight", 0);
mScreenDensity = intent.getIntExtra("mScreenDensity", 0);
mMediaProjection = createMediaProjection();
mMediaRecorder = createMediaRecorder();
mVirtualDisplay = createVirtualDisplay(); //必须在mMediaRecorder.prepare()之后调用,否则会报错"fail to get surface"
mMediaRecorder.start();
} catch (Exception e) {
e.printStackTrace();
}
return Service.START_NOT_STICKY;
}
private VirtualDisplay createVirtualDisplay() {
return mMediaProjection.createVirtualDisplay("mediaProjection", mScreenWidth, mScreenHeight, mScreenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null);
}
private MediaRecorder createMediaRecorder() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
//定义输出视频文件的命名
filePathName = SCREEN_RECORD_FILE_PATH + simpleDateFormat.format(new Date()) + ".mp4";
//创建MediaRecprder对象
MediaRecorder mediaRecorder = new MediaRecorder();
//录制视频来源
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
//控制视频输出格式,录制后文件是一个3gp文件,支持音频和视频录制
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
//设置录制的视频编码比特率
mediaRecorder.setVideoEncodingBitRate(5 * mScreenWidth * mScreenHeight);
//设置音频编码格式为高级音频编码
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//设置视频的编码格式
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
//设置要捕获的视频的宽度和高度
mediaRecorder.setVideoSize(mScreenWidth, mScreenHeight);
//设置视频帧率
mediaRecorder.setVideoFrameRate(60);
try {
//设置输出文件的路径
mediaRecorder.setOutputFile(filePathName);
mediaRecorder.prepare();
} catch (Exception e) {
e.printStackTrace();
}
return mediaRecorder;
}
private MediaProjection createMediaProjection() {
return ((MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE)).getMediaProjection(mResultCode, mResultData);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mVirtualDisplay != null) {
mVirtualDisplay.release();
mVirtualDisplay = null;
}
if (mMediaRecorder != null) {
try {
mMediaRecorder.setOnErrorListener(null);
mMediaRecorder.setOnInfoListener(null);
mMediaRecorder.setPreviewDisplay(null);
mMediaRecorder.stop();
} catch (IllegalStateException e) {
e.printStackTrace();
}
mMediaRecorder.release();
mMediaRecorder = null;
}
if (mMediaProjection != null) {
mMediaProjection.stop();
mMediaProjection = null;
}
if (touchState != 1) {
showScreenTouchPositions(0);
}
Toast.makeText(this, R.string.screen_record_finish, Toast.LENGTH_SHORT).show();
//通知媒体库更新内容,否则一开始录制完的视频文件在相册中是找不到的,只有在文件管理播放过后才能出现在相册页面
MediaScannerConnection.scanFile(getApplicationContext(), new String[]{filePathName}, null, null);
}
}
在createMediaRecorder() 方法中主要是对视频的输出格式、编码格式、视频格式等进行一个设置。完成后记得在onDestroy()中释放它们。
同开启录屏一样,只需设置一个动作监听并进行相应操作即可。
framework\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\FloatScreenRecordedWindow.java
//停止录屏
private void stopScreenRecord() {
Intent intent = new Intent(mContext, ScreenRecordService.class);
mContext.stopService(intent);
}
需求明确指出,播放录制出来的视频文件,文件内容不包括有悬浮窗。即录制过程中,单独过滤悬浮窗。这点一开始确实是个难题,后面了解到一切都是起于surface绘制。且我们用的是VirtualDisplay(虚显)。Android支持多个屏幕:主显,外显,和虚显。这里不做过多讲解。所以我们需要在native层修改个文件。
在开始修改native层文件之前需要在悬浮窗的创建地方给予它识别id。
framework\base\packages\SystemUI\src\com\android\systemui\statusbar\phone\FloatScreenRecordedWindow.java
在getLayoutParams()方法中添加
lp.setTitle("ScreenRecordedWindow"); //虚显过滤标识判断
再进入到native层文件。
framework\native\services\surfaceflinger\surfaceflinger.cpp
在该文件的rebuildLayerStacks() 方法中添加判断。
void SurfaceFlinger::rebuildLayerStacks() {
.....
if (!drawRegion.isEmpty()) {
//判断是否是虚显
if(DisplayDevice::DISPLAY_VIRTUAL == displayDevice->getDisplayType()){
string a= layer->getName().string();
//过滤ScreenRecordedWindow的窗体
string::size_type idx = a.find("ScreenRecordedWindow#0");
if(idx == string::npos){
layersSortedByZ.add(layer);
}
}else{
layersSortedByZ.add(layer);
}
}
.....
}
自此,整体的系统屏幕录制的内容就在这了,剩下的比如事件计时,触摸反馈这些内容相对简单,就不说明了,有兴趣也可以交流探讨。
Android实现系统级屏幕录制(上)