Android APP 音视频(02)MediaProjection录屏与MediaCodec编码

devtools/2024/9/22 20:17:55/

说明: 此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 上。

录屏功能的实现流程如下:

  1. 权限申请:APP需要请求用户授权使用屏幕录制功能。这会涉及 AndroidManifest.xml 文件的修改以及添加必要的权限,如 WRITE_EXTERNAL_STORAGERECORD_AUDIO
  2. 触发用户授权:通过 MediaProjectionManager 创建一个 Intent 来触发系统的屏幕录制授权界面。用户同意授权后,应用程序可以在 onActivityResult 中接收到结果。
  3. 获取 MediaProjection 实例:如果用户授权成功,则可以通过 MediaProjectionManagergetMediaProjection() 方法获取一个 MediaProjection 实例 。
  4. 创建 VirtualDisplay:使用 MediaProjection 实例创建 VirtualDisplay,它将捕获屏幕内容并将其显示在 Surface 上。
  5. 开始录制:调用 MediaRecorderstart() 方法开始录制屏幕内容。
  6. 结束录制:录制完成后,调用 MediaRecorderstop()reset() 方法停止录制并重置 MediaRecorder 状态,然后释放 VirtualDisplay 资源。

MediaProjection 录屏的原理主要是通过系统授权,捕获屏幕内容并利用虚拟显示设备将内容渲染到录制器上,实现屏幕录制的功能。开发者在使用时需要考虑到用户授权、资源管理和异常处理等关键步骤 。

1.2 MediaCodec编码说明

MediaCodec 是 Android 提供的一个音视频编解码器类,允许应用程序对音频和视频数据进行编码(压缩)和解码(解压缩)。它在 Android 4.1(API 级别 16)版本中引入,广泛应用于处理音视频数据,如播放视频、录制音频等。

以下是 MediaCodec 编码的基本步骤:

  1. 创建 MediaCodec 实例:通过调用 MediaCodec.createEncoderByType 方法并传入编码类型(如 "video/avc" 或 "audio/mp4a-latm")来创建编码器。

  2. 配置编码参数:通过调用 configure 方法配置编码器,传入编码参数如比特率、帧率、编码格式等。

  3. 准备输入和输出 Surface:为编码器准备输入和输出 Surface。输入 Surface 用于传递待编码的数据,输出 Surface 用于接收编码后的数据。

  4. 开始编码:调用 start 方法启动编码器。

  5. 发送输入数据:将待编码的数据通过 write 方法发送到编码器的输入队列。

  6. 处理输出数据:监听输出队列,通过 dequeueOutputBuffer 方法获取编码后的数据,并进行处理或存储。

  7. 停止编码:编码完成后,调用 stop 方法停止编码器。

  8. 释放资源:调用 release 方法释放编码器资源。

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 ...><serviceandroid: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;@Overridepublic void onCreate() {super.onCreate();startMediaProjectionForeground();}@Overridepublic 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@Overridepublic 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();}}@Overridepublic 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;@Overrideprotected 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 -> {// 创建屏幕录制的 IntentIntent 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"><Buttonandroid: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对码流进行播放,说明编码生成的码流是有效的,截图如下所示:


http://www.ppmy.cn/devtools/84861.html

相关文章

win10安装ElasticSearch7.x和分词插件

说明&#xff1a; 以下内容整理自网络&#xff0c;格式调整优化&#xff0c;更易阅读&#xff0c;希望能对需要的人有所帮助。 一 安装 Java环境 ElasticSearch使用Java开发的&#xff0c;依赖Java环境&#xff0c;安装 ElasticSearch 7.x 之前&#xff0c;需要先安装jdk-8。…

动静资源的转发操作

目录 Nginx中的location指令 静态资源的转发 动态资源的转发 注意事项 深入研究 如何在Nginx中实现对特定后缀文件的静态资源进行反向代理&#xff1f; Nginx中location指令的优先级是怎样确定的&#xff1f; 为什么在使用proxy_pass时要区分是否带有斜杠&#xff1f; N…

MATLAB中balance函数用法

目录 语法 说明 示例 局限性 balance函数的功能是对角线缩放以提高特征值准确性。 语法 [T,B] balance(A) [S,P,B] balance(A) B balance(A) B balance(A,noperm) 说明 [T,B] balance(A) 返回相似变换矩阵 T 以使 B T\A*T 和 B 具有尽可能接近的、近似相等的行列范…

数据结构:栈

文章目录 1. 栈的概念和结构2. 栈的顺序存储实现2.1 初始化2.2 入栈2.3 判断栈是否为空2.4 出栈2.5 取栈顶数据2.6 获取栈中有效元素个数2.7 打印栈中的数据2.8 销毁 3. 栈的链式存储实现3.1 初始化3.2 判断栈是否为空3.3 入栈3.4 出栈3.5 取栈顶元素3.6 获取栈中有效元素个数3…

《企业实战分享 · CodeGeeX 初体验》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

Java面试八股之什么是声明式事务管理,spring怎么实现声明式事务管理?

什么是声明式事务管理&#xff0c;spring怎么实现声明式事务管理&#xff1f; 声明式事务管理是一种编程范式&#xff0c;它允许开发人员通过声明性的配置或注解&#xff0c;而不是硬编码事务处理逻辑&#xff0c;来指定哪些方法或类应该在其上下文中执行事务。这种方法将事务…

Vue学习---vue 防抖处理函数,是处理什么场景

Vue防抖处理函数是用来处理在快速连续操作中&#xff0c;只执行最后一次操作的情况。 例如&#xff0c;在输入框输入时&#xff0c;我们可能希望只在用户完成输入后进行处理&#xff0c;而不是在每次键入时都处理。(n秒后触发一次) 以下是一个简单的Vue防抖处理函数的例子&am…

LeetCode206 反转链表

前言 题目&#xff1a; 206. 反转链表 文档&#xff1a; 代码随想录——反转链表 编程语言&#xff1a; C 解题状态&#xff1a; 有了思路以后没敢尝试 思路 需要注意的是创建指针不会申请额外的内存空间。 代码 方法一&#xff1a; 双指针法/迭代 我的理解是创建了三个指针…