说明: 此MediaProjection 录屏和编码实操主要针对Android12.0系统。通过MediaProjection获取屏幕数据,将数据通过mediacodec编码输出H264码流(使用ffmpeg播放),存储到sd卡上。
1 MediaProjection录屏与编码简介
这里主要是使用MediaProjection获取屏幕数据,将数据通过mediacodec编码输出到存储卡上。这里主要介绍 MediaProjection的基本原理和流程、 MediaCodec编码的简单说明,便于对代码有所理解。
1.1 MediaProjection录屏原理和流程
MediaProjection 是 Android 提供的一个用于屏幕捕捉和屏幕录制的功能,它允许应用程序在获得用户授权的情况下捕获设备屏幕的内容。这项技术自 Android 5.0(Lollipop)起引入,并在之后的版本中得到广泛应用和发展。
MediaProjection 的主要组件包括:
- MediaProjectionManager:系统服务,用于创建和管理 MediaProjection 会话。
- MediaProjection:表示屏幕捕获会话的令牌,通过用户的授权获得。
- VirtualDisplay:一个虚拟的显示设备,它可以捕获屏幕内容并将其渲染到指定的 Surface 上。
录屏功能的实现流程如下:
- 权限申请:APP需要请求用户授权使用屏幕录制功能。这会涉及
AndroidManifest.xml
文件的修改以及添加必要的权限,如WRITE_EXTERNAL_STORAGE
和RECORD_AUDIO
。 - 触发用户授权:通过
MediaProjectionManager
创建一个 Intent 来触发系统的屏幕录制授权界面。用户同意授权后,应用程序可以在onActivityResult
中接收到结果。 - 获取 MediaProjection 实例:如果用户授权成功,则可以通过
MediaProjectionManager
的getMediaProjection()
方法获取一个MediaProjection
实例 。 - 创建 VirtualDisplay:使用
MediaProjection
实例创建VirtualDisplay
,它将捕获屏幕内容并将其显示在 Surface 上。 - 开始录制:调用
MediaRecorder
的start()
方法开始录制屏幕内容。 - 结束录制:录制完成后,调用
MediaRecorder
的stop()
和reset()
方法停止录制并重置MediaRecorder
状态,然后释放VirtualDisplay
资源。
MediaProjection 录屏的原理主要是通过系统授权,捕获屏幕内容并利用虚拟显示设备将内容渲染到录制器上,实现屏幕录制的功能。开发者在使用时需要考虑到用户授权、资源管理和异常处理等关键步骤 40。
1.2 MediaCodec编码说明
MediaCodec 是 Android 提供的一个音视频编解码器类,允许应用程序对音频和视频数据进行编码(压缩)和解码(解压缩)。它在 Android 4.1(API 级别 16)版本中引入,广泛应用于处理音视频数据,如播放视频、录制音频等。
MediaCodec 支持处理三种数据类型:压缩数据、原始音频数据和原始视频数据。这些数据可以通过 ByteBuffer 传输给 MediaCodec 进行处理。对于原始视频数据,使用 Surface 作为输入源可以提高编解码器的性能。针对本工程,主要通过获得录屏的原始数据,通过mediacodec压缩成H264码流。
2 MediaProjection录屏与编码代码完整解读(android Q)
2.1 关于权限部分的处理
关于权限,需要在AndroidManifest.xml中添加权限,具体如下所示:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
这里尤其要注意android.permission.FOREGROUND_SERVICE的添加。关于运行时权限的请求等,这里给出一个工具类参考代码,具体如下所示:
public class Permission {
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 1;
//需要申请权限的数组
private static final String[] permissions = {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
};
//保存真正需要去申请的权限
private static final List<String> permissionList = new ArrayList<>();
public static int RequestCode = 100;
public static void requestManageExternalStoragePermission(Context context, Activity activity) {
if (!Environment.isExternalStorageManager()) {
showManageExternalStorageDialog(activity);
}
}
private static void showManageExternalStorageDialog(Activity activity) {
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle("权限请求")
.setMessage("请开启文件访问权限,否则应用将无法正常使用。")
.setNegativeButton("取消", null)
.setPositiveButton("确定", (dialogInterface, i) -> {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
})
.create();
dialog.show();
}
public static void checkPermissions(Activity activity) {
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(permission);
}
}
if (!permissionList.isEmpty()) {
requestPermission(activity);
}
}
public static void requestPermission(Activity activity) {
ActivityCompat.requestPermissions(activity,permissionList.toArray(new String[0]),RequestCode);
}
}
2.2 MediaProjection服务的添加
从 Android 12 开始,如果应用需要使用 MediaProjection
进行屏幕录制,必须将相关的服务声明为前台服务。这是因为屏幕录制涉及到用户隐私,因此系统需要确保用户明确知道该服务正在运行。需要在应用的 AndroidManifest.xml
文件中声明服务,并添加相应的权限(2.1中已经添加)和特性,具体编写参考如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
...>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application ...>
<service
android:name=".serviceset.MediaProjectionService"
android:exported="true"
android:foregroundServiceType="mediaProjection" />
<!-- 其他组件声明 -->
</application>
</manifest>
添加这些后,接下来需要实现.serviceset.MediaProjectionService 的代码,具体如下所示:
public class MediaProjectionService extends Service {
private MediaProjection mMediaProjection;
public static int resultCode;
public static Intent resultData;
public static Notification notification;
public static Context context;
@Override
public void onCreate() {
super.onCreate();
startMediaProjectionForeground();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MediaProjectionManager mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, resultData);
H264EncoderThread h264EncoderThread = new H264EncoderThread(mediaProjection, 640, 1920);
h264EncoderThread.start();
return START_NOT_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void startMediaProjectionForeground() {
String channelId = "CHANNEL_ID_MEDIA_PROJECTION";
NotificationManager NOTIFICATION_MANAGER = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this,channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("服务已启动");
NotificationChannel channel = new NotificationChannel(channelId, "屏幕录制", NotificationManager.IMPORTANCE_HIGH);
NOTIFICATION_MANAGER.createNotificationChannel(channel);
notificationBuilder.setChannelId(channelId);
Notification notification = notificationBuilder.build();
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
}
}
2.3 编码的处理
关于编码部分,主要是MediaCodec的初始化、编码处理部分和文件写入操作,代码如下所示:
public class H264EncoderThread extends Thread{
private MediaProjection mMediaProjection;
MediaCodec mediaCodec;
private final String TAG = "H264EncoderThread";
public H264EncoderThread(MediaProjection mMediaProjection, int width, int height) {
this.mMediaProjection = mMediaProjection;
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
format.setInteger(MediaFormat.KEY_FRAME_RATE, 20);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 30);
format.setInteger(MediaFormat.KEY_BIT_RATE, width * height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaCodec.configure(format,null,null,CONFIGURE_FLAG_ENCODE);
Surface surface= mediaCodec.createInputSurface();
mMediaProjection.createVirtualDisplay("wangdsh-test", width, height, 2,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null
);
} catch (IOException e) {
Log.e("TAG",e.toString());
//e.printStackTrace();
}
}
@Override
public void run() {
super.run();
mediaCodec.start();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true) {
int outIndex = mediaCodec.dequeueOutputBuffer(info, 11000);
if (outIndex >= 0) {
ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outIndex);
byte[] ba = new byte[byteBuffer.remaining()];
byteBuffer.get(ba);
FileUtils.writeBytes(ba);
FileUtils.writeContent(ba);
mediaCodec.releaseOutputBuffer(outIndex, false);
}
}
}
}
其中涉及的FileUtils参考实现如下:
public class FileUtils {
private static final String TAG = "FileUtils";
public static void writeBytes(byte[] array) {
FileOutputStream writer = null;
try {
writer = new FileOutputStream(Environment.getExternalStorageDirectory() + "/codecoutput.h264", true);
writer.write(array);
writer.write('\n');
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static String writeContent(byte[] array) {
char[] HEX_CHAR_TABLE = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
StringBuilder sb = new StringBuilder();
for (byte b : array) {
sb.append(HEX_CHAR_TABLE[(b & 0xf0) >> 4]);
sb.append(HEX_CHAR_TABLE[b & 0x0f]);
}
Log.d(TAG, "writeContent-: " + sb.toString());
try {
FileWriter writer = new FileWriter(Environment.getExternalStorageDirectory() + "/codecH264.txt", true);
writer.write(sb.toString());
writer.write("\n");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
}
2.4 主流程代码参考实现
这里以 H264encoderMediaProjActivity为例,给出一个MediaProjection录屏与编码功能代码的参考实现。具体实现如下:
public class H264encoderMediaProjActivity extends AppCompatActivity {
private MediaProjectionManager mMediaProjectionManager;
Context mContext;
private ActivityResultLauncher<Intent> screenCaptureLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
mContext = this;
setContentView(R.layout.h264_encode_media_projection);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Permission.checkPermissions(this);
Permission.requestManageExternalStoragePermission(getApplicationContext(), this);
mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
screenCaptureLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent resultData = result.getData();
MediaProjectionService.resultCode = result.getResultCode();
MediaProjectionService.resultData = resultData;
MediaProjectionService.context = mContext;
Intent SERVICE_INTENT = new Intent(this, MediaProjectionService.class);
startForegroundService(SERVICE_INTENT);
}
}
);
Button mButton = findViewById(R.id.button);
mButton.setOnClickListener(view -> {
// 创建屏幕录制的 Intent
Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent();
// 启动屏幕录制请求
screenCaptureLauncher.launch(captureIntent);
});
}
}
这里涉及的layout布局文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
tools:context=".MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/startLive"
android:gravity="center"
android:id="@+id/button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
2.5 MediaProjection录屏与编码 demo实现效果
实际运行效果展示如下:
使用ffmpeg对码流进行播放,说明编码生成的码流是有效的,截图如下所示:
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » Android APP 音视频(02)MediaProjection录屏与MediaCodec编码
发表评论 取消回复