屏幕錄制(一)——MediaProjection 簡(jiǎn)介

本文講述使用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ù)

源碼下載請(qǐng)點(diǎn)擊這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末底燎,一起剝皮案震驚了整個(gè)濱河市刃榨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌双仍,老刑警劉巖枢希,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異朱沃,居然都是意外死亡苞轿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門逗物,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搬卒,“玉大人,你說我怎么就攤上這事翎卓∑跹” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵失暴,是天一觀的道長(zhǎng)坯门。 經(jīng)常有香客問我,道長(zhǎng)逗扒,這世上最難降的妖魔是什么古戴? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮矩肩,結(jié)果婚禮上现恼,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好述暂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布痹升。 她就那樣靜靜地躺著建炫,像睡著了一般畦韭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肛跌,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天艺配,我揣著相機(jī)與錄音,去河邊找鬼衍慎。 笑死转唉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的稳捆。 我是一名探鬼主播赠法,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼乔夯!你這毒婦竟也來了砖织?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤末荐,失蹤者是張志新(化名)和其女友劉穎侧纯,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甲脏,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡眶熬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了块请。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娜氏。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墩新,靈堂內(nèi)的尸體忽然破棺而出贸弥,到底是詐尸還是另有隱情,我是刑警寧澤抖棘,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布茂腥,位于F島的核電站,受9級(jí)特大地震影響切省,放射性物質(zhì)發(fā)生泄漏最岗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一朝捆、第九天 我趴在偏房一處隱蔽的房頂上張望般渡。 院中可真熱鬧,春花似錦、人聲如沸驯用。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蝴乔。三九已至记餐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間薇正,已是汗流浹背片酝。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挖腰,地道東北人雕沿。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像猴仑,于是被迫代替她去往敵國(guó)和親审轮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容