安卓提供了一个屏幕投影服务(Media Projection Service),可用于将屏幕影像投影到虚拟显示设备(Surface)
利用这个服务,我们可以对屏幕进行截图和录像
屏幕截图
final private static int code = 10086;
//截图
MediaProjectionManager manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(manager.createScreenCaptureIntent(), code);
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode != code || resultCode != Activity.RESULT_OK) return;
//获取屏幕投影服务
MediaProjectionManager manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
MediaProjection projection = manager.getMediaProjection(resultCode, data);
//获取屏幕大小
int w = getWindow().getDecorView().getWidth();
int h = getWindow().getDecorView().getHeight();
int dpi = Device.getScreenDpi(ctx);
//将屏幕投影到虚拟显示设备(ImageReader)
//ImageReader可以保存多个帧的数据,由于我们截图只需要一帧,所以最大图片数量设置为1
ImageReader imageReader = ImageReader.newInstance(w, h, PixelFormat.RGBA_8888, 1);
VirtualDisplay display = projection.createVirtualDisplay(
"ScreenCapture",
w,
h,
dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader.getSurface(),
null,
null
);
//获取图像回调
imageReader.setOnImageAvailableListener(reader -> {
//获取图像数据
Image image = imageReader.acquireLatestImage();
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
//Image转Bitmap,并进行修剪
//ImageReader为了保持字节对齐,字节集中会有空的字节数据
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * w;
Bitmap bitmap = Bitmap.createBitmap(w + rowPadding / pixelStride, h, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap = Bitmap.createBitmap(bitmap, rowPadding / pixelStride / 2, 0, w, h);
image.close();
//写入文件
Bitmaps.writeBitmapToFile(bitmap, "sdcard/001/1.png", 100);
//停止投影,释放资源
//如果不停止,会一直向ImageReader写入图片
display.release();
projection.stop();
//消息提示
TipBox.tip("截图成功");
}, null);
}
屏幕录像
屏幕录像和屏幕截图的原理是一致的,但是视频需要通过MediaCodec编码为H264,再通过MediaMuxer封装为MP4
代码中的VideoPlayer是我自己的界面控件,大家可以无视,另外代码中用到了一些简化工具,大家需要适当调整才能使用,并不影响核心代码
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import com.easing.commons.android.app.CommonActivity;
import com.easing.commons.android.io.Files;
import com.easing.commons.android.manager.Device;
import com.easing.commons.android.thread.Threads;
import com.easing.commons.android.ui.dialog.TipBox;
import java.nio.ByteBuffer;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.vov.vitamio.core.VideoPlayer;
import lombok.SneakyThrows;
@SuppressWarnings("all")
public class LoginActivity extends CommonActivity<LoginActivity> {
@BindView(R.id.bt1)
View bt1;
@BindView(R.id.bt2)
View bt2;
@BindView(R.id.bt3)
View bt3;
@BindView(R.id.v)
VideoPlayer view;
final private static int code = 10086;
final private static String path = "sdcard/001/1.mp4";
boolean recording = false;
boolean working = false;
int w;
int h;
int dpi;
MediaProjectionManager manager;
MediaProjection projection;
Surface surface;
VirtualDisplay display;
MediaCodec mediaCodec;
MediaMuxer mediaMuxer;
MediaCodec.BufferInfo bufferInfo;
Integer videoTrackIndex;
protected void create() {
setContentView(R.layout.activity_main);
ButterKnife.bind(this, ctx);
//申请存储卡权限
requestAllPermissionWithCallback();
}
@Override
protected void onPermissionOk() {
view.showControlPane(false);
view.url("http://47.107.180.190:8657/07201811130443:1:1/stream");
bt1.setOnClickListener(v -> {
});
bt2.setOnClickListener(v -> {
if (working)
stopRecord();
else
startRecord();
});
bt3.setOnClickListener(v -> {
APP.ctx.finishProcess();
});
}
@SneakyThrows
private void startRecord() {
//获取屏幕信息
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
w = windowManager.getDefaultDisplay().getWidth();
h = windowManager.getDefaultDisplay().getHeight();
dpi = Device.getScreenDpi(ctx);
//配置编码器
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", w, h);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 6000000);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);
mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = mediaCodec.createInputSurface();
mediaCodec.start();
//开始屏幕投影服务
manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(manager.createScreenCaptureIntent(), code);
TipBox.tip("开始录制");
working = true;
}
private void stopRecord() {
//注意这里是将recording置为false,而不是working置为false
//等资源释放完毕时,才会自动将working置为false
//这样当我们快速练连点录制按钮时,就不会出现资源尚未释放,又开始重新录制的问题
//这就是我们为什么引入recording和working两个控制变量的原因
//recording=false表示我们命令工作停止,working=false表示实际工作已经完全停止
//大家在处理耗时指令和状态切换任务时,注意利用这个技巧
recording = false;
}
@Override
@SneakyThrows
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode != code || resultCode != Activity.RESULT_OK) return;
//获取屏幕投影服务
projection = manager.getMediaProjection(resultCode, data);
//投影到编码器的Surface
display = projection.createVirtualDisplay(
"record_screen",
w,
h,
dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface,
null,
null
);
//开启录制线程
Threads.post(() -> {
Files.createFile(path);
mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
bufferInfo = new MediaCodec.BufferInfo();
recording = true;
while (recording) {
int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
//输出格式改变
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = mediaCodec.getOutputFormat();
videoTrackIndex = mediaMuxer.addTrack(newFormat);
mediaMuxer.start();
}
//暂无数据,请等待
if (index == MediaCodec.INFO_TRY_AGAIN_LATER)
Threads.sleep(10);
//得到编码数据
if (index >= 0) {
ByteBuffer encodedData = mediaCodec.getOutputBuffer(index);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) bufferInfo.size = 0;
if (bufferInfo.size == 0) encodedData = null;
if (encodedData != null) {
encodedData.position(bufferInfo.offset);
encodedData.limit(bufferInfo.offset + bufferInfo.size);
mediaMuxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo);
}
mediaCodec.releaseOutputBuffer(index, false);
}
}
release();
});
}
private void release() {
display.release();
display = null;
projection.stop();
projection = null;
mediaMuxer.stop();
mediaMuxer.release();
mediaMuxer = null;
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
videoTrackIndex = null;
bufferInfo = null;
TipBox.tip("录制完成");
working = false;
}
}