屏幕录制(一)——MediaProjection 简介

本文讲述使用Android API MediaProjection 录制手机屏幕


一、实现效果

这个Demo主要是实现Android手机屏幕录制的功能,可以实现视频、音频的录制,可以选取录制视频的效果,是否开启音频录制。截图如下:

点击START按钮开始屏幕录制,这里还可以选择标清或高清视频,是否开启音频录制等;点击STOP按钮结束录制。

二、代码分析

整个Demo比较简单,只有两个类:一个是应用程序入口MainActivity,一个是具体实现录制功能的ScreenRecordService。

在MainActivity中,点击START按钮,系统向用户请求屏幕录制的相关权限,这里获取权限其实是调用 mediaProjectionManager.createScreenCaptureIntent()获得一个intent,通过 startActivityForResult(intent) 请求权限。在onActivityResult() 中响应用户动作,获得允许则开始屏幕录制。代码如下,新建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;
    
    /** 是否已经开启视频录制 */
    private boolean isStarted = false;
    /** 是否为标清视频 */
    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;
            }
        });
    }
    
    /**
     * 开启屏幕录制时的UI状态
     */
    private void statusIsStarted() {
        mTextView.setText(R.string.stop_recording);
        mTextView.setBackgroundDrawable(getResources().getDrawable(R.drawable.selector_red_bg));
    }
    
    /**
     * 结束屏幕录制后的UI状态
     */
    private void statusIsStoped() {
        mTextView.setText(R.string.start_recording);
        mTextView.setBackgroundDrawable(getResources().getDrawable(R.drawable.selector_green_bg));
    }
    
    /**
     * 获取屏幕相关数据
     */
    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);
    }
    
    /**
     * 获取屏幕录制的权限
     */
    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) {
                // 获得权限,启动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);
                // 已经开始屏幕录制,修改UI状态
                isStarted = !isStarted;
                statusIsStarted();
                simulateHome(); // this.finish();  // 可以直接关闭Activity
                Log.i(TAG, "Started screen recording");
            } else {
                Toast.makeText(this, R.string.user_cancelled, Toast.LENGTH_LONG).show();
                Log.i(TAG, "User cancelled");
            }
        }
    }
    
    /**
     * 关闭屏幕录制,即停止录制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的实例,通过它才可以获得MediaProjection的实例。然后在createMediaRecorder()方法中创建MediaRecorder的实例,并且完成对它的配置。录制的视频的质量就是在这里配置完成的,主要是setVideoEncodingBitRate(), setVideoEncodingBitRate()这两个方法控制。在配置完成后一定要记得mediaRecorder.prepare(); 并且这行代码必须在创建VirtualDisplay的实例之前调用,否则不能正常获取到 Surface的实例。新建类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;
    /** 是否为标清视频 */
    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() 之后调用,否则报错"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;
    }

}

由上基本就可以实现屏幕录制的功能,但是MediaProjection是在API 21中加入的,所以只能在21以上的手机上使用。在低Android版本的手机上也可以在不root的情况下实现截屏、屏幕录制等功能,但是那都只有应用程序本身占用的屏幕范围,不包括状态栏。最后记得在manifest.xml中加入以下权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

如果你的项目中并不需要录制音频,则 "android.permission.RECORD_AUDIO" 这句可以不要。这里录制的音频只能是来自麦克风的声音,并不能直接录制手机发出的声音,比如电话录音等。

三、补充与总结

屏幕录制的步骤大概可以总结为:1)通过MediaProjectionManager取得向用户申请权限的intent,在onActivityResult()完成对用户动作的响应;2)用户允许后开始录制,可以直接写在一个Activity里,但是像这样另外写在Service里更为妥当,录制的代码也可以单独抽出来写成一个ScreenRecorder的类;3)获取MediaProjection的实例,获取及配置MediaRecorder的实例,并记得MediaRecorder需要prepare();4)获取VirtualDisplay的实例,它也是MediaProjection, MediaRecorder完成交互的地方,录制的屏幕内容其实就是mediaRecorder.getSurface() 获得的 surface 上的内容。

如果不使用MediaProjection + MediaRecorder组合,也可以使用MediaProjection + MediaCodec + MediaMuxer组合实现相同的功能。其中各个类的作用简要总结如下:

MediaMuxer:将音频和视频进行混合生成多媒体文件,输出mp4格式;
MediaCodec:将音视频进行压缩编码,并可以对Surface内容进行编码,实现屏幕录像功能;
MediaExtrator:将音视频分路,和MediaCodec正好反过程;
MediaFormat:用于描述多媒体数据的格式;
MediaRecoder:用于录像并压缩编码,相较于MediaCodec更适合屏幕录像;
MediaPlayer:用于播放压缩编码后的音视频文件;
AudioRecord:用于录制PCM数据;
AudioTrack:用于播放PCM数据; PCM即原始音频采样数据

源码下载请点击这里

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容