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的版本还没有那么多,所以就要自行实现这些功能
相关文章推荐
- 关于Android 5.0以上截屏API MediaProjection的使用方式总结
- Android之MediaProjectionManager实现手机截屏总结
- Android MediaProjection 录屏
- Android使用PullToRefresh完成ListView下拉刷新和左滑删除功能
- android中JCMediaManager超级播放器的使用
- Android实现录屏直播(三)MediaProjection + VirtualDisplay + librtmp + MediaCodec实现视频编码并推流到rtmp服务器
- Android MediaProjection 录屏
- Android实现录屏直播(三)MediaProjection + VirtualDisplay + librtmp + MediaCodec实现视频编码并推流到rtmp服务器
- android 4.4 下使用 DisplayManager.createVirtualDisplay 录屏
- Android中使用SharedPreferences完成记住账号密码的功能
- 回发或回调参数无效。在配置中使用 或在页面中使用 启用了事件验证。出于安全目的,此功能验证回发或回调事件的参数是否来源于最初呈现这些事件的服务器控件。如果数据有效并且是预期的,则使用 ClientScriptManager.RegisterForEventValidation 方法来注册回发或回调数据以进行验证。
- C#使用Binding事件完成超越内置类型转换的功能
- 在cxGrid 6 中完成数据录入功能续之如何使用Lookup字段
- 回发或回调参数无效。在配置中使用 或在页面中使用 启用了事件验证。出于安全目的,此功能验证回发或回调事件的参数是否来源于最初呈现这些事件的服务器控件。如果数据有效并且是预期的,则使用 ClientScriptManager.RegisterForEvent
- 在dnn中使用 DNN Text Suggest Control实现自动完成功能
- 在ant中使用cvs功能自动完成每日构建。
- Struts2下使用jsonplugin及jquery完成ajax功能
- 回发或回调参数无效。在配置中使用 或在页面中使用 启用了事件验证。出于安全目的,此功能验证回发或回调事件的参数是否来源于最初呈现这些事件的服务器控件。如果数据有效并且是预期的,则使用 ClientScriptManager.RegisterForEvent
- Atlas学习手记(4):使用AutoComplete Extender实现自动完成功能
- 回发或回调参数无效。在配置中使用 或在页面中使用 启用了事件验证。出于安全目的,此功能验证回发或回调事件的参数是否来源于最初呈现这些事件的服务器控件。如果数据有效并且是预期的,则使用 ClientScriptManager.RegisterForEventValidation 方法来注册回发或回调数据以进行验证。