说明: 此CameraX预览和编码实操主要针对Android12.0系统。通过CameraX预览获取yuv格式数据,将yuv格式数据通过mediacodec编码输出H264码流(使用ffmpeg播放),存储到sd卡上。
1 CameraX 和 MediaCodec简介
1.1 CameraX简介
CameraX 是一个由 Google 开发的 Android Jetpack 库,旨在简化 Android 应用中的相机操作。它提供了一个一致的 API 界面,使得开发者可以更容易地在应用中集成和使用相机功能。以下是 CameraX 的一些关键特点和优势:
- 简化的 API:CameraX 提供了一个简单且一致的 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编码
2 CameraX预览与MediaCodec编码代码完整解读(android Q)
2.1 关于权限部分的处理
关于权限,需要在AndroidManifest.xml中添加权限,具体如下所示:
<uses-feature
android: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.view
implementation "androidx.camera:camera-core:1.3.4"
// CameraX Camera2 extensions[可选]拓展库可实现人像、HDR、夜间和美颜、滤镜但依赖于OEM
implementation "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;
@Override
protected 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() {
@Override
public void onClick(View view) {
isCapturePreview = !isCapturePreview;
if(isCapturePreview){
mButton.setText(R.string.startCapture);
startCamera();
}else{
mButton.setText(R.string.stopCapture);
stopCamera();
}
}
});
// 初始化 ImageAnalysis
imageAnalysis = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(executor, new ImageAnalysis.Analyzer() {
@Override
public 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() {
// 请求 CameraProvider
cameraProviderFuture = 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();
}
}
@Override
protected 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.PreviewView
android: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"
/>
<Button
android: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实现效果
实际运行效果展示如下: