Android录屏研究

简介

Android录屏可以用在很多方面,比如录制精彩的游戏过程,录制喜欢的视频片段,或是给朋友展示手机桌面等等

常见的录屏软件

AZ Screen Recorder
免费使用,付费增值
Android 5.0以上系统,无须ROOT

ilos screen recorder
免费使用,付费增值
Android 5.0以上系统,无须ROOT

SCR Screen Recorder
免费使用,付费增值
Android系统,需要ROOT

Android录屏API

在Android 4.4 以前,是无法使用录屏功能的,既没有内置工具支持,也没有开放API;

从Android 4.4 KitKat系统开始,通过自带的 ScreenRecord工具支持屏幕录制(截屏保存成mp4),不提供开放API;

从Android 5.0 Lollipop系统开始,提供开放API用于屏幕截取和共享;

使用限制:
仅允许捕获非安全的屏幕内容,不允许捕获系统音频。

注: APP可以通过使用SurfaceView.setSecure()方法来拒绝画面被录制;

不过凡事都有例外,以上方法均是基于非ROOT系统的,一旦成功ROOT,所有的限制就都不存在了。

Android录屏BUG

由于这个BUG出现的几率太高,而且是必现,所以有必要单独拿出来说一下。

在 Android 5.1 系统中,当首次开启屏幕录制功能的时候,系统会弹出一个提示框,英文是”Do not show again”,中文是”不再显示”,不要勾选此对话框,否则会导致UI出问题,如果你不小心勾选了也不要紧,重装一下录屏软件,再次启动就又可以选了。

Android 4.4录屏示例

使用SDK自带的adb工具来完成录屏:

adb shell screenrecord /sdcard/output.mp4

此命令将录制从当前时间开始180秒长度的视频,分辨率为手机屏幕分辨率,码率为4M,视频格式为MP4,存储在SD卡,名字为output.mp4。

注意事项:

  • 输入 –time-limit N,限制视频录制时间为N秒。如果不限制,默认180秒。
  • 输入–size N*N,限制录制视频分辨率为N*N。如果未指定,默认使用手机的分辨率。
  • 输入 –bit-rate,指定视频的比特率为6Mbps。如果不指定,默认为4Mbps。
  • 某些设备可能无法直接录制,原因是分辨率太高。如果遇到此类问题,系统将自行指定较低的分辨率。
  • 不支持录制过程中屏幕旋转,如果录制过程中旋转,有可能画面被切断。
  • 无法同步录制音频。

Android 5.0录屏示例

录屏API位于SDK的 android.media.projection 类下,共有3个子类:

MediaProjection 用于捕获屏幕内容或录制系统声音;
MediaProjection.Callback 用于回调消息;
MediaProjectionManager 用于管理MediaProjection对象;

Android项目示例中包含了一个录屏的示例
MediaProjectionDemo
samples/ApiDemos/src/com/example/android/apis/media/projection/MediaProjectionDemo.java

代码如下

package com.example.android.apis.media.projection;

import com.example.android.apis.R;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.Toast;
import android.widget.ToggleButton;

import java.util.ArrayList;
import java.util.List;

public class MediaProjectionDemo extends Activity {
private static final String TAG = "MediaProjectionDemo";
private static final int PERMISSION_CODE = 1;
// 录屏输出的分辨率
private static final List<Resolution> RESOLUTIONS = new ArrayList<Resolution>() {{
add(new Resolution(640,360));
add(new Resolution(960,540));
add(new Resolution(1366,768));
add(new Resolution(1600,900));
}};

private int mScreenDensity;
private MediaProjectionManager mProjectionManager;

private int mDisplayWidth;
private int mDisplayHeight;
private boolean mScreenSharing;

private MediaProjection mMediaProjection;
private VirtualDisplay mVirtualDisplay;
private Surface mSurface;
private SurfaceView mSurfaceView;
private ToggleButton mToggle;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.media_projection);
// 获取当前屏幕的DPI
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mScreenDensity = metrics.densityDpi;

mSurfaceView = (SurfaceView) findViewById(R.id.surface);
// Surface的获取放在此处并不合适,因为可能为空
// 最好是放在 SurfaceHolder 的回调函数 surfaceCreated() 中赋值。
mSurface = mSurfaceView.getHolder().getSurface();
// 获取录屏管理对名
mProjectionManager =
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);

ArrayAdapter<Resolution> arrayAdapter = new ArrayAdapter<Resolution>(
this, android.R.layout.simple_list_item_1, RESOLUTIONS);
Spinner s = (Spinner) findViewById(R.id.spinner);
s.setAdapter(arrayAdapter);
s.setOnItemSelectedListener(new ResolutionSelector());
s.setSelection(0);

mToggle = (ToggleButton) findViewById(R.id.screen_sharing_toggle);
}

@Override
public void onDestroy() {
super.onDestroy();
if (mMediaProjection != null) {
mMediaProjection.stop();
mMediaProjection = null;
}
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode != PERMISSION_CODE) {
Log.e(TAG, "Unknown request code: " + requestCode);
return;
}
if (resultCode != RESULT_OK) {
Toast.makeText(this,
"User denied screen sharing permission", Toast.LENGTH_SHORT).show();
return;
}
mMediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
mMediaProjection.registerCallback(new MediaProjectionCallback(), null);
mVirtualDisplay = createVirtualDisplay();
}

public void onToggleScreenShare(View view) {
if (((ToggleButton) view).isChecked()) {
shareScreen();
} else {
stopScreenSharing();
}
}

private void shareScreen() {
mScreenSharing = true;
if (mSurface == null) {
return;
}
if (mMediaProjection == null) {
startActivityForResult(mProjectionManager.createScreenCaptureIntent(),
PERMISSION_CODE);
return;
}
mVirtualDisplay = createVirtualDisplay();
}

private void stopScreenSharing() {
if (mToggle.isChecked()) {
mToggle.setChecked(false);
}
mScreenSharing = false;
if (mVirtualDisplay != null) {
mVirtualDisplay.release();
mVirtualDisplay = null;
}
}

private VirtualDisplay createVirtualDisplay() {
return mMediaProjection.createVirtualDisplay("ScreenSharingDemo",
mDisplayWidth, mDisplayHeight, mScreenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mSurface, null /*Callbacks*/, null /*Handler*/);
}

private void resizeVirtualDisplay() {
if (mVirtualDisplay == null) {
return;
}
mVirtualDisplay.resize(mDisplayWidth, mDisplayHeight, mScreenDensity);
}

private class ResolutionSelector implements Spinner.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> parent, View v, int pos, long id) {
Resolution r = (Resolution) parent.getItemAtPosition(pos);
ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
if (getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE) {
mDisplayHeight = r.y;
mDisplayWidth = r.x;
} else {
mDisplayHeight = r.x;
mDisplayWidth = r.y;
}
lp.height = mDisplayHeight;
lp.width = mDisplayWidth;
mSurfaceView.setLayoutParams(lp);
}

@Override
public void onNothingSelected(AdapterView<?> parent) { /* Ignore */ }
}

private class MediaProjectionCallback extends MediaProjection.Callback {
@Override
public void onStop() {
mMediaProjection = null;
stopScreenSharing();
}
}

private class SurfaceCallbacks implements SurfaceHolder.Callback {
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mDisplayWidth = width;
mDisplayHeight = height;
resizeVirtualDisplay();
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurface = holder.getSurface();
if (mScreenSharing) {
shareScreen();
}
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (!mScreenSharing) {
stopScreenSharing();
}
}
}

private static class Resolution {
int x;
int y;

public Resolution(int x, int y) {
this.x = x;
this.y = y;
}

@Override
public String toString() {
return x + "x" + y;
}
}
}

核心的几个方法如下

  • MediaProjectionManager.createScreenCaptureIntent() 生成一个用于屏幕捕获的对象
  • startActivityForResult() 启动屏幕捕获对象
  • onActivityResult() 接收屏幕捕获的返回数据
  • MediaProjection.createVirtualDisplay() 将屏幕内容捕获到 Surface 对象中

参考

Android 5.0 API 屏幕截图和共享
获取Activity的结果