Android录屏功能流程图
Android录屏功能的流程图如下:
启动编码器之后,在MediaCodec.Callback的onOutputBufferAvailable方法中对video的数据进行处理,可以保存到本地文件,也可以返送到服务器设备进行实时显示本手机的屏幕。
MediaCodec简介
MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。它是 Android 低级多媒体支持基础结构的一部分。MediaCodec可以通过传递不同的参数,调用静态方法来创建软/硬编码器。软硬编解码器有各自的优缺点。软编码器使用CPU进行工作,兼容性较好,压缩率比较高。硬编码器使用GPU进行工作,性能优良,内存占用少,压缩率比较低。MediaCodec可以通过名称和类型两种方式开创建相应的实例对象:MediaCodec.createByCodecName("OMX.google.h264.encoder")软编码器和MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)硬编码器。选择h264这种视频格式的原因:免授权费,各方面的性能都比较均衡。Android支持的比较好还有h265,VP8,VP9等等格式。
Android录屏相关的名词介绍
MediaProjection:Android系统分配给应用可以录屏的类,只能分配给一个应用独占,其他应用可以抢占。比如应用A通过MediaProjectionManager请求并获得MediaProjection,应用B可以通过MediaProjectionManager请求并获得MediaProjection,这时候应用A丢失MediaProjection。 VirtualDisplay:创建虚拟显示。获取VirtualDisplay关联的surface,各自实现。本例子中VirtualDisplay用的是MediaCodec创建的。
MediaCodec状态
MediaCodec的状态如下图所示:
MediaCodec的三个方法,
- configure 配置编码的相关参数。
- start 启动编码器。
- stop 停止编码器。
给MediaCodec设置回调,编码帧可用时,会从回调返回。
/**
* 给MediaCodec设置回调,编码帧可用时,会从回调返回
*/
private void setCallback() {
mMediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
// 编码的outputBuffer可用,即有新的视频帧
if (index >= 0 && isRunning) {
mBufferInfo = info;
try {
encodeFrame(index);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "onOutputBufferAvailable: Exception: e = " + e);
}
} else {
Log.d(TAG, "onOutputBufferAvailable: encoder is not running");
}
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
e.printStackTrace();
Log.e(TAG, "onError: encoder Exception: e = " + e);
// mMediaCodec.reset();
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
// 更新sps、pps
resetOutputFormat();
}
});
}
MainActivity的代码如下:
package com.techcaicai.screenrecord;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.io.File;
public class MainActivity extends Activity {
private static final int RECORD_REQUEST_CODE = 101;
private static final String TAG = "MainActivity";
Button screen_record_start;
Button screen_record_stop;
private MediaProjection mMediaProjection;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 启动一个前台的service,录屏安全机制需要
startService(new Intent(this, MediaService.class));
screen_record_start = findViewById(R.id.screen_record_start);
screen_record_stop = findViewById(R.id.screen_record_stop);
screen_record_start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
requestRecording();
}
});
}
private void stopRecording() {
// 验证停止后立即启动
RecordManager.getInstance().stopRecord();
RecordManager.getInstance().release();
if (mMediaProjection != null) {
mMediaProjection.stop();
mMediaProjection = null;
}
Toast.makeText(this, "录制结束", Toast.LENGTH_SHORT).show();
}
protected void requestRecording() {
MediaProjectionManager mMediaProjectionManage = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
if (mMediaProjectionManage != null) {
Intent captureIntent = mMediaProjectionManage.createScreenCaptureIntent();
startActivityForResult(captureIntent, RECORD_REQUEST_CODE);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RECORD_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
//一键投屏
Log.i(TAG, "允许投屏权限");
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
mMediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
startRecord();
} else {
Log.i(TAG, "拒绝投屏权限");
}
}
}
/**
* 开始录制
*/
@SuppressLint("SimpleDateFormat")
private void startRecord() {
File cacheDir = getExternalCacheDir();
// /storage/emulated/0/Android/data/com.techcaicai.screenrecord/cache
Log.d(TAG, "startRecord: ========= path" + cacheDir.getAbsolutePath());
RecordManager.getInstance().setFilePath(cacheDir.getAbsolutePath() + "/caicai.h264");
RecordManager.getInstance().startRecord(mMediaProjection);
Toast.makeText(this, "录制开始", Toast.LENGTH_SHORT).show();
}
}
MainActivity如果mMediaProjection没有实例化,就通过requestRecording方法请求系统权限来获取,如果用户同意了系统请求,参考上面的onActivityResult方法,初始化mMediaProjection之后再实例化MediaCodec,配置MediaCodec,实例化VirtualDisplay,然后启动MediaCodec来录屏。还有一个需要注意的知识点就是在高版本的Android系统上录屏的时候需要显示一个前台通知,否则会应用会报错不能成功录屏,这就需要定义一个Service,在Service内部启动一个前台通知。Manifest配置如下:
<service android:name=".MediaService"
android:enabled="true"
android:foregroundServiceType="mediaProjection"
android:exported="true"/>
Service的具体实现如下面代码:
package com.techcaicai.screenrecord;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
public class MediaService extends Service {
private final String NOTIFICATION_CHANNEL_ID="com.techcaicai.screenrecord.MediaService";
private final String NOTIFICATION_CHANNEL_NAME="com.techcaicai.screenrecord.channel_name";
private final String NOTIFICATION_CHANNEL_DESC="com.techcaicai.screenrecord.channel_desc";
public MediaService() {
}
@Override
public void onCreate() {
super.onCreate();
startNotification();
}
public void startNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//Call Start foreground with notification
Intent notificationIntent = new Intent(this, MediaService.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground))
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("Starting Service")
.setContentText("Starting monitoring service")
.setContentIntent(pendingIntent);
Notification notification = notificationBuilder.build();
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(NOTIFICATION_CHANNEL_DESC);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
startForeground(1, notification); //必须使用此方法显示通知,不能使用notificationManager.notify,否则还是会报上面的错误
}
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
}
查看或者分析h264格式的数据,我比较喜欢用VLC media player工具,我们用adb pull命令把视频文件从手机中导出。
用VLC media player工具播放,效果图如下:
视频中还有一些花屏的现象,等有时间再做优化。
总结
音视频编解码是一个比较有意思的领域,可以在很多方面发挥作用,比如是手机投屏,远程会议,远程控制手机等等领域,希望文章对您有帮助,如果文中有问题,希望您不吝指教。