安卓提供了一個屏幕投影服務(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;
}
}