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实现效果

实际运行效果展示如下:

相关推荐
ShanLiang_Build37 分钟前
Android 使用Charles抓包显示Unknown
android
逃课的小学生1 小时前
android 安全sdk相关
android
今人不见古时月,今月曾经照古人1 小时前
android studio Terminal控制台命令打包 apk
android·ide·android studio
Winston Wood1 小时前
Android 图形系统之一:概览
android·图形系统
柯南二号1 小时前
Android 实现双列图片瀑布流式布局
android·recycleview·瀑布流
manok2 小时前
Android - 权限管理漏洞
android·sast·静态分析规则
冷眼看人间恩怨2 小时前
【Android】AnimationDrawable帧动画的实现
android·帧动画
飘逸高铁侠2 小时前
使用 Python 删除视频的某一段并保留其他时间段
python·音视频·剪辑
EasyNVR2 小时前
NVR录像机汇聚管理EasyNVR多个NVR同时管理基于B/S架构的技术特点与能力应用
安全·架构·音视频·监控·视频监控
所待.3832 小时前
MySQL之视图
android·数据库·mysql