当前位置: 首页 > article >正文

深入理解 Android MediaCodec 视频编码

1. 了解 MediaCodec

Android 提供 MediaCodec API 用于硬件加速视频编码和解码。相比于 FFmpeg 等软件编码器,MediaCodec 直接使用设备的硬件编码器,具有 低功耗、低延迟 的特点,适用于 实时视频处理(如视频通话、直播)。

2. 从 Camera2 获取视频数据

在 Android 上,我们可以使用 Camera2 API 获取 YUV 格式的视频帧,并将其传递给 MediaCodec 进行编码。

Camera2 采集视频帧

val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraId = cameraManager.cameraIdList[0] // 选择第一个摄像头
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val previewSize = map?.getOutputSizes(SurfaceTexture::class.java)?.first() ?: Size(1280, 720)

3. MediaCodec 只能接受 YUV,不支持 RGB

MediaCodec 硬件编码器通常 只支持 YUV 格式,而不支持 RGB。原因如下:

  • YUV 更适合视频压缩:YUV 分量将亮度与色度分离,便于 降低色度数据的精度,从而减少带宽占用。
  • RGB 存储空间大:RGB 采用 24-bit 存储,而 YUV 可以用 12-bit(YUV420) 进行高效存储。

如何将 RGB 转换为 YUV

如果你的数据是 RGB,可以使用 RenderScriptOpenGL ES 进行转换,或者使用 libyuv 这样的库。

val rs = RenderScript.create(context)
val yuvConverter = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
// 转换逻辑...

4. 如何确定宽高、帧率、码率、关键帧间隔

1. 分辨率(宽高)

通常选择 相机支持的分辨率目标应用需求。常见的选择:

  • 1920×1080(1080p)→ 高清录像
  • 1280×720(720p)→ 大多数中端设备
  • 640×480(VGA)→ 低带宽应用(如视频通话)
val width = 1280
val height = 720

2. 帧率(Frame Rate)

帧率影响视频流畅度,一般和相机支持的帧率一致。

  • 30fps(默认)→ 适用于普通视频
  • 60fps → 适用于高帧率游戏录制
  • 15fps → 适用于低带宽视频
val frameRate = 30

3. 码率(Bitrate)

码率决定了视频质量和文件大小。经验公式:

val bitRate = width * height * frameRate * 0.1 // 经验公式
  • 1080p:4000-8000kbps
  • 720p:2500-5000kbps
  • 480p:1000-2500kbps
val mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height)
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate.toInt())

4. 关键帧间隔(I-frame Interval)

  • 1 秒(每 30 帧 1 个 I-frame) → 适用于视频通话
  • 2-5 秒 → 适用于普通视频
  • 10 秒以上 → 存储优化
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)

5. 同步 vs 异步模式

MediaCodec 支持 同步(blocking)异步(callback) 两种模式:

同步模式(低延迟场景)

  • 适用于 低延迟场景,如 视频通话、游戏录制
  • dequeueInputBuffer() 获取输入 buffer,填充数据后 queueInputBuffer()
  • dequeueOutputBuffer() 获取编码后的数据。
val inputBufferIndex = mediaCodec.dequeueInputBuffer(10000)
if (inputBufferIndex >= 0) {
    val inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex)
    inputBuffer?.put(yuvData)
    mediaCodec.queueInputBuffer(inputBufferIndex, 0, yuvData.size, timestamp, 0)
}

异步模式(适用于后台处理)

  • MediaCodec.Callback() 处理数据,适用于 后台编码 场景。
mediaCodec.setCallback(object : MediaCodec.Callback() {
    override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
        val inputBuffer = codec.getInputBuffer(index)
        inputBuffer?.put(yuvData)
        codec.queueInputBuffer(index, 0, yuvData.size, timestamp, 0)
    }
    override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
        val outputBuffer = codec.getOutputBuffer(index)
        // 处理编码后的数据
        codec.releaseOutputBuffer(index, false)
    }
})

6. 总结

参数说明
分辨率根据相机支持的分辨率选择(如 720p、1080p)
帧率一般和相机帧率一致(如 30fps)
码率经验公式 width × height × frameRate × 0.1
关键帧间隔1-5 秒(适用于普通视频),10 秒以上(存储优化)
同步模式适用于低延迟场景,如视频通话
异步模式适用于后台处理

MediaCodec 是 Android 进行硬件视频编码的核心组件,理解如何正确配置 MediaCodec,能够优化编码质量、减少延迟,并适应不同的应用场景。

7. 完整的 Camera2 + MediaCodec 编码 Demo

以下是一个完整的示例代码,展示如何使用 Camera2 采集视频数据并通过 MediaCodec 进行 H.264 编码。

import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaCodecInfo;
import android.os.Build;
import android.os.Bundle;
import android.view.Surface;
import android.view.TextureView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.hardware.camera2.*;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;

import java.nio.ByteBuffer;
import java.util.Collections;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "CameraEncoder";
    private CameraDevice cameraDevice;
    private CameraCaptureSession captureSession;
    private MediaCodec mediaCodec;
    private TextureView textureView;
    private ImageReader imageReader;
    private MediaCodec.BufferInfo bufferInfo;

    private static final int REQUEST_CAMERA_PERMISSION = 200;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textureView = findViewById(R.id.textureView);
        bufferInfo = new MediaCodec.BufferInfo();

        // 初始化 MediaCodec
        initMediaCodec();

        // 检查相机权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
            } else {
                startCamera();  // 权限已经获得,可以启动相机
            }
        } else {
            startCamera();  // 在低版本 Android 上直接启动相机
        }
    }

    // 请求权限后的回调方法
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CAMERA_PERMISSION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                startCamera();  // 权限授权成功,启动相机
            } else {
                Toast.makeText(this, "相机权限被拒绝", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void startCamera() {
        CameraManager cameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
        try {
            String cameraId = cameraManager.getCameraIdList()[0];  // 获取第一个摄像头
            CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
            cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice camera) {
                    cameraDevice = camera;
                    createCameraPreviewSession();
                }

                @Override
                public void onDisconnected(@NonNull CameraDevice camera) {
                    cameraDevice.close();
                }

                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                    Log.e(TAG, "Camera error: " + error);
                }
            }, null);
        } catch (Exception e) {
            Log.e(TAG, "Camera initialization error: ", e);
        }
    }

    private void createCameraPreviewSession() {
        try {
            // 创建 ImageReader,获取摄像头帧,设置为 YUV 格式
            imageReader = ImageReader.newInstance(1920, 1080, ImageFormat.YUV_420_888, 2);
            Surface previewSurface = new Surface(textureView.getSurfaceTexture());
            imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
                @Override
                public void onImageAvailable(ImageReader reader) {
                    Image image = reader.acquireLatestImage();
                    if (image != null) {
                        // 获取图像数据并传递给编码方法
                        ByteBuffer inputBuffer = image.getPlanes()[0].getBuffer();
                        int length = inputBuffer.remaining();
                        encodeFrame(inputBuffer, length);
                        image.close();
                    }
                }
            }, null);

            // 设置摄像头预览请求
            CaptureRequest.Builder previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            previewRequestBuilder.addTarget(previewSurface);

            cameraDevice.createCaptureSession(Collections.singletonList(previewSurface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                    if (cameraDevice == null) return;
                    captureSession = session;
                    try {
                        CaptureRequest previewRequest = previewRequestBuilder.build();
                        captureSession.setRepeatingRequest(previewRequest, null, null);
                    } catch (Exception e) {
                        Log.e(TAG, "Error setting up preview: ", e);
                    }
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                    Log.e(TAG, "Camera configuration failed");
                }
            }, null);

        } catch (Exception e) {
            Log.e(TAG, "Error creating camera preview session: ", e);
        }
    }


    // 初始化 MediaCodec 来处理摄像头视频流
    private void initMediaCodec() {
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1250000);
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);

            mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mediaCodec.start();
        } catch (Exception e) {
            Log.e(TAG, "MediaCodec initialization failed: ", e);
        }
    }

    private void encodeFrame(ByteBuffer inputBuffer, int length) {
        try {
            int inputBufferIndex = mediaCodec.dequeueInputBuffer(10000);
            if (inputBufferIndex >= 0) {
                ByteBuffer buffer = mediaCodec.getInputBuffer(inputBufferIndex);
                buffer.clear();
                buffer.put(inputBuffer);
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, System.nanoTime() / 1000, 0);

                // 获取编码后的数据
                int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
                while (outputBufferIndex >= 0) {
                    ByteBuffer encodedData = mediaCodec.getOutputBuffer(outputBufferIndex);
                    byte[] encodedBytes = new byte[bufferInfo.size];
                    encodedData.get(encodedBytes);
                    // 在这里可以处理编码后的数据(如保存为文件或推流)

                    mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                    outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Error encoding frame: ", e);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (cameraDevice != null) {
            cameraDevice.close();
        }
        if (mediaCodec != null) {
            mediaCodec.stop();
            mediaCodec.release();
        }
    }
}

布局文件 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextureView
        android:id="@+id/textureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

说明

  1. 权限管理:请求 CAMERA 权限,确保相机可用。
  2. Camera2 API:通过 CameraManager 获取相机 ID,打开相机,设置 CaptureRequest
  3. MediaCodec 编码:创建 MediaCodec,配置编码格式,使用 Surface 作为输入源。
  4. Camera2 采集 & 发送数据:将 Surface 作为相机输出目标,连续采集视频数据并编码。
  5. 释放资源:在 onDestroy 关闭相机和编码器,防止资源泄漏。

这个 Demo 展示了如何使用 Camera2 API 获取视频流,并通过 MediaCodec 进行 H.264 编码,最终可用于实时推流或本地存储。


http://www.kler.cn/a/571911.html

相关文章:

  • React进阶之前端业务Hooks库(六)
  • 遗传算法详解及在matlab中的使用
  • SSM记忆旅游网站
  • 基于 RBAC 的前端权限管理实现教程
  • ADB、Appium 和 大模型融合开展移动端自动化测试
  • 路由基础学习
  • 清华团队提出HistoCell,从组织学图像推断超分辨率细胞空间分布助力癌症研究|顶刊精析·25-03-02
  • 自由学习记录(40)
  • 基于微信小程序的停车场管理系统的设计与实现
  • Tomcat:Java Web应用的强大支撑
  • 05 HarmonyOS NEXT高效编程秘籍:Arkts函数调用与声明优化深度解析
  • 复合机器人为 CNC 毛坯件上下料注入 “智能强心针”
  • CentOS 7中安装Dify
  • Docker 容器的数据卷
  • LeetCode 42.接雨水
  • 虚拟机IP的配置,让它上网
  • 奖学金(acwing)c++
  • Redis 排行榜实现:处理同分数时的排名问题
  • 探秘基带算法:从原理到5G时代的通信变革【八】QAM 调制 / 解调
  • SSH远程登录并执行命令