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

news2024/11/14 6:04:08

说明: 此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 ...>
        <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对码流进行播放,说明编码生成的码流是有效的,截图如下所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1946051.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【测开能力提升-Javascript】JavaScript运算符流程结构

1. 递增递减运算符 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><script>// 前置递增运算符var age10age //类似于ageage1&#xff0c; 先加1后返回值alert(age)// 后置…

【数据结构 | 哈希表】一文了解哈希表(散列表)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

【SQL语句大全(MySQL)】

SQL语法 添加删除修改查询基本查询条件查询分组函数/聚合函数分组查询排序分页查询&#xff08;限制查询&#xff09;多表查询连接查询根据年代分类连接查询根据连接方式分类1、内连接2、左外连接3、右外连接 多张表连接的语法格式 嵌套查询 SQL语句书写顺序 添加 INSERT INTO…

什么是STP环路保护

在运行生成树协议的网络中&#xff0c;根端口和其他阻塞端口状态是依靠不断接收来自上游设备的BPDU维持。当由于链路拥塞或者单向链路故障导致这些端口收不到来自上游交换设备的BPDU时&#xff0c;设备会重新选择根端口。原先的根端口会转变为指定端口&#xff0c;而原先的阻塞…

2019年9月全国英语等级考试第三级笔试真题

2019年9月全国英语等级考试第三级笔试真题

vue3.0学习笔记(三)——计算属性、监听器、ref属性、组件通信

1. computed 函数 定义计算属性&#xff1a; computed 函数&#xff0c;是用来定义计算属性的&#xff0c;计算属性不能修改。 计算属性应该是只读的&#xff0c;特殊情况可以配置 get set 核心步骤&#xff1a; 导入 computed 函数 执行函数 在回调参数中 return 基于响应…

尚品汇-sku存入Redis缓存(二十三)

目录&#xff1a; &#xff08;1&#xff09;分布式锁改造获取sku信息 &#xff08;2&#xff09;使用Redisson 分布式锁 AOP实现缓存 &#xff08;3&#xff09;定义缓存aop注解 &#xff08;1&#xff09;分布式锁改造获取sku信息 前面学习了本地锁的弊端&#xff0c;…

Springboot validated JSR303校验

1.导入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency> 2.测试类 package com.jmj.gulimall.product.testC;import lombok.Data;import javax.val…

《0基础》学习Python——第二十讲__网络爬虫/<3>

一、用post请求爬取网页 同样与上一节课的get强求的内容差不多&#xff0c;即将requests.get(url,headershead)代码更换成requests.post(url,headershead),其余的即打印获取的内容&#xff0c;如果content-typejson类型的&#xff0c;打印上述代码的请求&#xff0c;则用一个命…

argon主题调整日记

前言 argon主题是一款由solstice23开发的一款简洁美观的WordPress主题&#xff0c;在使用过程中也发现了几个可以优化的点&#xff0c;在查阅主题文档无果后对其进行以下几点修改。 1、使用子主题 为了避免修改源文件而引起主题更新后修改丢失的问题&#xff0c;还是尽量使用子…

一个C++模板工厂的编译问题的解决。针对第三方库的构造函数以及追加了的对象构造函数。牵扯到重载、特化等

一窥模板的替换和匹配方式&#xff1a;偏特化的参数比泛化版本的还要多&#xff1a;判断是不是std::pair&#xff1c;,&#xff1e;。_stdpair模板参数太多-CSDN博客 简介 在一个项目里&#xff0c;调用了第三封的库&#xff0c;这个库里面有个类用的很多&#xff0c;而且其构…

Mindspore框架循环神经网络RNN模型实现情感分类|(六)模型加载和推理(情感分类模型资源下载)

Mindspore框架循环神经网络RNN模型实现情感分类 Mindspore框架循环神经网络RNN模型实现情感分类|&#xff08;一&#xff09;IMDB影评数据集准备 Mindspore框架循环神经网络RNN模型实现情感分类|&#xff08;二&#xff09;预训练词向量 Mindspore框架循环神经网络RNN模型实现…

【Vue实战教程】之 Vue Router 路由详解

Vue Router路由 1 路由基础 1.1 什么是路由 用Vue.js创建的项目是单页面应用&#xff0c;如果想要在项目中模拟出来类似于页面跳转的效果&#xff0c;就要使用路由。其实&#xff0c;我们不能只从字面的意思来理解路由&#xff0c;从字面上来看&#xff0c;很容易把路由联想…

CV13_混淆矩阵、F1分数和ROC曲线

1.1 混淆矩阵Confusion Matrix 混淆矩阵&#xff08;Confusion Matrix&#xff09;是机器学习和统计学中用于描述监督学习算法性能的特定表格布局。它是一种特定类型的误差矩阵&#xff0c;可以非常直观地表示分类模型在测试数据集上的预测结果与实际结果之间的对比。 混淆矩…

谣言检测文献阅读十二—A Convolutional Approach for Misinformation Identification

系列文章目录 谣言检测文献阅读一—A Review on Rumour Prediction and Veracity Assessment in Online Social Network谣言检测文献阅读二—Earlier detection of rumors in online social networks using certainty‑factor‑based convolutional neural networks谣言检测文…

FreeModbus学习——读输入寄存器eMBFuncReadInputRegister

FreeModbus版本&#xff1a;1.6 当功能码为04时&#xff0c;也就是读输入寄存器MB_FUNC_READ_INPUT_REGISTER 看一下它是怎么调用读输入寄存器处理函数的 当功能码为04时&#xff0c;调用读输入寄存器处理函数 这个函数在数组xFuncHandlers中&#xff0c;也就是eMBFuncRead…

Mysql数据库第四次作业

mysql> create table student(sno int primary key auto_increment,sname varchar(30) not null unique,Ssex varchar(2) check (Ssex男 or Ssex女) not null,Sage int not null,Sdept varchar(10) default计算机 not null); mysql> create table Course(Con int primar…

【通信协议-RTCM】MSM语句(2) - RINEXMSM7语句总结(重要!自动化开发计算卫星状态常用)

注释&#xff1a; 在工作中主要负责的是RTCM-MSM7语句相关开发工作&#xff0c;所以主要介绍的就是MSM7语句相关内容 1. 相位校准参考信号 2. MSM1、MSM2、MSM3、MSM4、MSM5、MSM6和MSM7的消息头内容 DATA FIELDDF NUMBERDATA TYPENO. OF BITSNOTES Message Number - 消息编…

1. Docker的介绍和安装 (二)

5 Docker的原理 5.1 Namespace Namespace&#xff08;命名空间&#xff09;提供了一个独立的工作环境&#xff0c;Docker使用Namespace来隔离容器&#xff0c;使得每个容器都有自己独立的系统资源&#xff08;如进程ID、主机名、网络等&#xff09;。 PID Namespace&#xf…

SBTI科学碳目标认证是什么?SBTI科学碳目标的重要性

SBTI科学碳目标认证&#xff0c;作为企业在应对气候变化和追求可持续发展道路上的重要里程碑&#xff0c;其认证过程严谨而系统。以下是获得SBTI科学碳目标认证的详细步骤&#xff1a; 首先&#xff0c;企业需要在线注册并提交承诺书&#xff0c;郑重承诺在未来24个月内提交科学…