您的位置:首页 > 移动开发 > Android开发

Android 使用MediaProjectionManager 完成录屏功能

2018-01-18 10:32 1671 查看

 Android 使用MediaProjectionManager 完成录屏功能

 关于Android 视频录制功能,下文只介绍录制本机屏幕(不是使用摄像头来进行拍摄)

 在Android5.0版本之后,系统给我们提供了**MediaProjectionManager** 和**MediaProjection** 来实现录制视频功能,目前市面的真机基本上都到Android6.0的阶段,所以该功能大家可以放心使用,下面就是具体步骤:
  

1.配置权限以及请求权限(Android6.0)
录屏一般需要两个权限,读写文件权限以及录音权限(视情况而定)

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


2.进行请求系统录屏的回调如果需要录制的内容就在本页面Activity(Fragment)中可以直接请求录屏,如果需要录制的内容是整个App进程中,就需要做些特别的处理,先有一个空白的Activity进行录屏请求,得到回调之后使用WindowManger添加一个View(方便进行交互),我介绍的就是后面一种:

1.空白页面进行录屏请求:
/**
* 实现录屏功能
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class ScreenRecordingActivity extends Activity {
public static final String TAG = "ScreenRecordingActivity";
private static final int STORAGE_REQUEST_CODE = 101;
private static final int RECORD_REQUEST_CODE = 201;
private MediaProjectionManager projectionManager;
private MediaProjection mediaProjection;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
requestPermission();
}

/**
* 请求录屏权限
*/
private void requestPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {  //
ToastUtil.toastLong(this,getString(AFResourceUtil.getStringId(this,"screen_fail_hint")));
LogUtil.e(TAG,"该设配为Android5.0以下接口");
finish();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {   //
boolean isSdCard = AppUtil.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, getString(AFResourceUtil.getStringId(this, "missing_sd_permssion")));
boolean isRecord = AppUtil.checkPermission(this, Manifest.permission.RECORD_AUDIO, "");
if (isSdCard && isRecord) {  //  有权限启动录屏
screenRecording();
} else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, STORAGE_REQUEST_CODE);
}
} else {   //  5.0 和 5.1  进行录屏
screenRecording();
}
}

private void screenRecording() {
Intent captureIntent = projectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, RECORD_REQUEST_CODE);
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RECORD_REQUEST_CODE && resultCode == RESULT_OK) {
LogUtil.e(TAG,"可以进行录屏");
mediaProjection = projectionManager.getMediaProjection(resultCode, data);
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int orientation = getResources().getConfiguration().orientation;
RecorderView.Companion.getInstance().showRecord().setRecorderConfig(mediaProjection,metrics,orientation).startRecorder();
finish();
}else { //
LogUtil.e(TAG,"用户取消了录屏");
finish();
}
}

@TargetApi(Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {//
switch (requestCode) {
case STORAGE_REQUEST_CODE:
LogUtil.e(TAG, "permissions:" + permissions);
LogUtil.e(TAG, "results:" + grantResults);
// 只要不授权就返回
if (grantResults[0] == PackageManager.PERMISSION_GRANTED&&grantResults[1] == PackageManager.PERMISSION_GRANTED) {
screenRecording();
} else {
ToastUtil.toastLong(this, "用户未授权无法录制");
finish();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

}


2.具体实现开始,暂停,重新开始,完成录制,导出文件等功能,录屏视图
/**
*  用于结束录制以及显示录制时间的视图
*  Android5.0 以下的手机不允许使用录屏功能
*/
@Suppress("UNREACHABLE_CODE")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class RecorderView private constructor() {
// 窗口管理器
private lateinit var mWM: WindowManager
// 管理器的参数
private lateinit var mWMParams: WindowManager.LayoutParams
// 上下文
private lateinit var mActivity: Activity
// 主视图
private lateinit var mMainView: LinearLayout
// 结束录制的按钮
private lateinit var mRecordFinishView: ImageView
// 显示录制的时间
private lateinit var mRecordTime: TimerTextView
// 媒体
private lateinit var mediaProjection: MediaProjection
// 录屏类
private lateinit var mediaRecorder: MediaRecorder
// 虚拟屏幕,录屏或者截屏时创建的
private lateinit var virtualDisplay: VirtualDisplay
// 是否在录制
var isRecorder: Boolean = false
// 是否暂停
private var isPause: Boolean = false
private var width: Int = 720
private var height: Int = 1080
private var dpi: Int = 0
private var orientation: Int = 0 // 横竖屏标识
//  当前录制视频视频的路径
private lateinit var currentVideoFilePath: String
//  录制视频的集合
private lateinit var mediaPathList: ArrayList<String>
//  最后录屏完成之后的视频文件路径
private lateinit var saveMediaPath: String

companion object {
val TAG = "RecorderView"
val WINDOW_ORIGINAL_SIZE = 100f    // window 大小
@SuppressLint("StaticFieldLeak")
val instance = RecorderView()
}

/**
*  显示录屏按钮
*/
fun showRecord(): RecorderView {
mActivity = AnFengPaySDK.getInstance().gameActivity  // 使用游戏的activity进行显示
initView()
initParams()
try {
mWM.addView(mMainView, mWMParams)
} catch (e: Exception) {
LogUtil.e(TAG, "添加视图异常:" + e.toString())
}
return this
}

/**
*  初始化录屏参数
*/
private fun initRecorder() {
mediaRecorder = MediaRecorder()
if (dpi >= DisplayMetrics.DENSITY_XHIGH) {  // 视频最大的尺寸 720 * 1280 ,其他视频尺寸使用屏幕大小
width = if (orientation != Configuration.ORIENTATION_LANDSCAPE) 720 else 1280
height = if (orientation != Configuration.ORIENTATION_LANDSCAPE) 1280 else 720
}
mediaRecorder.setOrientationHint(if (orientation != Configuration.ORIENTATION_LANDSCAPE) 0 else 90)
LogUtil.e("record", "当前录屏样式:" + if (orientation != Configuration.ORIENTATION_LANDSCAPE) "竖屏" else "横屏")
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT)  //  音频源
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)  //  视频来源
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) //  视频输出格式
//
currentVideoFilePath = getRecorderDir()
mediaRecorder.setOutputFile(currentVideoFilePath)  // 录制输出文件名
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
mediaRecorder.setMaxDuration(1 * 60 * 1000)               // 设置最大时长5分钟
mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024)   //  设置视频文件的比特率,经过测试该属性对于视频大小影响最大
mediaRecorder.setVideoSize(width, height)
mediaRecorder.setVideoFrameRate(30)
mediaRecorder.setOnErrorListener(OnRecordErrorListener()) // 录制发生错误的监听
mediaRecorder.setOnInfoListener(OnRecordInfoListener()) //
try {
mediaRecorder.prepare()
mediaPathList.add(currentVideoFilePath) // 调用一次该方法就在此处将加入集合
} catch (e: IOException) {
e.printStackTrace()
}
}

/**
* 初始化视图
*/
private fun initView() {
mMainView = AFResourceUtil.inflateViewByXML(mActivity, "window_record_ball") as LinearLayout
mRecordFinishView = mMainView.findViewById(AFResourceUtil.getId(mActivity, "iv_record")) as ImageView
mRecordTime = mMainView.findViewById(AFResourceUtil.getId(mActivity, "tv_time")) as TimerTextView
mRecordFinishView.setOnClickListener(OnRecordListener())
mediaPathList = arrayListOf()
}

/**
* 初始化布局参数
*/
private fun initParams() {
mWM = mActivity.windowManager
mWMParams = WindowManager.LayoutParams()
//noinspection ResourceType
mWMParams.type = WindowManager.LayoutParams.LAST_APPLICATION_WINDOW// 悬浮窗口的层级,暂时调为last层
// 设置悬浮求的背景为透明的
mWMParams.format = PixelFormat.RGBA_8888// 表示透明,下面可以看见
mWMParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
// 设置固定在视图的中上部
mWMParams.gravity = Gravity.C
cc3e
ENTER_HORIZONTAL or Gravity.TOP
// 宽高都设置为50 , 改变
mWMParams.width = SizeUtil.dip2px(mActivity, WINDOW_ORIGINAL_SIZE)
mWMParams.height = mWMParams.width
}

/**
*  设置录屏参数
*/
fun setRecorderConfig(mp: MediaProjection, displayMetrics: DisplayMetrics, orientation: Int): RecorderView {
width = displayMetrics.widthPixels
height = displayMetrics.heightPixels
dpi = displayMetrics.densityDpi
this.orientation = orientation
mediaProjection = mp
return this
}

/**
*  开始录屏
*/
fun startRecorder() {
if (orientation == 0) {
throw  RuntimeException("请先设置录屏参数")
}
if (!isRecorder) {   // 没有录屏时
LogUtil.e(TAG, "开始录屏")
startMedia()
isRecorder = true
mRecordTime.startTimer()
}
}

/**
* 暂停录制
* <p>
*  切换到后台就会触发暂停录制
*  将
* </p>
*/
fun pauseRecorder() {
if (isRecorder && !isPause) {
LogUtil.e(TAG, "执行暂停录制操作")
mRecordTime.pauseTimer()
isPause = true
stopMedia()
}
}

/**
*   重启录屏
*/
fun resumeRecorder() {
if (isRecorder && isPause) {
LogUtil.e(TAG, "重启录屏")
mRecordTime.resumeTimer()
isRecorder = true
isPause = false
startMedia()
}

}

/**
*  完成录制
*/
fun finishRecorder() {
if (isRecorder) {  //
LogUtil.e(TAG, "结束录屏")
isRecorder = false
isPause = false
mRecordTime.stopTimer()
stopMedia()
virtualDisplay.release()    // 结束录屏之后将画布和录屏管理器设置为空
mediaProjection.stop()
// 合并此次录制的所有屏幕
try {
saveMediaPath = getRecorderDir()
VideoUtils.appendMp4List(mediaPathList, saveMediaPath)
mediaPathList.clear()
LogUtil.e(TAG, "完成录制视频文件地址:" + saveMediaPath)
} catch (e: Exception) {
LogUtil.e(TAG, "合并視頻出錯")
}
removeRecorderView()
}
}

/**
*  录屏开始
*/
private fun startMedia() {
initRecorder()
createVirtualDisplay()
mediaRecorder.start()
}

/**
*  停止录屏
*/
private fun stopMedia() {
mediaRecorder.stop()
mediaRecorder.reset()
}

/**
* 移除悬浮按钮
*/
fun removeRecorderView() {
try {
mWM.removeViewImmediate(mMainView)
} catch (e: Exception) {
LogUtil.e(TAG, "异常信息:" + e)
}
}

/**
* 创建虚拟屏幕以进行录屏
*/
private fun createVirtualDisplay() {
// 如果当前屏幕 尺寸 大于 XHIGH  则统一使用 720 * 1280 ,其他就使用本身屏幕的大小
try {
virtualDisplay = mediaProjection.createVirtualDisplay("MainScreen", width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mediaRecorder.surface, null, null)
} catch (e: Exception) {
LogUtil.e(TAG, "创建画布异常:" + e.toString())
}
}

/**
*  获取截屏文件路径
*/
private fun getRecorderDir(): String {
return FileManager.screenRecordCache.path + "/" + "AF_" + AppUtil.getRecorderTime() + ".mp4"
}

inner class OnRecordListener : View.OnClickListener {
override fun onClick(v: View?) {
when (v) {
mRecordFinishView -> {
LogUtil.e(TAG, "停止录屏")
finishRecorder()
}
}
}
}

inner class OnRecordErrorListener : MediaRecorder.OnErrorListener {
override fun onError(mr: MediaRecorder?, what: Int, extra: Int) {
// 发生错误,停止录制
LogUtil.e(TAG, "录屏错误")
}
}

inner class OnRecordInfoListener : MediaRecorder.OnInfoListener {
override fun onInfo(mr: MediaRecorder?, what: Int, extra: Int) {
when (what) {
MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
-> {
LogUtil.e(TAG, "录制达到最大时长")
finishRecorder()
}
}
}
}

}



3.录屏视图xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:id="@+id/iv_record"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/bg_record_finish" />

<com.anfeng.pay.view.TimerTextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="3dp"
android:text="@string/sdk_time"
android:textColor="@android:color/white" />
</LinearLayout>


4.计时器视图

/**
* 计时器视图
* 显示效果如下:
* "09:11"
*/
public class TimerTextView extends TextView {

public static final String TAG = "TimerTextView";
/**
* 是否还在进行及时
*/
private boolean isRun = false;
/**
* 上下文
*/
private Context mContext;
/**
* 计时器
*/
private Timer mTimer;
/**
* 秒数
*/
private int second;
public static final int SET_TEXT=111;
@SuppressLint("HandlerLeak")
Handler mHandler= new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg.what==SET_TEXT){
LogUtil.e("record","设置计时文字:"+second);
setTimerText(timeFormat(second));
}
}
};

public TimerTextView(Context context) {
super(context);
}

public TimerTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public TimerTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
}

public boolean isRun() {
return isRun;
}

/**
* 开始计时
*/
public void startTimer() {
LogUtil.e("record","开始录制计时");
if(null==mTimer){
mTimer = new Timer(true);
}
mTimer.schedule(new TimerTask() {
@Override
public void run() {
second++;
mHandler.sendEmptyMessageAtTime(SET_TEXT,0);
}
},500,1000); // 由于启动录屏需要将近一秒钟
isRun=true;
}

/**
* 恢复计时器
*/
public void resumeTimer(){
startTimer();
}
/**
* 暂停计时
*/
public void pauseTimer() {
isRun=false;
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
mTimer = null;
}
}

/**
* 完成及时,恢复初始化
*/
public void stopTimer() {
pauseTimer();
second=0;
}

private void setTimerText(String time){
this.setText(time);
}
/**
* 时间转换器
* 格式:"mm:ss"
* @return  计时
*/
private String timeFormat(int second) {
int seconds = second % 60;
int minutes = (second / 60) % 60;
int hours = second / 3600;
StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb, Locale.getDefault());
if (hours > 0) {
return formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
} else {
return formatter.format("%02d:%02d", minutes, seconds).toString();
}
}
}


5.视频剪辑类,辅助合并Mp4格式文件的工具类
  Android studio 添加依赖
  

  
compile 'com.googlecode.mp4parser:isoparser:1.1.21'//  视频剪辑工具类
import com.coremedia.iso.boxes.Container;
import com.googlecode.mp4parser.authoring.Movie;
import com.googlecode.mp4parser.authoring.Track;
import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
import com.googlecode.mp4parser.authoring.tracks.AppendTrack;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;

/**
*  视频工具类
*/
public final class VideoUtils {

/**
* 对Mp4文件集合进行追加合并(按照顺序一个一个拼接起来)
*
* @param mp4PathList [输入]Mp4文件路径的集合(支持m4a)(不支持wav)
* @param outPutPath  [输出]结果文件全部名称包含后缀(比如.mp4)
* @throws IOException 格式不支持等情况抛出异常
*/
public static void appendMp4List(List<String> mp4PathList, String outPutPath) throws IOException {
List<Movie> mp4MovieList = new ArrayList<>();// Movie对象集合[输入]
for (String mp4Path : mp4PathList) {// 将每个文件路径都构建成一个Movie对象
mp4MovieList.add(MovieCreator.build(mp4Path));
}
List<Track> audioTracks = new LinkedList<>();// 音频通道集合
List<Track> videoTracks = new LinkedList<>();// 视频通道集合
for (Movie mp4Movie : mp4MovieList) {// 对Movie对象集合进行循环
for (Track inMovieTrack : mp4Movie.getTracks()) {
if ("soun".equals(inMovieTrack.getHandler())) {// 从Movie对象中取出音频通道
audioTracks.add(inMovieTrack);
}
if ("vide".equals(inMovieTrack.getHandler())) {// 从Movie对象中取出视频通道
videoTracks.add(inMovieTrack);
}
}
}
Movie resultMovie = new Movie();// 结果Movie对象[输出]
if (!audioTracks.isEmpty()) {// 将所有音频通道追加合并
resultMovie.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
}
if (!videoTracks.isEmpty()) {// 将所有视频通道追加合并
resultMovie.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
}
Container outContainer = new DefaultMp4Builder().build(resultMovie);// 将结果Movie对象封装进容器
FileChannel fileChannel = new RandomAccessFile(String.format(outPutPath), "rw").getChannel();
outContainer.writeContainer(fileChannel);// 将容器内容写入磁盘
fileChannel.close();
// 合并成功之后将碎片文件合并
for (String mp4Path : mp4PathList) {// 将合并完的文件删除
FileManager.deleteFile(mp4Path);
}
}
}


 总结 系统API 基本上已经跟我们把录屏度封装的很好了,就是一些细节的处理,还有在Android7.0  MediaRecorder() 还实现了onPause  和 onResume 方法 ,但是由于国内7.0的版本还没有那么多,所以就要自行实现这些功能

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐