美文网首页Android 音视频开发
屏幕录制(一)——MediaProjection 简介

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

作者: 葛藤湾 | 来源:发表于2016-08-16 17:17 被阅读8301次

    本文讲述使用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即原始音频采样数据

    源码下载请点击这里

    相关文章

      网友评论

        本文标题:屏幕录制(一)——MediaProjection 简介

        本文链接:https://www.haomeiwen.com/subject/arvpsttx.html