深入理解 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,可以使用 RenderScript
或 OpenGL 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>
说明
- 权限管理:请求
CAMERA
权限,确保相机可用。 - Camera2 API:通过
CameraManager
获取相机 ID,打开相机,设置CaptureRequest
。 - MediaCodec 编码:创建
MediaCodec
,配置编码格式,使用Surface
作为输入源。 - Camera2 采集 & 发送数据:将
Surface
作为相机输出目标,连续采集视频数据并编码。 - 释放资源:在
onDestroy
关闭相机和编码器,防止资源泄漏。
这个 Demo 展示了如何使用 Camera2 API 获取视频流,并通过 MediaCodec 进行 H.264 编码,最终可用于实时推流或本地存储。