下载地址:https://download.csdn.net/download/ink_s/13125347
权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
// 要申请的权限
private String[] permissions = {
Manifest.permission.SYSTEM_ALERT_WINDOW,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
};
录屏服务
method = 1 或 2是保存或直接获取H246裸流
数据回调
public interface RecordServiceListener {
void onImageData(byte[] buf);
}
method = 3 MediaCodec + MediaMuxer 将H264数据保存为MP4
method = 4 使用 MediaRecorder
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.Service;
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.MediaRecorder;
import android.media.projection.MediaProjection;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.view.Surface;
import com.inks.inkslibrary.Utils.L;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import static android.media.MediaCodec.CONFIGURE_FLAG_ENCODE;
import static android.media.MediaFormat.KEY_BIT_RATE;
import static android.media.MediaFormat.KEY_FRAME_RATE;
import static android.media.MediaFormat.KEY_I_FRAME_INTERVAL;
public class RecordService extends Service {
private static final String TAG = RecordService.class.getSimpleName();
private MediaProjection mediaProjection = null;
private final IBinder mBinder = new RecordService.LocalBinder();
private Context context;
private VirtualDisplay mVirtualDisplay;
private MediaCodec vEncoder;
private boolean isVideoEncoder;
private MediaCodec.BufferInfo vBufferInfo = new MediaCodec.BufferInfo();
private Thread videoEncoderThread;
private RandomAccessFile mH264DataFile = null;
private int w;
private int h;
private static final int TIMEOUT_US = 0;
private long lastTime = 0;
private MediaMuxer mediaMuxer;
private int mVideoTrack = -1;
MediaRecorder mMediaRecorder;
public interface RecordServiceListener {
void onImageData(byte[] buf);
}
private RecordServiceListener recordServiceListener;
@Override
public void onCreate() {
L.e("RecordService onCreate");
super.onCreate();
if (Build.VERSION.SDK_INT >= 26) {
startForeground(0, new Notification());
}
context = this;
}
public void start(RecordServiceListener recordServiceListener, MediaProjection mediaProjection, int w, int h,int method) throws Exception {
this.w = w;
this.h = h;
this.mediaProjection = mediaProjection;
if (mediaProjection != null) {
this.recordServiceListener = recordServiceListener;
//prepareVideoEncoder(1280,720);
prepareVideoEncoder(w, h);
switch (method){
case 1:
File file = new File(context.getExternalFilesDir(null)
+ "/" + getDateTimeStr() + "_____A" + w + "X" + h + ".264");
try {
mH264DataFile = new RandomAccessFile(file, "rw");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
startVideoEncode1();
break;
case 2:
file = new File(context.getExternalFilesDir(null)
+ "/" + getDateTimeStr() + "_____B" + w + "X" + h + ".264");
try {
mH264DataFile = new RandomAccessFile(file, "rw");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
startVideoEncode2();
break;
case 3:
file = new File(context.getExternalFilesDir(null)
+ "/" + getDateTimeStr() + "_____C" + w + "X" + h + ".mp4");
mediaMuxer = new MediaMuxer(file.getPath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
startVideoEncode3();
break;
case 4:
file = new File(context.getExternalFilesDir(null)
+ "/" + getDateTimeStr() + "_____D" + w + "X" + h + ".mp4");
startMediaRecorder(w,h,file.getPath());
break;
}
} else {
throw new RuntimeException("MediaProjection null");
}
}
public void stop() {
isVideoEncoder = false;
if (null != vEncoder) {
vEncoder.stop();
}
if (mediaMuxer != null) {
mediaMuxer.stop();
mediaMuxer.release();
}
if (mMediaRecorder != null) {
mMediaRecorder.setOnErrorListener(null);
mMediaRecorder.reset();
mMediaRecorder.release();
}
if (mVirtualDisplay != null) mVirtualDisplay.release();
if (mediaProjection != null) mediaProjection.stop();
}
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1:
break;
}
}
};
public void prepareVideoEncoder(int width, int height) throws IOException {
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(KEY_BIT_RATE, width * height);
format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
format.setInteger(KEY_FRAME_RATE, 20);
format.setInteger(KEY_I_FRAME_INTERVAL, 1);
// 当画面静止时,重复最后一帧,不影响界面显示(好像并没有什么用)
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / 20);
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
encoder.configure(format, null, null, CONFIGURE_FLAG_ENCODE);
Surface surface = encoder.createInputSurface();
mVirtualDisplay = mediaProjection.createVirtualDisplay("-display", width, height, 1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
vEncoder = encoder;
}
//把h264裸流保存为.264文件
public void startVideoEncode1() {
if (vEncoder == null) {
throw new RuntimeException("vEncoder null,请初始化视频编码器");
}
if (isVideoEncoder) {
throw new RuntimeException("It's already started. Please stop.");
}
videoEncoderThread = new Thread() {
@Override
public void run() {
try {
vEncoder.start();
while (isVideoEncoder && !Thread.interrupted()) {
try {
int outputBufferId = vEncoder.dequeueOutputBuffer(vBufferInfo, 0);
if (outputBufferId >= 0) {
// 有效输出
// 获取到的实时帧视频数据
ByteBuffer encodedData = vEncoder.getOutputBuffer(outputBufferId);
byte[] dataToWrite = new byte[vBufferInfo.size];
encodedData.get(dataToWrite, 0, vBufferInfo.size);
saveH264DataToFile(dataToWrite);
// onEncodedAvcFrame(bb, vBufferInfo);
vEncoder.releaseOutputBuffer(outputBufferId, false);
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}
} catch (Exception e) {
isVideoEncoder = false;
if (null != vEncoder) {
vEncoder.stop();
}
if (mVirtualDisplay != null) mVirtualDisplay.release();
if (mediaProjection != null) mediaProjection.stop();
}
}
};
isVideoEncoder = true;
videoEncoderThread.start();
}
//把h264裸流回调给调用方或保存为.264文件,修改onEncodedAvcFrame选择是保存还是回调
public void startVideoEncode2() {
if (vEncoder == null) {
throw new RuntimeException("vEncoder null,请初始化视频编码器");
}
if (isVideoEncoder) {
throw new RuntimeException("It's already started. Please stop.");
}
videoEncoderThread = new Thread() {
@Override
public void run() {
try {
vEncoder.start();
while (isVideoEncoder && !Thread.interrupted()) {
try {
int outputBufferId = vEncoder.dequeueOutputBuffer(vBufferInfo, 0);
if (outputBufferId >= 0) {
ByteBuffer bb = vEncoder.getOutputBuffer(outputBufferId);
onEncodedAvcFrame(bb, vBufferInfo);
vEncoder.releaseOutputBuffer(outputBufferId, false);
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}
} catch (Exception e) {
isVideoEncoder = false;
if (null != vEncoder) {
vEncoder.stop();
}
if (mVirtualDisplay != null) mVirtualDisplay.release();
if (mediaProjection != null) mediaProjection.stop();
}
}
};
isVideoEncoder = true;
videoEncoderThread.start();
}
//MediaCodec + MediaMuxer 将H264数据保存为MP4
public void startVideoEncode3() {
if (vEncoder == null) {
throw new RuntimeException("vEncoder null,请初始化视频编码器");
}
if (isVideoEncoder) {
throw new RuntimeException("It's already started. Please stop.");
}
videoEncoderThread = new Thread() {
@Override
public void run() {
try {
vEncoder.start();
while (isVideoEncoder && !Thread.interrupted()) {
try {
int index = vEncoder.dequeueOutputBuffer(vBufferInfo, TIMEOUT_US);
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 后续输出格式变化
MediaFormat newFormat = vEncoder.getOutputFormat();
mVideoTrack = mediaMuxer.addTrack(newFormat);
mediaMuxer.start();
} else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 请求超时
try {
// wait 10ms
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (index >= 0) {
// 有效输出
// 获取到的实时帧视频数据
ByteBuffer encodedData = vEncoder.getOutputBuffer(index);
if ((vBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
L.e("ignoring BUFFER_FLAG_CODEC_CONFIG");
vBufferInfo.size = 0;
}
if (vBufferInfo.size == 0) {
L.e("info.size == 0, drop it.");
encodedData = null;
} else {
}
if (encodedData != null) {
mediaMuxer.writeSampleData(mVideoTrack, encodedData, vBufferInfo);
}
vEncoder.releaseOutputBuffer(index, false);
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}
} catch (Exception e) {
isVideoEncoder = false;
if (null != vEncoder) {
vEncoder.stop();
vEncoder.release();
mediaMuxer.stop();
mediaMuxer.release();
}
if (mVirtualDisplay != null) mVirtualDisplay.release();
if (mediaProjection != null) mediaProjection.stop();
}
}
};
isVideoEncoder = true;
videoEncoderThread.start();
}
//MediaRecorder
public void startMediaRecorder(int w,int h,String mPath) {
try{
isVideoEncoder = true;
initMediaRecorder(w,h,mPath);
//在mediarecorder.prepare()方法后调用
mVirtualDisplay = mediaProjection.createVirtualDisplay(TAG + "-display", w, h, 1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mMediaRecorder.getSurface(), null, null);
mMediaRecorder.start();
}catch (Exception e){
e.printStackTrace();
isVideoEncoder = false;
if (null != vEncoder) {
vEncoder.stop();
vEncoder.release();
}
if (mMediaRecorder != null) {
mMediaRecorder.setOnErrorListener(null);
mMediaRecorder.reset();
mMediaRecorder.release();
}
if (mVirtualDisplay != null) mVirtualDisplay.release();
if (mediaProjection != null) mediaProjection.stop();
}
}
public void initMediaRecorder(int w,int h,String mPath) {
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
// mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setOutputFile(mPath);
mMediaRecorder.setVideoSize(w, h);
mMediaRecorder.setVideoFrameRate(20);
mMediaRecorder.setVideoEncodingBitRate(w*h);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
//mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
try {
mMediaRecorder.prepare();
} catch (Exception e) {
e.printStackTrace();
}
}
//保存H264视频到本地
private void saveH264DataToFile(byte[] dataToWrite) {
try {
mH264DataFile.write(dataToWrite, 0, dataToWrite.length);
} catch (IOException e) {
e.printStackTrace();
}
}
public static final int NAL_SLICE = 1;
public static final int NAL_SLICE_DPA = 2;
public static final int NAL_SLICE_DPB = 3;
public static final int NAL_SLICE_DPC = 4;
public static final int NAL_SLICE_IDR = 5;
public static final int NAL_SEI = 6;
public static final int NAL_SPS = 7;
public static final int NAL_PPS = 8;
public static final int NAL_AUD = 9;
public static final int NAL_FILLER = 12;
private byte[] sps_pps_buf;
//https://blog.csdn.net/a289973483/article/details/79220988
private void onEncodedAvcFrame(ByteBuffer bb, final MediaCodec.BufferInfo vBufferInfo) {
int offset = 4;
//判断帧的类型
if (bb.get(2) == 0x01) {
offset = 3;
}
int type = bb.get(offset) & 0x1f;
if (type == NAL_SPS) {
//[0, 0, 0, 1, 103, 66, -64, 13, -38, 5, -126, 90, 1, -31, 16, -115, 64, 0, 0, 0, 1, 104, -50, 6, -30]
//打印发现这里将 SPS帧和 PPS帧合在了一起发送
// SPS为 [4,len-8]
// PPS为后4个字节
sps_pps_buf = new byte[vBufferInfo.size];
bb.get(sps_pps_buf);
L.e(Arrays.toString(sps_pps_buf));
//TODO
/*
final byte[] pps = new byte[4];
final byte[] sps = new byte[vBufferInfo.size - 12];
bb.getInt();// 抛弃 0,0,0,1
bb.get(sps, 0, sps.length);
bb.getInt();
bb.get(pps, 0, pps.length);
Log.d(TAG, "解析得到 sps:" + Arrays.toString(sps) + ",PPS=" + Arrays.toString(pps));
*/
} else if (type == NAL_SLICE /* || type == NAL_SLICE_IDR */) {
final byte[] bytes = new byte[vBufferInfo.size];
bb.get(bytes);
//回调数据流
// if (null != recordServiceListener) {
// recordServiceListener.onImageData(bytes);
// }
saveH264DataToFile(bytes);
// Log.v(TAG, "视频数据 " + Arrays.toString(bytes));
} else if (type == NAL_SLICE_IDR) {
// I帧,前面添加sps和pps
final byte[] bytes = new byte[vBufferInfo.size];
bb.get(bytes);
byte[] newBuf = new byte[sps_pps_buf.length + bytes.length];
System.arraycopy(sps_pps_buf, 0, newBuf, 0, sps_pps_buf.length);
System.arraycopy(bytes, 0, newBuf, sps_pps_buf.length, bytes.length);
// if (null != recordServiceListener) {
//回调数据流
// recordServiceListener.onImageData(newBuf);
// }
saveH264DataToFile(newBuf);
// Log.v(TAG, "sps pps " + Arrays.toString(sps_pps_buf));
// Log.v(TAG, "视频数据 " + Arrays.toString(newBuf));
}
}
@Override
public IBinder onBind(Intent intent) {
L.e("TCP:onBind");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
L.e("RecordService:onUnbind");
return super.onUnbind(intent);
}
@Override
public void onDestroy() {
L.e("RecordService:onDestroy");
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
public class LocalBinder extends Binder {
public RecordService getService() {
return RecordService.this;
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
L.e("RecordService:onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
private static String getDateTimeStr() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH_mm_ss");
Date date = new Date();
String dateStr = simpleDateFormat.format(date);
return dateStr;
}
}
activity中调用
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ListView;
import com.inks.inkslibrary.Utils.ClickUtil;
import com.inks.inkslibrary.Utils.L;
import com.inks.inkslibrary.Utils.T;
import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity {
private MediaProjectionManager mMediaProjectionManager;
private static final int REQUEST_CODE = 1;
@BindView(R.id.width)
EditText width;
@BindView(R.id.height)
EditText height;
private int w;
private int h;
private RecordService recordService;
private final ServiceConnection recordServiceConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
recordService = ((RecordService.LocalBinder) service).getService();
L.e("RecordService ");
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.e("RecordService", "RecordService启动失败");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent recordServiceIntent = new Intent(this, RecordService.class);
bindService(recordServiceIntent, recordServiceConn, BIND_AUTO_CREATE);
ButterKnife.bind(this);
}
public void click(View view) {
if (!ClickUtil.isFastDoubleClick((long) 200)) {
switch (view.getId()){
case R.id.start:
String wStr = width.getText().toString().trim();
String hStr = height.getText().toString().trim();
if(TextUtils.isEmpty(wStr) || TextUtils.isEmpty(hStr)){
T.showShort(this,"请输入宽高");
break;
}
try {
w = Integer.parseInt(wStr);
h =Integer.parseInt(hStr);
}catch (Exception e){
T.showShort(this,"请输入宽高");
break;
}
prepareScreen();
break;
case R.id.stop:
recordService.stop();
break;
case R.id.clear:
cleanFilePath( Objects.requireNonNull(this.getExternalFilesDir(null)).getPath());
break;
}
}
}
public void prepareScreen() {
mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
assert mMediaProjectionManager != null;
Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK || requestCode != REQUEST_CODE) return;
try {
recordService.start(recordServiceListener,mMediaProjectionManager.getMediaProjection(resultCode, data),w,h,4);
} catch (Exception e) {
e.printStackTrace();
}
}
RecordService.RecordServiceListener recordServiceListener = new RecordService.RecordServiceListener() {
@Override
public void onImageData(byte[] buf) {
}
};
public void cleanFilePath(String path) {
L.e("path");
File file = new File(path);
if (!file.exists()) return;
if (file.isFile() || file.list() == null) {
L.e("1");
return;
} else {
File[] files = file.listFiles();
for (File a : files) {
cleanFile(a.getPath());
}
}
}
public void cleanFile(String path) {
File file = new File(path);
if (!file.exists()) return;
if (file.isFile() || file.list() == null) {
file.delete();
} else {
File[] files = file.listFiles();
for (File a : files) {
cleanFile(a.getPath());
}
file.delete();
}
}
}
activity.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/width"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="4"
android:maxLines="1"
android:layout_margin="10dp"
android:hint="宽"
android:text="1280"
android:singleLine="true"/>
<EditText
android:id="@+id/height"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="4"
android:maxLines="1"
android:layout_margin="10dp"
android:hint="高"
android:text="720"
android:singleLine="true"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/start"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="开始"
android:onClick="click"/>
<Button
android:id="@+id/stop"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="停止"
android:onClick="click"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/clear"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="清空文件"
android:onClick="click"/>
<TextClock
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:format24Hour="yyyy年 MM月dd日hh时mm分ss秒"/>
</LinearLayout>
</LinearLayout>