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

说明: 此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中添加权限,具体如下所示:

html 复制代码
<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"/>

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

java 复制代码
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的初始化、编码处理部分和文件写入操作,代码如下所示:

java 复制代码
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,具体如下:

bash 复制代码
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编码 代码的参考实现。代码如下所示:

java 复制代码
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(可自定义) 内容如下:

html 复制代码
<?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实现效果

实际运行效果展示如下:

相关推荐
贺biubiu7 小时前
2025 年终总结|总有那么一个人,会让你千里奔赴...
android·程序员·年终总结
xuekai200809017 小时前
mysql-组复制 -8.4.7 主从搭建
android·adb
nono牛8 小时前
ps -A|grep gate
android
未知名Android用户9 小时前
Android动态变化渐变背景
android
行业探路者9 小时前
二维码标签是什么?主要有线上生成二维码和文件生成二维码功能吗?
学习·音视频·语音识别·二维码·设备巡检
nono牛10 小时前
Gatekeeper 的精确定义
android
stevenzqzq11 小时前
android启动初始化和注入理解3
android
城东米粉儿13 小时前
compose 状态提升 笔记
android
粤M温同学14 小时前
Android 实现沉浸式状态栏
android
Android系统攻城狮14 小时前
Android16音频之获取Record状态AudioRecord.getState:用法实例(一百七十七)
音视频·android16·音频进阶