前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android开发笔记(一百三十)截图和录屏

Android开发笔记(一百三十)截图和录屏

作者头像
aqi00
发布2019-01-18 14:52:19
2.8K0
发布2019-01-18 14:52:19
举报
文章被收录于专栏:老欧说安卓老欧说安卓

屏幕捕捉

Android5.0之后开放了屏幕捕捉的API,因此开发者便可以直接通过代码进行截图与录屏,而无需操作系统底层了。屏幕捕捉的功能由MediaProjectionManager媒体投影管理器实现,该管理器的对象从系统服务MEDIA_PROJECTION_SERVICE中获得。注意MediaProjectionManager是Android5.0之后新增的工具,故代码中要补充判断系统版本,如果是4.*及以下版本,则不可处理屏幕捕捉操作。 具体的屏幕捕捉,还要调用媒体投影管理器对象的getMediaProjection方法,获取MediaProjection媒体投影对象。MediaProjection主要有两个方法,说明如下: createVirtualDisplay : 创建虚拟显示层。可分别指定显示层的名称、宽度、高度、密度、标志、渲染表面等等。其中标志通常取值DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,渲染表面则按照截图和录屏两种方式分别取值。 stop : 停止投影。 屏幕捕捉的用途主要是截图和录屏,这有点像摄像头的功能,截图对应拍照,而录屏对应录像。对于拍照和录像,我们知道需要创建一个SurfaceView表面视图做为画面预览层,那么就屏幕捕捉而言,也需要创建一个虚拟显示对象做为投影预览层。这个投影预览层即前面createVirtualDisplay方法返回的VirtualDisplay对象,具体的表面对象则为createVirtualDisplay方法中的渲染表面参数,也就是一个Surface对象。如果当前为截图操作,那么调用ImageReader对象的getSurface方法获得渲染表面;如果当前为录屏操作,那么调用MediaCodec对象的createInputSurface方法获得渲染表面。

截图

给屏幕截图用到了ImageReader,它的常用方法说明如下: newInstance : 静态函数,构造一个图像读取器,可指定图像的宽度、高度、色彩模式,以及图像数量。 getSurface : 获取图像的渲染表面。在实现截图功能时,这里的表面对象要作为createVirtualDisplay方法的输入参数。 acquireLatestImage : 获得最近的一幅图像数据。该方法返回Image对象,需转换为Bitmap格式。 下面是把Image对象转换为Bitmap格式的示例代码:

代码语言:javascript
复制
	public static Bitmap getBitmap(Image image) {
		int width = image.getWidth();
		int height = image.getHeight();
		Image.Plane[] planes = image.getPlanes();
		ByteBuffer buffer = planes[0].getBuffer();
		int pixelStride = planes[0].getPixelStride();
		int rowStride = planes[0].getRowStride();
		int rowPadding = rowStride - pixelStride * width;
		Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride,
				height, Bitmap.Config.ARGB_8888);
		bitmap.copyPixelsFromBuffer(buffer);
		bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
		image.close();
		return bitmap;
	}

截图服务的主要逻辑代码如下所示:

代码语言:javascript
复制
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class CaptureService extends Service implements FloatClickListener {
	private static final String TAG = "CaptureService";
	private MediaProjectionManager mMpMgr;
	private MediaProjection mMP;
	private ImageReader mImageReader;
	private String mImagePath, mImageName;
	private int mScreenWidth, mScreenHeight, mScreenDensity;
	private VirtualDisplay mVirtualDisplay;
	private FloatView mFloatView;

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	@Override
	public void onCreate() {
		super.onCreate();
		mImagePath = Environment.getExternalStoragePublicDirectory(
				Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/ScreenShots/";
		mMpMgr = MainApplication.getInstance().getMpMgr();
		mScreenWidth = DisplayUtil.getSreenWidth(this);
		mScreenHeight = DisplayUtil.getSreenHeight(this);
		mScreenDensity = DisplayUtil.getSreenDensityDpi(this);
		mImageReader = ImageReader.newInstance(mScreenWidth, mScreenHeight, PixelFormat.RGBA_8888, 2);
		if (mFloatView == null) {
			mFloatView = new FloatView(MainApplication.getInstance());
			mFloatView.setLayout(R.layout.float_capture);
		}
		mFloatView.setOnFloatListener(this);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (mFloatView != null && mFloatView.isShow() == false) {
			mFloatView.show();
		}
		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onFloatClick(View v) {
		Toast.makeText(this, "准备截图", Toast.LENGTH_SHORT).show();
		mHandler.postDelayed(mStartVirtual, 100); // 准备屏幕
		mHandler.postDelayed(mCapture, 500); // 进行截图
		mHandler.postDelayed(mStopVirtual, 1000); // 释放屏幕
	}
	
	private Handler mHandler = new Handler();
	private Runnable mStartVirtual = new Runnable() {
		@Override
		public void run() {
			mFloatView.mContentView.setVisibility(View.INVISIBLE);
			if (mMP == null) {
				mMP = mMpMgr.getMediaProjection(MainApplication.getInstance().getResultCode(), 
						MainApplication.getInstance().getResultIntent());
			}
			mVirtualDisplay = mMP.createVirtualDisplay("capture_screen", mScreenWidth, mScreenHeight,
					mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
					mImageReader.getSurface(), null, null);
		}
	};

	private Runnable mCapture = new Runnable() {
		@Override
		public void run() {
			mImageName = Utils.getNowDateTime() + ".png";
			Log.d(TAG, "mImageName=" + mImageName);
			Bitmap bitmap = FileUtil.getBitmap(mImageReader.acquireLatestImage());
			if (bitmap != null) {
				FileUtil.createFile(mImagePath, mImageName);
				FileUtil.saveBitmap(mImagePath+mImageName, bitmap, "PNG", 100);
				Toast.makeText(CaptureService.this, "截图成功:"+mImagePath+mImageName, Toast.LENGTH_SHORT).show();
			} else {
				Toast.makeText(CaptureService.this, "截图失败:未截到屏幕图片", Toast.LENGTH_SHORT).show();
			}
		}
	};

	private Runnable mStopVirtual = new Runnable() {
		@Override
		public void run() {
			mFloatView.mContentView.setVisibility(View.VISIBLE);
			if (mVirtualDisplay != null) {
				mVirtualDisplay.release();
				mVirtualDisplay = null;
			}
		}
	};

	@Override
	public void onDestroy() {
		if (mFloatView != null && mFloatView.isShow() == true) {
			mFloatView.close();
		}
		if (mMP != null) {
			mMP.stop();
		}
		super.onDestroy();
	}

}

录屏

把用户在屏幕上的操作行为录制成视频,我们称之为录屏。因为视频有多种格式,不同格式的编码过程也不相同,所以录屏的过程比起截图要复杂得多,主要功能点简述如下: 1、需要控制何时开始录屏,何时结束录屏; 2、设置视频的编码格式,及其对应的编码过程; 3、指定视频的常见播放参数,如尺寸、位率、帧率、色彩等等; 具体到编码实现上,录屏使用了MediaCodec媒体编码器和MediaMuxer媒体转换器两个工具,通过这两个工具的相互配合,方能完成屏幕录制功能。 下面是媒体编码器MediaCodec的主要方法说明: createEncoderByType : 静态函数,根据编码格式构造一个媒体编码器。编码格式通常取值MediaFormat.MIMETYPE_VIDEO_AVC。 configure : 设置媒体编码的参数,包括视频格式、视频宽高、视频位率、视频帧率等等。 createInputSurface : 创建一个用于输入的表面对象。在实现录屏功能时,这里的表面对象要作为createVirtualDisplay方法的输入参数。 start : 开始编码。 dequeueOutputBuffer : 给输出缓冲区排队。返回该输出缓冲区的索引位置。 getOutputFormat : 获取输出格式。 getOutputBuffer : 根据索引位置获取输出缓冲区的数据。 releaseOutputBuffer : 释放指定索引位置的输出缓冲区。 stop : 停止编码。 release : 释放媒体编码资源。 下面是媒体转换器MediaMuxer的主要方法说明: 构造函数 : 根据文件路径与文件格式构造一个媒体转换器。文件格式通常取值MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4。 addTrack : 把指定格式添加到转换轨道上。返回轨道的索引位置。 start : 开始工作。 writeSampleData : 把编码转换后的数据写入索引位置的轨道。该方法在MediaCodec的getOutputBuffer方法之后调用。 stop : 停止工作。 release : 释放媒体转换资源。 由于截图和录屏可用于捕捉其它App的画面,为了让录屏App在其它界面上也能响应控制操作,因此要把录屏App的控制条做成悬浮窗的样式,通过悬浮窗按钮完成截图或者录屏功能。有关悬浮窗的说明参见《Android开发笔记(一百一十八)自定义悬浮窗》。 录屏服务的主要逻辑代码如下所示:

代码语言:javascript
复制
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RecordService extends Service implements FloatClickListener {
	private static final String TAG = "RecordService";
	private String mVideoPath, mVideoName;
	private MediaProjectionManager mMpMgr;
	private MediaProjection mMP;
	private VirtualDisplay mVirtualDisplay;
	private int mScreenWidth, mScreenHeight, mScreenDensity;

	private MediaCodec mMediaCodec;
	private MediaMuxer mMediaMuxer;
	private boolean isRecording = false, isMuxerStarted = false;
	private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
	private int mVideoTrackIndex = -1;
	private FloatView mFloatView;
	private ImageView iv_record;

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	@Override
	public void onCreate() {
		super.onCreate();
		mVideoPath = Environment.getExternalStoragePublicDirectory(
				Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/ScreenRecords/";
		mMpMgr = MainApplication.getInstance().getMpMgr();
		mScreenWidth = DisplayUtil.getSreenWidth(this);
		mScreenHeight = DisplayUtil.getSreenHeight(this);
		mScreenDensity = DisplayUtil.getSreenDensityDpi(this);
		if (mFloatView == null) {
			mFloatView = new FloatView(MainApplication.getInstance());
			mFloatView.setLayout(R.layout.float_record);
		}
		mFloatView.setOnFloatListener(this);
		iv_record = (ImageView) mFloatView.mContentView.findViewById(R.id.iv_record);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (mFloatView != null && mFloatView.isShow() == false) {
			mFloatView.show();
		}
		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public void onFloatClick(View v) {
		isRecording = !isRecording;
		if (isRecording) {
			iv_record.setImageResource(R.drawable.ic_record_pause);
			Toast.makeText(RecordService.this, "开始录屏", Toast.LENGTH_SHORT).show();
			recordStart();
		} else {
			iv_record.setImageResource(R.drawable.ic_record_begin);
			Toast.makeText(RecordService.this, "结束录屏:"+mVideoPath+mVideoName, Toast.LENGTH_SHORT).show();
		}
	}
	
	private String prepare() {
		MediaFormat format = MediaFormat.createVideoFormat(
				MediaFormat.MIMETYPE_VIDEO_AVC, mScreenWidth, mScreenHeight); //视频格式与宽高
		format.setInteger(MediaFormat.KEY_BIT_RATE, 300*1024*8); //每秒多少位,这里设置每秒300K
		format.setInteger(MediaFormat.KEY_FRAME_RATE, 20); //每秒多少帧,每秒20帧则每帧大小15K
		format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); //设置颜色格式
		format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); //设置关键帧的间隔
		try {
			mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
			mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
			mMediaCodec.start(); //开始视频编码
			return null;
		} catch (Exception e) {
			e.printStackTrace();
			return e.getMessage();
		}
	}

	private void recordStart() {
		String result = prepare();
		if (result != null) {
			Toast.makeText(this, "准备录屏发生异常:"+result, Toast.LENGTH_SHORT).show();
			return;
		}
		if (mMP == null) {
			mMP = mMpMgr.getMediaProjection(MainApplication.getInstance().getResultCode(), 
					MainApplication.getInstance().getResultIntent());
		}
		mVirtualDisplay = mMP.createVirtualDisplay("ScreenRecords", mScreenWidth, mScreenHeight, mScreenDensity, 
				DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaCodec.createInputSurface(), null, null);
		new RecordThread().start();
	}

	private class RecordThread extends Thread {
		@Override
		public void run() {
			try {
				Log.d(TAG, "RecordThread Start");
				FileUtil.createDir(mVideoPath);
				mVideoName = Utils.getNowDateTime() + ".mp4"; //文件格式为MPEG-4
				mMediaMuxer = new MediaMuxer(mVideoPath+mVideoName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
				while (isRecording) {
					int index = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000); //返回缓冲区的索引
					Log.d(TAG, "缓冲区的索引为" + index);
					if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { //输出格式发生变化
						if (isMuxerStarted) {
							throw new IllegalStateException("输出格式已经发生变化");
						}
						MediaFormat newFormat = mMediaCodec.getOutputFormat();
						mVideoTrackIndex = mMediaMuxer.addTrack(newFormat);
						mMediaMuxer.start();
						isMuxerStarted = true;
						Log.d(TAG, "新的输出格式是:"+newFormat.toString()+",媒体转换器的轨道索引是"+mVideoTrackIndex);
					} else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { //请求超时
						Thread.sleep(50);
					} else if (index >= 0) { //正常输出
						if (!isMuxerStarted) {
							throw new IllegalStateException("媒体转换器尚未添加格式轨道");
						}
						encodeToVideo(index);
						mMediaCodec.releaseOutputBuffer(index, false);
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				release();
			}
		}
	}

	private void encodeToVideo(int index) {
		ByteBuffer encoded = mMediaCodec.getOutputBuffer(index);
		if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { //如果不是媒体数据
			mBufferInfo.size = 0;
		}
		if (mBufferInfo.size == 0) { //缓冲区不存在有效数据
			encoded = null;
		} else {
			Log.d(TAG, "缓冲区大小=" + mBufferInfo.size
					+ ", 持续时间=" + mBufferInfo.presentationTimeUs
					+ ", 偏移=" + mBufferInfo.offset);
		}
		if (encoded != null) {
			encoded.position(mBufferInfo.offset);
			encoded.limit(mBufferInfo.offset + mBufferInfo.size);
			mMediaMuxer.writeSampleData(mVideoTrackIndex, encoded, mBufferInfo); //写入视频文件
		}
	}

	private void release() {
		isRecording = false;
		isMuxerStarted = false;
		if (mMediaCodec != null) {
			mMediaCodec.stop();
			mMediaCodec.release();
			mMediaCodec = null;
		}
		if (mVirtualDisplay != null) {
			mVirtualDisplay.release();
			mVirtualDisplay = null;
		}
		if (mMediaMuxer != null) {
			mMediaMuxer.stop();
			mMediaMuxer.release();
			mMediaMuxer = null;
		}
	}

	@Override
	public void onDestroy() {
		release();
		if (mFloatView != null && mFloatView.isShow() == true) {
			mFloatView.close();
		}
		if (mMP != null) {
			mMP.stop();
		}
		super.onDestroy();
	}
}

点此查看Android开发笔记的完整目录

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017年02月06日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 屏幕捕捉
  • 截图
  • 录屏
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档