Android APP 音视频(03)CameraX预览与MediaCodec编码

devtools/2024/9/22 17:01:13/

说明: 此CameraX预览和编码实操主要针对Android12.0系统。通过CameraX预览获取yuv格式数据,将yuv格式数据通过mediacodec编码输出H264码流(使用ffmpeg播放),存储到sd卡上。


CameraX 和 MediaCodec简介

1.1 CameraX简介

CameraX 是一个由 Google 开发的 Android Jetpack 库,旨在简化 Android 应用中的相机操作。它提供了一个一致的 API 界面,使得开发者可以更容易地在应用中集成和使用相机功能。以下是 CameraX 的一些关键特点和优势:

  • 简化的 APICameraX 提供了一个简单且一致的 API,使得开发者可以轻松地访问相机硬件,而无需处理底层的复杂性。
  • 兼容性CameraX 支持从 Android 5.0(API 级别 21)到最新版本的 Android 系统,确保了广泛的设备兼容性。
  • 预览和捕获CameraX 允许开发者轻松地实现相机预览和图像捕获功能。它提供了一个预览界面,用户可以通过它查看相机捕获的实时图像。
  • 配置灵活性CameraX 允许开发者根据需要配置相机的各种参数,如分辨率、帧率、焦距等。
  • 异步处理CameraX 使用异步处理机制,确保相机操作不会阻塞主线程,从而提高应用的响应性和性能。
  • 权限管理CameraX 还帮助开发者管理相机权限,确保应用在需要时能够获得必要的权限。
  • 扩展性CameraX 提供了扩展点,允许开发者根据需要添加额外的功能,如图像处理、视频录制等。
  • 集成简单:通过依赖项添加 CameraX 库到项目中,开发者可以快速开始使用 CameraX
  • 文档和社区支持CameraX 拥有详细的文档和活跃的社区,为开发者提供了丰富的资源和支持。

总的来说,CameraX 是一个强大的工具,可以帮助开发者在 Android 应用中实现高质量的相机功能,同时减少开发工作量和提高应用的稳定性。针对本文的实际需求,这里主要参照了如下文章的内容及代码实现:Android APP Camerax应用(02)预览流程

1.2 MediaCodec简介

MediaCodec 是 Android 平台上的一个 API,用于高效地进行多媒体数据的编码和解码操作。它主要用于处理视频和音频数据,支持各种格式的编解码,如 H.264、H.265、VP8、VP9、AAC 等。以下是 MediaCodec 的一些关键特点和功能:

  • 高效处理:MediaCodec 利用硬件加速来处理视频和音频数据,可以显著提高编解码的效率和性能。
  • 格式支持:MediaCodec 支持多种编解码格式,包括但不限于 H.264、H.265、VP8、VP9、AAC、HEVC 等。
  • 可扩展性:开发者可以根据需要扩展 MediaCodec 的功能,例如添加新的编解码器或支持新的媒体格式。
  • 兼容性:MediaCodec 支持从 Android 4.1(Jelly Bean,API 级别 16)到最新版本的 Android 系统。
  • 异步操作:MediaCodec 采用异步操作模式,可以在后台线程中处理编解码任务,从而不会阻塞主线程。
  • 缓冲管理:MediaCodec 提供了对输入输出缓冲区的管理,允许开发者控制数据流的传输和处理。
  • 配置灵活性:开发者可以通过配置 MediaCodec 的参数来调整编解码器的行为,例如设置编码质量、比特率、帧率等。
  • 实时处理:MediaCodec 支持实时视频和音频的编解码,适用于需要快速响应的应用,如视频通话、直播等。
  • 安全性:MediaCodec 支持加密和解密操作,可以处理受保护的媒体内容。
  • 示例和文档:MediaCodec 有丰富的示例代码和文档,帮助开发者快速上手和解决常见问题。

MediaCodec 是 Android 开发者在处理多媒体数据时的重要工具,特别是在需要处理大量视频和音频数据的场景中。通过使用 MediaCodec,开发者可以构建高效、灵活且功能强大的多媒体应用。针对本文的实际需求,这里主要使用了MediaCodec编码相关知识。参照了如下文章的内容及代码实现:Android APP 音视频(02)MediaProjection录屏与MediaCodec编码

CameraX预览与MediaCodec编码代码完整解读(android Q)

2.1 关于权限部分的处理

关于权限,需要在AndroidManifest.xml中添加权限,具体如下所示:

<uses-featureandroid:name="android.hardware.camera"android:required="false" />
<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.CAMERA"/>

关于运行时权限的请求等,这里给出一个工具类参考代码,具体如下所示:

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 编码的处理

关于编码部分,主要是MediaCodec的初始化、编码处理部分和文件写入操作,代码如下所示:

public class H264Encoder {MediaCodec mediaCodec;int index;int width;int height;public H264Encoder(int width, int height) {this.width = width;this.height = height;}public void initMediaCodecEncoder()  {try {mediaCodec = MediaCodec.createEncoderByType("video/avc");MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height);mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); //IDR帧刷新时间mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);mediaCodec.start();} catch (IOException e) {Log.e("TAG",e.toString());//e.printStackTrace();}}public void startMediaCodecEncoder(byte[] input) {int inputBufferIndex = mediaCodec.dequeueInputBuffer(10000);MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();if (inputBufferIndex >= 0) {ByteBuffer inputBuffer =   mediaCodec.getInputBuffer(inputBufferIndex);if (inputBuffer != null) {inputBuffer.clear();inputBuffer.put(input);}mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, computPts(), 0);index++;}int outputBufferIndex =   mediaCodec.dequeueOutputBuffer(bufferInfo,100000);if (outputBufferIndex >= 0) {ByteBuffer  outputBuffer= mediaCodec.getOutputBuffer(outputBufferIndex);byte[] data = new byte[bufferInfo.size];if (outputBuffer != null) {outputBuffer.get(data);}FileUtils.writeBytes(data);FileUtils.writeContent(data);mediaCodec.releaseOutputBuffer(outputBufferIndex, false);}}public int computPts() {return 1000000 / 15 * index;}
}

2.3 CameraX预览与H264编码调用主流程代码参考实现

针对CameraX,添加deps依赖。在项目的 build.gradle 文件中添加 CameraX 库的依赖。build.gradle 中添加deps,具体如下:

dependencies {...implementation libs.camera.viewimplementation "androidx.camera:camera-core:1.3.4"
// CameraX Camera2 extensions[可选]拓展库可实现人像、HDR、夜间和美颜、滤镜但依赖于OEMimplementation "androidx.camera:camera-camera2:1.3.4"
// CameraX Lifecycle library[可选]避免手动在生命周期释放和销毁数据implementation "androidx.camera:camera-lifecycle:1.3.4"
// CameraX View class[可选]最佳实践,最好用里面的PreviewView,它会自行判断用SurfaceView还是TextureView来实现implementation libs.androidx.camera.view.v100alpha23...
}

这里以 H264encoderCameraXActivity 为例,给出一个预览流程与H264编码 代码的参考实现。代码如下所示:

public class H264encoderCameraXActivity extends AppCompatActivity {private Button mButton;Context mContext;private PreviewView previewView;private ImageAnalysis imageAnalysis;private ExecutorService executor;private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;private boolean isCapturePreview = false;ProcessCameraProvider mCameraProvider;Preview mPreview;H264Encoder h264Encode = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);mContext = this;setContentView(R.layout.h264_encode_camerax);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);executor = Executors.newSingleThreadExecutor();previewView = findViewById(R.id.viewFinder);mButton = findViewById(R.id.button);mButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {isCapturePreview = !isCapturePreview;if(isCapturePreview){mButton.setText(R.string.startCapture);startCamera();}else{mButton.setText(R.string.stopCapture);stopCamera();}}});// 初始化 ImageAnalysisimageAnalysis = new ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build();imageAnalysis.setAnalyzer(executor, new ImageAnalysis.Analyzer() {@Overridepublic void analyze(@NonNull ImageProxy imageProxy) {if(h264Encode == null){int width = imageProxy.getWidth();int height = imageProxy.getHeight();h264Encode = new H264Encoder(width, height);h264Encode.initMediaCodecEncoder();}Log.d("XXXX","-----------------get Data");// 处理图像数据h264Encode.startMediaCodecEncoder(getYUVDataFromImageProxy(imageProxy));imageProxy.close();}});}public byte[] getYUVDataFromImageProxy(ImageProxy imageProxy) {// 获取 ImageProxy 的宽度和高度int width = imageProxy.getWidth();int height = imageProxy.getHeight();// 创建一个足够大的数组来存储 YUV 数据int yuvSize = width * height * 3 / 2;byte[] yuvBytes = new byte[yuvSize];// 从 ImageProxy 获取 Y 平面的数据ByteBuffer yBuffer = imageProxy.getPlanes()[0].getBuffer();yBuffer.get(yuvBytes, 0, yBuffer.remaining());// 计算 U 和 V 值的起始位置int uvStart = width * height;// 从 ImageProxy 获取 U 和 V 平面的数据ByteBuffer uBuffer = imageProxy.getPlanes()[1].getBuffer();ByteBuffer vBuffer = imageProxy.getPlanes()[2].getBuffer();// 交错 U 和 V 数据到 yuvBytes 数组中for (int i = 0; i < (height*width / 2); i+=2) {int index = uvStart + i;yuvBytes[index] = uBuffer.get();yuvBytes[index + 1] = vBuffer.get();}return yuvBytes;}private void startCamera() {// 请求 CameraProvidercameraProviderFuture = ProcessCameraProvider.getInstance(this);//检查 CameraProvider 可用性,验证它能否在视图创建后成功初始化cameraProviderFuture.addListener(() -> {try {mCameraProvider = cameraProviderFuture.get();bindPreview(mCameraProvider);} catch (ExecutionException | InterruptedException e) {e.printStackTrace();}}, ContextCompat.getMainExecutor(this));}//选择相机并绑定生命周期和用例private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {mPreview = new Preview.Builder().build();CameraSelector cameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();cameraProvider.unbindAll();cameraProvider.bindToLifecycle(this, cameraSelector, mPreview, imageAnalysis);mPreview.setSurfaceProvider(previewView.getSurfaceProvider());}private void stopCamera() {if ((mCameraProvider != null) && mCameraProvider.isBound(mPreview)) {mCameraProvider.unbindAll();imageAnalysis.clearAnalyzer();executor.shutdown();}}@Overrideprotected void onDestroy() {super.onDestroy();if (imageAnalysis != null) {imageAnalysis.clearAnalyzer(); // 清除分析器}if (executor != null) {executor.shutdown(); // 关闭线程池}}
}

其中布局文件 h264_encode_camerax.xml(可自定义) 内容如下:

<?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"><androidx.camera.view.PreviewViewandroid:id="@+id/viewFinder"android:layout_width="372dp"android:layout_height="240dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"/><Buttonandroid:layout_width="match_parent"android:layout_height="50dp"android:text="@string/startCapture"android:id="@+id/button"app:layout_constraintTop_toBottomOf="@id/viewFinder"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintHorizontal_bias="0.5"/></androidx.constraintlayout.widget.ConstraintLayout>

2.4 CameraX预览与MediaCodec编码 demo实现效果

实际运行效果展示如下:


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

相关文章

git拉完代码总是自动创建一个新的节点

git拉完代码&#xff0c;总是自动生成弹出这个信息 然后还会在git上面留下一个节点&#xff0c;这个节点没啥用&#xff0c;显示着感觉有点碍事。 而且后续的git push 之后&#xff0c;会覆盖掉自己的git commit 的提示&#xff0c;其他人cr代码的时候看到的是 解决方法&#…

24年第三届钉钉杯大学生大数据挑战赛浅析

需要完整资料&#xff0c;请关注WX&#xff1a;“小何数模”&#xff01; 本次钉钉杯大数据挑战赛的赛题已正式出炉&#xff0c;无论是赛题难度还是认可度&#xff0c;该比赛都是仅次于数模国赛的独一档&#xff0c;可以用于国赛前的练手训练。考虑到大家解题实属不易&#xf…

使用umi作为模板如何实现权限管理

三种权限管理的方法&#xff1a; 在做后台管理系统时&#xff0c;难免会使用到权限管理&#xff0c;权限管理方式有三种&#xff0c;分别是&#xff1a;路由、守卫、后端配合。 路由&#xff1a;通过动态路由&#xff0c;根据登录人员不同注册不同的路由&#xff0c;直接让没…

[Spark] 数据倾斜, 原因确定, 解决方法

判断是否存在倾斜 任务执行时间 观察 Spark 任务中各个阶段的执行时间。如果某些任务或阶段的执行时间明显长于其他任务或阶段&#xff0c;可能存在数据倾斜。例如&#xff0c;在 Spark UI 中&#xff0c;如果看到某些 Task 执行时间远远超过平均执行时间&#xff0c;就可能是数…

RK平台:V4l2 抓数据应用

这篇文章以c程序v4l2取流demo为例介绍一下应用取数据流的流程&#xff0c;demo参考官方例程修改&#xff0c;文章最后贴上完整的取流应用。 1.打开设备节点 取数据流的节点一般是video节点&#xff0c;如果是从rkcif节点取数据流的话&#xff0c;一般是video0节点&#xff0c…

LeetCode 2766题: 重新放置石块(原创)

【题目描述】 给你一个下标从 0 开始的整数数组 nums &#xff0c;表示一些石块的初始位置。再给你两个长度 相等 下标从 0 开始的整数数组 moveFrom 和 moveTo 。 在 moveFrom.length 次操作内&#xff0c;你可以改变石块的位置。在第 i 次操作中&#xff0c;你将位置在 moveF…

nginx 安装第三方插件

安装 nginx-http-concat 和 nginx_upstream_check_module 1.新增目录 mkdir -p /var/lib/nginx/third_module 2.下载安装包并解压 # 下载并解压 #nginx_upstream_check_module wget https://codeload.github.com/yaoweibin/nginx_upstream_check_module/zip/master#nginx-…

ROM修改进阶教程------修改rom 开机自动安装指定apk 自启脚本完整步骤解析

rom修改的初期认识 在解包修改系统分区过程中。很多客户需求刷完rom后自动安装指定apk。这种与内置apk有区别。而且一些极个别apk无法内置。今天对这种修改rom刷入机型后第一次启动后自动安装指定apk的需求做个步骤解析。 在前期博文中我有做过说明。官方系统固件解…