Android实现系统级屏幕录制(下)

Android实现系统级屏幕录制

  • 录屏服务的启动
    • 权限申请
    • 服务启动
    • 停止录屏
    • 过滤悬浮窗

录屏服务的启动

点击播放按钮后就开始实现我们的屏幕录制功能,点击结束按钮后,停止录屏功能,并生成音频文件。

权限申请

我们应该知道,屏幕录制需要申请的权限有读写权限、录屏权限。

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实现系统级屏幕录制(上)

你可能感兴趣的:(Android,SystemUI相关)