主题 :Android 硬编码 · MediaCodec · MediaFormat · 码率控制 · H.264/H.265/HEVC · AAC
前置知识:Android 多媒体基础(Surface/Canvas/Bitmap)、音视频编码基本概念(I/P/B帧、AAC Profile)
一、引言:为什么 MediaCodec 编码参数如此关键
Android 的 MediaCodec API 封装了对硬件编码器的访问接口。编码参数配置的正确与否,直接决定了:
- 视频质量:码率、色彩格式、GOP 结构
- 编码性能:编码速度、CPU/GPU 占用、功耗
- 输出兼容性:Profile/Level 决定可播放范围
- 延迟控制:实时通信 vs 离线录制的参数差异
- 硬件适配:不同设备编码器能力差异巨大
一个配置不当的编码器,轻则产生马赛克/花屏,重则编码器初始化失败直接崩溃。本文系统梳理所有编码参数,提供可直接运行的 Demo,覆盖视频编码器、音频编码器的完整参数体系。
二、MediaCodec 编码器全景架构
2.1 编码器的数据流
┌─────────────────────────────────────────────────────────────┐
│ MediaCodec Encoder │
│ │
│ 输入(生产者) │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ YUV/RGB 数据 │───→│ InputBuffer 队列 │ │
│ │ ByteBuffer │ │ queueInputBuffer() │ │
│ │ 或 Surface │ │ createInputSurface() │ │
│ └──────────────┘ └────────────────┬─────────────────┘ │
│ │ │
│ 编码器核心(硬件 ASIC / DSP / GPU) │ │
│ ┌────────────────────────────────────▼──────────────────┐ │
│ │ ┌────────────┐ ┌──────────────┐ ┌────────────┐ │ │
│ │ │ H.264/HEVC │ │ AAC/Opus │ │ VP8/VP9 │ │ │
│ │ │ 编码核 │ │ 编码核 │ │ 编码核 │ │ │
│ │ └────────────┘ └──────────────┘ └────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ 输出(消费者) │
│ ┌────────────────┬───────────────────────────────┐ │
│ │ H.264 NALU │ OutputBuffer 队列 │ │
│ │ AAC ES 流 │ dequeueOutputBuffer() │ │
│ └────────────────┴──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 三种编码模式
| 模式 | API | 输入方式 | 适用场景 | 性能 |
|---|---|---|---|---|
| ByteBuffer | getInputBuffer() |
手动填充 YUV/RGB 数据 | 软硬件混合、需要预处理 | 中等 |
| Surface | createInputSurface() |
Surface 直接接收 Camera/Canvas 绘制 |
Camera 实时编码、屏幕录制 | 最优 |
| Image | getInputImage() |
读写字节缓冲 | 特殊格式处理 | 较低 |
最佳实践 :优先使用 Surface 模式------数据无需在 APP 层拷贝,直接由硬件写入,解码侧同理。CameraX → Surface → MediaCodec → Surface 是目前最低延迟的 pipeline。
2.3 编码器创建方法
kotlin
// ===== 方法一:按 MIME 类型创建(最常用)=====
val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
// ===== 方法二:按编码器名称创建(需要特定编码器时)=====
val encoderList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
for (info in encoderList.codecInfos) {
if (!info.isEncoder) continue
if (info.name == "OMX.qcom.video.encoder.avc") {
val encoder = MediaCodec.createByCodecName(info.name)
}
}
// ===== 方法三:查询某 MIME 类型是否有编码器 =====
val hasEncoder = MediaCodecList(MediaCodecList.REGULAR_CODECS)
.codecInfos.any { !it.isEncoder && it.supportedTypes.contains("video/avc") }
三、MediaFormat:编码参数的载体
MediaFormat 是 Android 多媒体参数的核心载体,所有编码参数都通过它传递给编码器:
kotlin
// ===== 创建视频格式 =====
val videoFormat = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, // MIME 类型
1920, // 宽度
1080 // 高度
)
// ===== 设置参数 =====
videoFormat.apply {
setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000) // 8 Mbps
setInteger(MediaFormat.KEY_FRAME_RATE, 30) // 30 fps
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) // 每秒 1 个 I 帧
setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.VideoEncoderCapabilities.BITRATE_MODE_CBR)
}
// ===== 配置编码器 =====
encoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
encoder.start()
四、视频编码参数详解
4.1 必需参数(缺一不可)
4.1.1 KEY_MIME:编码格式
| MIME 常量 | 标准名称 | 容器封装 | 备注 |
|---|---|---|---|
video/avc |
H.264 / AVC | MP4 / MKV / TS | 最广泛兼容 |
video/hevc |
H.265 / HEVC | MP4 / MKV | Android 5.0+ |
video/vp8 |
VP8 | WebM / MKV | Android 4.0+ |
video/vp9 |
VP9 | WebM / MKV | Android 4.4+ |
video/av01 |
AV1 | MP4 / MKV | Android 10+ |
kotlin
// 各格式编码器创建
val h264Encoder = MediaCodec.createEncoderByType("video/avc")
val h265Encoder = MediaCodec.createEncoderByType("video/hevc")
val vp8Encoder = MediaCodec.createEncoderByType("video/vp8")
val vp9Encoder = MediaCodec.createEncoderByType("video/vp9")
val av1Encoder = MediaCodec.createEncoderByType("video/av01")
4.1.2 KEY_WIDTH / KEY_HEIGHT:分辨率
kotlin
videoFormat.setInteger(MediaFormat.KEY_WIDTH, 1920)
videoFormat.setInteger(MediaFormat.KEY_HEIGHT, 1080)
常见分辨率对照表:
| 分辨率 | 标识 | 典型码率(1080p@30fps) | 典型码率(720p@30fps) |
|---|---|---|---|
| 7680×4320 | 8K UHD | 80--120 Mbps | --- |
| 3840×2160 | 4K UHD | 35--60 Mbps | --- |
| 2560×1440 | 2K QHD | 15--25 Mbps | --- |
| 1920×1080 | FHD | 8--15 Mbps | 5--8 Mbps |
| 1280×720 | HD | 4--8 Mbps | 2--4 Mbps |
| 854×480 | SD | 1--3 Mbps | 1--2 Mbps |
| 640×360 | LD | 0.5--1.5 Mbps | 0.5--1 Mbps |
| 426×240 | 最小 | 0.3--0.8 Mbps | 0.3--0.5 Mbps |
分辨率必须是 2 的倍数(编码器内部以 16×16 宏块为单位):宽度/高度必须为 2 的倍数,推荐为 16 的倍数以获得最佳性能。
4.1.3 KEY_BIT_RATE:码率(最核心参数)
kotlin
// 设置码率(单位:bits per second)
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000) // 8 Mbps
// 音频码率
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128_000) // 128 kbps
常见码率推荐表:
| 场景 | 分辨率 | 帧率 | 推荐码率 | 编码器模式 |
|---|---|---|---|---|
| 直播(运动多) | 1920×1080 | 30 | 6--10 Mbps | CBR |
| 直播(静止为主) | 1920×1080 | 30 | 3--6 Mbps | VBR |
| 点播录制 | 1920×1080 | 30 | 5--12 Mbps | VBR |
| 屏幕录制 | 屏幕分辨率 | 屏幕帧率 | 8--20 Mbps | CQ |
| 视频通话 | 1280×720 | 30 | 1.5--4 Mbps | CBR |
| 视频通话(弱网) | 640×480 | 15 | 0.5--1.5 Mbps | CBR |
| 监控存储 | 1920×1080 | 15 | 2--5 Mbps | VBR |
| 慢动作(120fps) | 1280×720 | 120 | 10--20 Mbps | CBR |
4.1.4 KEY_FRAME_RATE:帧率
kotlin
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
| 帧率 | 适用场景 | 运动容限 | 带宽消耗 |
|---|---|---|---|
| 120 fps | 慢动作回放 | 极高 | 4× 30fps |
| 60 fps | 游戏录制、高清直播 | 高 | 2× 30fps |
| 30 fps | 标准视频、会议 | 中 | 基准 |
| 24 fps | 电影风格 | 低 | 0.8× 30fps |
| 15 fps | 弱网通话、屏幕共享 | 很低 | 0.5× 30fps |
| 10 fps | 极低带宽监控 | 极低 | 0.33× 30fps |
⚠️ API 约束 :
KEY_FRAME_RATE是建议值 ,编码器可能忽略此参数而使用设备支持的帧率。部分编码器会在configure()后通过getOutputFormat()返回实际使用的帧率。
4.1.5 KEY_I_FRAME_INTERVAL:I 帧间隔(GOP 长度)
kotlin
// 每 1 秒一个 I 帧(即 GOP = 1 秒)
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
// 每 2 秒一个 I 帧
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
// 每 5 秒一个 I 帧
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
// 全部 I 帧(无压缩,仅适用于屏幕录制等特殊场景)
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0)
// Android 11+(API 31):I 帧后跟 P 帧,无 B 帧
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, -1)
GOP(Group of Pictures)结构说明:
GOP = 1 秒,帧率 = 30 fps 时:
I[0] P[1] P[2] P[3] P[4] P[5] P[6] ... P[29] | I[30] P[31] ...
I帧 ↓ ↓ ↓ ↓ ↓ ↓ ↓ I帧
P帧参考前一帧(P[1]参考I[0],P[2]参考P[1]...)
I 帧大小:约 100--500 KB
P 帧大小:约 10--50 KB
总 GOP 大小:约 300--800 KB
码率 = GOP大小 / GOP时长 = ~400KB / 1s ≈ 3.2 Mbps
I 帧间隔对系统的影响:
| I 帧间隔 | 优势 | 劣势 |
|---|---|---|
| 0(全部 I 帧) | 最快 seek,无预测误差 | 码率极高(5--10×),存储大 |
| 1s | 适中,seek 较快 | 码率略高 |
| 2--5s | 码率节省,较好质量 | seek 需等待 I 帧 |
| 10s+ | 最低码率 | seek 卡顿明显 |
实时通信推荐 :1--2 秒(兼顾质量和 seek 体验);离线录制推荐:5--10 秒(质量优先)。
4.2 色彩格式参数
色彩格式决定了 YUV 数据的内存布局,直接影响编码效率和兼容性。
4.2.1 色彩格式全解析
| 常量 | 别名 | YUV 布局 | Y 平面 | U 平面 | V 平面 | 兼容性 |
|---|---|---|---|---|---|---|
COLOR_FormatSurface |
--- | Surface | GPU 内部 | GPU 内部 | GPU 内部 | 最优(零拷贝) |
COLOR_FormatYUV420Flexible |
--- | 灵活 | 线性 | 线性 | 线性 | 最广(API 21+) |
COLOR_FormatYUV420Planar |
I420 / YV12 | 平面 | W×H | W/2×H/2 | W/2×H/2 | 最广 |
COLOR_FormatYUV420SemiPlanar |
NV12 | 半平面 | W×H | W/2×H/2 | W×H/2 (交错) | 高 |
COLOR_FormatYUV420PackedSemiPlanar |
NV21 | --- | --- | --- | --- | 中 |
COLOR_FormatRGBA |
--- | --- | 4 通道 RGBA | --- | --- | 用于 Canvas |
内存布局图解:
COLOR_FormatYUV420Planar (I420):
┌─────────────────┬─────────────────┐
│ Y Plane │ U Plane │
│ (W × H) │ (W/2 × H/2) │
└─────────────────┴─────────────────┤
│ V Plane │
│ (W/2 × H/2) │
└─────────────────┘
内存总大小 = W×H + W×H/4 + W×H/4 = 1.5 × W × H 字节
COLOR_FormatYUV420SemiPlanar (NV12):
┌─────────────────┬─────────────────┐
│ Y Plane │ UV Interleaved │
│ (W × H) │ (W × H/2) │
└─────────────────┴─────────────────┘
内存总大小 = W×H + W×H/2 = 1.5 × W × H 字节
COLOR_FormatSurface(Surface 模式):
┌─────────────────────────┐
│ GPU 内部格式 │
│ (编码器直接访问) │
│ 无 APP 层 YUV 拷贝 │
└─────────────────────────┘
4.2.2 如何查询可用色彩格式
kotlin
fun printAvailableColorFormats(mime: String) {
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
for (info in codecList.codecInfos) {
if (info.isEncoder && info.supportedTypes.any { it == mime }) {
val caps = info.getCapabilitiesForType(mime)
Log.d("Codec", "编码器: ${info.name}, Mime: $mime")
for (cf in caps.colorFormats) {
val name = when (cf) {
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface -> "Surface(零拷贝)"
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible -> "YUV420Flexible"
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar -> "YUV420Planar (I420)"
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar -> "YUV420SemiPlanar (NV12)"
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar -> "YUV420PackedSemiPlanar (NV21)"
MediaCodecInfo.CodecCapabilities.COLOR_FormatRGBA -> "RGBA"
else -> "未知 (0x${cf.toString(16)})"
}
Log.d("Codec", " 色彩格式: $name (0x${cf.toString(16)})")
}
}
}
}
// 调用
printAvailableColorFormats(MediaFormat.MIMETYPE_VIDEO_AVC)
典型输出示例(某高通设备):
编码器: OMX.qcom.video.encoder.avc, Mime: video/avc
色彩格式: Surface(零拷贝)(0x4)
色彩格式: YUV420Flexible (0x23)
色彩格式: YUV420SemiPlanar (NV12) (0x15)
4.2.3 ByteBuffer 模式下的 YUV 填充
kotlin
// 将 Bitmap 转换为 NV12/NV21 并填入 InputBuffer
fun fillInputBuffer(
encoder: MediaCodec,
inputBufIndex: Int,
bitmap: Bitmap,
isNV21: Boolean = false // NV21 vs NV12
) {
val inputBuf = encoder.getInputBuffer(inputBufIndex) ?: return
inputBuf.clear()
val w = bitmap.width
val h = bitmap.height
// 1. 将 Bitmap 转换为 YUV
val yuvData = ByteArray(w * h * 3 / 2)
val argb = IntArray(w * h)
bitmap.getPixels(argb, 0, w, 0, 0, w, h)
rgbToNv21(argb, yuvData, w, h, isNV21)
// 2. 写入 InputBuffer
inputBuf.put(yuvData)
}
// RGB → NV21 转换
private fun rgbToNv21(rgb: IntArray, yuv: ByteArray, w: Int, h: Int, nv21: Boolean) {
val frameSize = w * h
var yIndex = 0
var uvIndex = frameSize
for (j in 0 until h) {
for (i in 0 until w) {
val pixel = rgb[j * w + i]
val r = (pixel shr 16) and 0xFF
val g = (pixel shr 8) and 0xFF
val b = pixel and 0xFF
// Y 分量
val y = ((66 * r + 129 * g + 25 * b + 128) shr 8) + 16
yuv[yIndex++] = y.coerceIn(0, 255).toByte()
// UV 分量(每 2×2 像素块共享一个 UV)
if (j % 2 == 0 && i % 2 == 0 && uvIndex < yuv.size - 1) {
val u = ((-38 * r - 74 * g + 112 * b + 128) shr 8) + 128
val v = ((112 * r - 94 * g - 18 * b + 128) shr 8) + 128
if (nv21) {
yuv[uvIndex++] = u.coerceIn(0, 255).toByte()
yuv[uvIndex++] = v.coerceIn(0, 255).toByte()
} else {
yuv[uvIndex++] = v.coerceIn(0, 255).toByte()
yuv[uvIndex++] = u.coerceIn(0, 255).toByte()
}
}
}
}
}
4.3 码率控制模式(BITRATE_MODE)
这是最容易被忽视但影响最大的参数组。
4.3.1 四种码率控制模式
| 模式 | 常量 | 码率特征 | 质量特征 | 适用场景 | API 版本 |
|---|---|---|---|---|---|
| CQ | BITRATE_MODE_CQ |
波动 | 质量恒定 | 高质量本地录制、电影级 | API 21+ |
| VBR | BITRATE_MODE_VBR |
波动 | 质量优先 | 点播、VOD、存储 | API 21+ |
| CBR | BITRATE_MODE_CBR |
恒定 | 稳定码率 | 直播、实时通信、CDN | API 21+ |
| CBR_FD | BITRATE_MODE_CBR_FD |
恒定 | 允许丢帧 | 极低带宽限制 | API 29+ |
kotlin
// API 29+:通过 VideoEncoderCapabilities 设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val caps = encoder.getVideoCapabilities()
val supportedModes = caps.supportedBitrateModes
if (supportedModes.contains(VideoEncoderCapabilities.BITRATE_MODE_CBR)) {
videoFormat.setInteger(
MediaFormat.KEY_BITRATE_MODE,
MediaFormat.BITRATE_MODE_CBR
)
}
}
// API 21-28:某些设备通过 KEY_PROFILE 设置隐式模式
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000)
4.3.2 码率控制模式详细对比
┌──────────────────────────────────────────────────────────────┐
│ CQ(Constant Quality) │
│ 码率图: ╱╲ ╱╲ │
│ ╱ ╲ ╱ ╲ 质量恒定(目标 QP) │
│ ╱ ╲╱ ╲ 码率波动(场景复杂↑,码率↑) │
│ ╱ 适用:高质量本地录制 │
├──────────────────────────────────────────────────────────────┤
│ VBR(Variable Bitrate) │
│ 码率图: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ │
│ 场景复杂度高 场景复杂度低 │
│ 质量下限保障(min bitrate) │
│ 质量上限限制(max bitrate) │
│ 适用:点播、存储、省流量 │
├──────────────────────────────────────────────────────────────┤
│ CBR(Constant Bitrate) │
│ 码率图: ━━━━━━━━━━━━━━━━━━━━━━ │
│ 码率恒定 │
│ 质量随场景波动(复杂→量化参数↑→质量↓) │
│ 适用:直播、实时通信、带宽稳定需求 │
├──────────────────────────────────────────────────────────────┤
│ CBR_FD(CBR with Frame Drop) │
│ 码率图: ━━━━ ━━ ━━━━ ░░░(丢帧) │
│ 超出码率能力时,主动丢帧而非降低质量 │
│ 适用:极低带宽 + 必须保持实时性 │
└──────────────────────────────────────────────────────────────┘
各模式推荐配置:
kotlin
// 场景 1:高质量本地录像(推荐 CQ 或 VBR)
val recordFormat = MediaFormat.createVideoFormat("video/avc", 1920, 1080).apply {
setInteger(KEY_BIT_RATE, 15_000_000) // 15 Mbps(1080p 高质量)
setInteger(KEY_FRAME_RATE, 30)
setInteger(KEY_I_FRAME_INTERVAL, 2)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setInteger(KEY_BITRATE_MODE, MediaFormat.BITRATE_MODE_CQ)
}
}
// 场景 2:实时通信(推荐 CBR)
val rtcFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720).apply {
setInteger(KEY_BIT_RATE, 2_000_000) // 2 Mbps(720p 通话)
setInteger(KEY_FRAME_RATE, 30)
setInteger(KEY_I_FRAME_INTERVAL, 1)
setInteger(KEY_BITRATE_MODE, MediaFormat.BITRATE_MODE_CBR)
// 可选:降低 I 帧大小(通话中 I 帧会明显卡顿)
setInteger(KEY_INTRA_REFRESH_PERIOD, 30) // API 23+:周期帧内刷新
}
// 场景 3:直播推流(推荐 CBR,兼容 CDN)
val liveFormat = MediaFormat.createVideoFormat("video/avc", 1920, 1080).apply {
setInteger(KEY_BIT_RATE, 6_000_000) // 6 Mbps(1080p 直播)
setInteger(KEY_FRAME_RATE, 30)
setInteger(KEY_I_FRAME_INTERVAL, 2)
setInteger(KEY_BITRATE_MODE, MediaFormat.BITRATE_MODE_CBR)
}
4.4 Profile 与 Level(H.264 / HEVC)
4.4.1 H.264 Profile 对比
| Profile | 编码特性 | 压缩效率 | 兼容性 | 适用场景 |
|---|---|---|---|---|
| Baseline | 无 B 帧、无 CABAC | 最低 | 最广(所有设备) | 实时通信、老设备 |
| Main | B 帧 + CABAC | 中等 | 大部分设备 | 通用场景 |
| High | 所有特性 | 最高 | 中高端设备 | 高质量点播 |
| Constrained High | High - 模拟参考帧 | 高 | 较广 | 实时通信+高质量 |
kotlin
// H.264 Profile 设置
videoFormat.setInteger(
MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh // 推荐 High
)
// 或约束 Baseline(兼容性最佳)
videoFormat.setInteger(
MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline
)
// 或 Constrained Baseline(RTC 推荐)
videoFormat.setInteger(
MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedBaseline
)
Profile 对照表:
Baseline Profile:
├── I/P 帧,无 B 帧
├── CABAC:❌(仅 CAVLC)
├── 宏块:16×16 / 8×8 / 4×4
└── 隔行扫描:❌
Main Profile:
├── I/P/B 帧
├── CABAC:✅
├── 宏块:16×16 / 8×8 / 4×4
└── 隔行扫描:✅(MBAFF)
└── 权重预测:❌
High Profile:
├── I/P/B 帧
├── CABAC:✅
├── 宏块:16×16 / 8×8 / 4×4
├── 自适应宏块划分:✅(4×4→16×16)
├── 独立分区:✅
├── 加权预测:✅(FMO + ASO)
└── 8×8 DCT 变换:✅(比 4×4 更平滑)
4.4.2 H.264 Level 与设备能力
Level 决定了分辨率、帧率、码率的上限:
| Level | 最大分辨率 | 最大帧率 | 最大码率 Baseline | 最大码率 High |
|---|---|---|---|---|
| 1 | 176×144 | 15 | 64 kbps | 80 kbps |
| 1b | 176×144 | 15 | 128 kbps | 160 kbps |
| 1.1 | 352×288 | 30 | 192 kbps | 240 kbps |
| 1.2 | 352×288 | 30 | 384 kbps | 480 kbps |
| 1.3 | 352×480 | 30 | 768 kbps | 960 kbps |
| 2 | 352×480 | 30 | 2 Mbps | 2.5 Mbps |
| 2.1 | 640×360 | 30 | 4 Mbps | 5 Mbps |
| 2.2 | 720×576 | 30 | 4 Mbps | 5 Mbps |
| 3 | 720×480 | 30 | 10 Mbps | 12.5 Mbps |
| 3.1 | 1280×720 | 30 | 14 Mbps | 17.5 Mbps |
| 3.2 | 1280×720 | 60 | 14 Mbps | 17.5 Mbps |
| 4 | 1920×1080 | 30 | 20 Mbps | 25 Mbps |
| 4.1 | 1920×1080 | 30 | 20 Mbps | 25 Mbps |
| 4.2 | 1920×1080 | 60 | 50 Mbps | 62.5 Mbps |
| 5 | 2048×1024 | 72 | 40 Mbps | 50 Mbps |
| 5.1 | 4096×2048 | 72 | 135 Mbps | 168 Mbps |
| 5.2 | 4096×2048 | 120 | 240 Mbps | 300 Mbps |
kotlin
// 设置 Level(自动降级,超出设备能力则自动调整)
videoFormat.setInteger(
MediaFormat.KEY_LEVEL,
MediaCodecInfo.CodecProfileLevel.AVCLevel4
)
// ===== 智能设置 Profile + Level =====
fun selectBestProfileLevel(
encoder: MediaCodec,
mime: String,
width: Int,
height: Int,
bitrate: Int,
frameRate: Int
): Pair<Int, Int> {
val caps = encoder.codecCapabilities
val supportedProfiles = caps.profileLevels
// 选择最高质量且支持的 Profile
val profile = when {
supportedProfiles.any { it.profile == MediaCodecInfo.CodecProfileLevel.AVCProfileHigh } ->
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh
supportedProfiles.any { it.profile == MediaCodecInfo.CodecProfileLevel.AVCProfileMain } ->
MediaCodecInfo.CodecProfileLevel.AVCProfileMain
else -> MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline
}
// 选择满足当前分辨率的最高 Level
val level = supportedProfiles
.filter { it.profile == profile }
.maxByOrNull {
// 选择 MaxLumaSamples 最大的 Level
val maxSamples = getMaxSamplesForLevel(it.level)
maxSamples
}?.level ?: MediaCodecInfo.CodecProfileLevel.AVCLevel31
return Pair(profile, level)
}
private fun getMaxSamplesForLevel(level: Int): Int {
return when (level) {
MediaCodecInfo.CodecProfileLevel.AVCLevel1 -> 25344
MediaCodecInfo.CodecProfileLevel.AVCLevel1b -> 25344
MediaCodecInfo.CodecProfileLevel.AVCLevel11 -> 101376
MediaCodecInfo.CodecProfileLevel.AVCLevel12 -> 101376
MediaCodecInfo.CodecProfileLevel.AVCLevel13 -> 101376
MediaCodecInfo.CodecProfileLevel.AVCLevel2 -> 101376
MediaCodecInfo.CodecProfileLevel.AVCLevel21 -> 202752
MediaCodecInfo.CodecProfileLevel.AVCLevel22 -> 405504
MediaCodecInfo.CodecProfileLevel.AVCLevel3 -> 405504
MediaCodecInfo.CodecProfileLevel.AVCLevel31 -> 921600
MediaCodecInfo.CodecProfileLevel.AVCLevel32 -> 1310720
MediaCodecInfo.CodecProfileLevel.AVCLevel4 -> 2097152
MediaCodecInfo.CodecProfileLevel.AVCLevel41 -> 2228224
MediaCodecInfo.CodecProfileLevel.AVCLevel42 -> 2516582
MediaCodecInfo.CodecProfileLevel.AVCLevel5 -> 5652480
MediaCodecInfo.CodecProfileLevel.AVCLevel51 -> 10485760
MediaCodecInfo.CodecProfileLevel.AVCLevel52 -> 16777216
else -> 0
}
}
4.4.3 HEVC(H.265)Profile 与 Level
kotlin
// HEVC Profile
videoFormat.setInteger(
MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.HEVCProfileMain // Main / Main10 / Main10HDR10
)
// HEVC Level
videoFormat.setInteger(
MediaFormat.KEY_LEVEL,
MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel31
)
| HEVC Profile | 色深 | HDR | 用途 |
|---|---|---|---|
| Main | 8 bit | ❌ | 通用 |
| Main10 | 10 bit | ✅ | HDR10 |
| Main10 HDR10 | 10 bit | ✅ + Static | HDR10 |
| Main10 HDR10Plus | 10 bit | ✅ + Dynamic | HDR10+ |
4.5 高级可选参数
4.5.1 KEY_MAX_INPUT_SIZE:最大输入缓冲
kotlin
// 建议值:分辨率 × 1.5(I420 格式大小)
videoFormat.setLong(
MediaFormat.KEY_MAX_INPUT_SIZE,
(width * height * 3 / 2).toLong()
)
// 或设置为 0(编码器自动分配)
videoFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, 0)
// 最小值约束
videoFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, 50_000)
⚠️ 若输入数据超过此大小,
queueInputBuffer()会抛出MediaCodec.CODE_ERROR_MAX_INPUT_SIZE_EXCEEDED。设为 0 可让编码器自动选择,但可能导致初始延迟。
4.5.2 KEY_COMPLEXITY:编码复杂度(速度/质量权衡)
kotlin
// 0(最快/最低质量)←→ 10 或更高(最慢/最高质量)
videoFormat.setInteger(MediaFormat.KEY_COMPLEXITY, 2) // 默认中等
// 编码器能力查询
val encoderCaps = encoder.codecCapabilities
val minComplexity = encoderCaps encoderCapabilities.minimumSupportedInstances
val maxComplexity = encoderCaps.getEncoderCapabilities().complexityRange.high
Log.d("Complexity", "支持范围: ${minComplexity}--$maxComplexity")
| 复杂度值 | 编码速度 | 质量 | 功耗 | 适用场景 |
|---|---|---|---|---|
| 0--1 | 极快 | 低 | 低 | 实时通信、预览 |
| 2--4 | 快 | 中 | 中 | 移动端录制 |
| 5--7 | 中等 | 高 | 高 | 标准录制 |
| 8--10 | 慢 | 最高 | 最高 | 离线高质量压缩 |
4.5.3 KEY_QUALITY:质量级别(CQ 模式下的 QP 控制)
kotlin
// 仅在 BITRATE_MODE_CQ 下有效
// 值范围通常为 0--100(值越高质量越好)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
videoFormat.setInteger(MediaFormat.KEY_QUALITY, 80) // 80/100
}
4.5.4 KEY_PREPEND_HEADER_TO_SYNC_FRAMES:关键帧头部注入
kotlin
// API 23+:在每个同步帧(I 帧)前自动添加 SPS/PPS NAL 单元
// 推荐开启,确保每个 I 帧独立可解码
videoFormat.setInteger(
MediaFormat.KEY_PREPEND_HEADER_TO_SYNC_FRAMES,
1 // 1 = 开启,0 = 关闭
)
4.5.5 KEY_REPEAT_PREVIOUS_FRAME_AFTER:重复帧(慢动作)
kotlin
// 用于慢动作编码(120fps 拍摄 → 30fps 输出)
// 指定帧显示时长(微秒)
// 120fps 拍摄 → 每帧 8333us → 设为 3×8333 = 25000us → 输出 30fps
videoFormat.setLong(
MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER,
25_000 // 25 ms = 帧率变为 40fps
)
4.5.6 KEY_INTRA_REFRESH_PERIOD:帧内刷新(API 23+)
帧内刷新是替代周期性 I 帧的低开销方案,在 RTC 场景中极为重要:
kotlin
// 设置帧内刷新周期(毫秒)
// 设备内部会在每个周期内逐步刷新一行宏块,而非发送完整 I 帧
videoFormat.setLong(
MediaFormat.KEY_INTRA_REFRESH_PERIOD,
1_000 // 每秒刷新一次(等于 GOP 长度)
)
帧内刷新原理:
传统 I 帧(每 30 帧一个 I 帧):
I[0] P[1] P[2] ... P[29] I[30] ...
↑ 大小 ~300KB ↑ 再次 ~300KB(网络抖动!)
帧内刷新(每帧刷新一行宏块,替代 I 帧):
I_row[0] P[1] ... P[14] I_row[15] P[16] ... I_row[29] I_row[30] ...
↑ ~20KB 逐步恢复 ↑ 每行 ~10KB(无大 I 帧!)
抗丢包能力大幅提升
4.5.7 KEY_TEMPORAL_LAYERING:时域分层(API 31+)
kotlin
// Android 12+(API 31):分层编码(Temporal Scalability)
// 将帧分为多个时域层:T0(关键帧)→ T1 → T2 → ...
// 低层丢失时,高层可选择性丢弃而不影响基础层
// 设置时域分层方案
videoFormat.setInteger(MediaFormat.KEY_TEMPORAL_LAYERING,
VideoEncoderFeatures.TEMPORAL_LAYERING_BTBL_HEVC) // HEVC
// 或
videoFormat.setInteger(MediaFormat.KEY_TEMPORAL_LAYERING,
VideoEncoderFeatures.TEMPORAL_LAYERING_BTBL_H264) // H.264
时域分层效果:
T0 (基础层): I I I I I I I I ← 最重要,始终保留
T1 (增强层): P P P P P P P ← 丢包首选丢弃
T2 (增强层): B B B B B B ← 进一步丢弃
总帧数:I P B P B P B P B P B P B P B P ...
↑ I (T0)
↑ T1 P
↑ T2 B
↑ T1 P ...
4.5.8 HDR 元数据(API 31+)
kotlin
// Android 12+:HDR10 编码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// HDRStaticInfo(CTA-861.3)
val hdrStaticInfo = android.media.HdrStaticInfo().apply {
// Primaries(色域原色)
primaries_x = intArrayOf(34000, 16000, 8500, 0, 0, 0)
primaries_y = intArrayToIntArray(intArrayOf(32000, 30000, 39850, 0, 0, 0))
// 白点
white_point_x = 15635
white_point_y = 16450
// 最大/最小亮度
max_luminance = 1_000_000 // 1000 nit
min_luminance = 1_000 // 0.1 nit
}
videoFormat.setFeature(
MediaFormat.FEATURE_HdrEncoding,
true
)
// 注意:HEVC Main10 Profile 需单独设置
}
4.6 码率范围控制(Android 12+,API 31)
kotlin
// Android 12+:设置码率范围(VBR 模式)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
videoFormat.setInteger(
"max-bitrate",
12_000_000 // VBR 模式下的最大码率
)
videoFormat.setInteger(
"typical-bitrate",
8_000_000 // 典型码率
)
}
// 从 VideoCapabilities 读取支持的码率范围
val videoCaps = encoder.videoCapabilities
val bitrateRange = videoCaps.bitrateRange
Log.d("BitrateRange", "支持码率: ${bitrateRange.lower} -- ${bitrateRange.upper} bps")
val frameRateRange = videoCaps.supportedFrameRates
Log.d("FrameRateRange", "支持帧率: ${frameRateRange.lower} -- ${frameRateRange.upper}")
val widthRange = videoCaps.supportedWidths
Log.d("WidthRange", "支持宽度: ${widthRange.lower} -- ${widthRange.upper}")
val heightRange = videoCaps.supportedHeights
Log.d("HeightRange", "支持高度: ${heightRange.lower} -- ${heightRange.upper}")
五、音频编码参数详解
5.1 音频格式全景
| MIME 类型 | 标准名称 | minSdk | 码率范围 | 声道 | 典型用途 |
|---|---|---|---|---|---|
audio/mp4a-latm |
AAC-LC | API 21+ | 64--320 kbps | 单/双 | 通用音频 |
audio/he-aac |
HE-AAC v1 | API 21+ | 32--128 kbps | 单/双 | 音乐流媒体 |
audio/he-aac-v2 |
HE-AAC v2 | API 21+ | 16--64 kbps | 单 | 超低码率 |
audio/opus |
Opus | API 21+ | 6--510 kbps | 单/双/多 | 实时通信 |
audio/amr-wb |
AMR-WB | API 21+ | 6.6--23.85 kbps | 单 | 语音 |
audio/amr-nb |
AMR-NB | API 21+ | 4.75--12.2 kbps | 单 | 语音(已淘汰) |
audio/ac3 |
AC3 | API 24+ | 192--640 kbps | 多 | 杜比环绕 |
audio/eac3 |
E-AC3(Dolby Digital+) | API 24+ | 256--1510 kbps | 多 | 杜比全景声 |
5.2 AAC 编码参数
kotlin
// ===== AAC-LC 标准配置 =====
val aacFormat = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
48_000, // 采样率 48 kHz
2 // 双声道
).apply {
setInteger(MediaFormat.KEY_BIT_RATE, 128_000) // 128 kbps(AAC-LC 推荐)
setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC)
setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 4096) // 最大输入缓冲
}
// ===== HE-AAC v1(高效率,用于流媒体)=====
val heAacFormat = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
48_000,
2
).apply {
setInteger(MediaFormat.KEY_BIT_RATE, 64_000) // 64 kbps(HE-AAC 推荐)
setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectHE)
}
// ===== HE-AAC v2(超低码率)=====
val heAacV2Format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
48_000,
2
).apply {
setInteger(MediaFormat.KEY_BIT_RATE, 32_000) // 32 kbps
setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectHEv2)
// ⚠️ HE-AACv2 仅支持单声道输出
}
5.3 AAC Profile 详细对比
| AAC Profile | Object Type ID | 效率 | 音质 | 最低码率 | 适用场景 |
|---|---|---|---|---|---|
| AAC-LC (Low Complexity) | 2 | 中 | 高 | 128 kbps (44kHz 立体声) | 通用(最常用) |
| HE-AAC v1 | 5 (SBR) | 高 | 中高 | 64 kbps (44kHz 立体声) | 流媒体音乐 |
| HE-AAC v2 | 29 (PS+SBR) | 极高 | 中 | 32 kbps (44kHz 立体声) | 超低码率语音 |
| AAC-LD (Low Delay) | 29 | --- | --- | --- | 实时通信 |
| AAC-ELD (Enhanced LD) | 39 | --- | --- | --- | VoIP |
SBR(Spectral Band Replication):将音频分为低频(基础)+ 高频(参数),低频全编码,高频用少量参数重建,节省约 50% 码率。
PS(Parametric Stereo):将立体声编码为单声道 + 空间参数,进一步节省约 50% 码率。
5.4 Opus 编码参数
kotlin
// ===== Opus 音频编码(Android 21+,实时通信最佳选择)=====
val opusFormat = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_OPUS,
48_000, // Opus 必须 48 kHz
2 // 声道数(Opus 支持 1--8 声道)
).apply {
// Opus 无需额外参数,码率由编码器内部控制
setInteger(MediaFormat.KEY_BIT_RATE, 64_000) // 推荐 64 kbps(高质量语音)
setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 5760) // 120ms × 48kHz × 2ch × 2B
}
// Opus 推荐码率表:
// 6 kbps :窄带语音(电话质量)
// 16 kbps :宽带语音(会议质量)
// 32 kbps :高质量语音
// 48 kbps :音乐质量(立体声)
// 64 kbps :高质量音乐
// 128 kbps:接近 CD 质量
5.5 PCM 编码参数
kotlin
// PCM 16bit 线性编码(最通用)
val pcmFormat = MediaFormat.createAudioFormat(
"audio/raw", // 注意:非标准 MIME
44_100,
2
).apply {
setInteger(MediaFormat.KEY_PCM_ENCODING,
AudioFormat.ENCODING_PCM_16BIT)
}
5.6 音频参数速查表
| 参数 | 视频 KEY | 音频 KEY | 类型 | 典型值 |
|---|---|---|---|---|
| 采样率 | --- | KEY_SAMPLE_RATE |
Int | 44100 / 48000 |
| 声道数 | --- | KEY_CHANNEL_COUNT |
Int | 1 / 2 / 6 |
| 码率 | KEY_BIT_RATE |
KEY_BIT_RATE |
Int | 视频: 1--20 Mbps; 音频: 64--320 kbps |
| Profile | KEY_PROFILE |
KEY_AAC_PROFILE |
Int | AVCProfileHigh / AACObjectLC |
| 最大输入 | KEY_MAX_INPUT_SIZE |
KEY_MAX_INPUT_SIZE |
Long | 视频: W×H×1.5; 音频: 4096 |
| PCM 编码 | --- | KEY_PCM_ENCODING |
Int | ENCODING_PCM_16BIT |
六、编码器能力查询:设备适配的核心
6.1 完整能力查询工具类
kotlin
/**
* MediaCodecCapability:编码器能力查询工具
* 在配置编码器前,先用此工具验证设备是否支持目标参数
*/
object MediaCodecCapability {
data class EncoderInfo(
val name: String,
val mime: String,
val profileLevels: List<Pair<Int, Int>>,
val colorFormats: List<Int>,
val maxInstances: Int,
val bitrateRange: IntRange,
val widthRange: IntRange,
val heightRange: IntRange,
val frameRateRange: IntRange
)
/**
* 查询指定 MIME 类型的最佳编码器
*/
fun findBestEncoder(mime: String): EncoderInfo? {
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
for (info in codecList.codecInfos) {
if (info.isEncoder && info.supportedTypes.contains(mime)) {
return getEncoderInfo(info, mime)
}
}
return null
}
/**
* 查询所有可用编码器
*/
fun findAllEncoders(): List<EncoderInfo> {
val result = mutableListOf<EncoderInfo>()
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
for (info in codecList.codecInfos) {
if (info.isEncoder) {
for (mime in info.supportedTypes) {
result.add(getEncoderInfo(info, mime))
}
}
}
return result
}
private fun getEncoderInfo(info: MediaCodecInfo, mime: String): EncoderInfo {
val caps = info.getCapabilitiesForType(mime)
val profileLevels = caps.profileLevels.map {
Pair(it.profile, it.level)
}
val (bitrateRange, widthRange, heightRange, frameRateRange) = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
mime.startsWith("video/") -> {
val vc = caps.videoCapabilities
IntRange(vc.bitrateRange.lower, vc.bitrateRange.upper) to
IntRange(vc.supportedWidths.lower, vc.supportedWidths.upper) to
IntRange(vc.supportedHeights.lower, vc.supportedHeights.upper) to
IntRange(
(vc.supportedFrameRates.lower * 1000).toInt(),
(vc.supportedFrameRates.upper * 1000).toInt()
)
}
else -> {
IntRange(0, Int.MAX_VALUE) to
IntRange(0, Int.MAX_VALUE) to
IntRange(0, Int.MAX_VALUE) to
IntRange(0, Int.MAX_VALUE)
}
}
return EncoderInfo(
name = info.name,
mime = mime,
profileLevels = profileLevels,
colorFormats = caps.colorFormats.toList(),
maxInstances = caps.maxSupportedInstances,
bitrateRange = bitrateRange,
widthRange = widthRange,
heightRange = heightRange,
frameRateRange = frameRateRange
)
}
/**
* 检查目标配置是否在编码器能力范围内
*/
fun isConfigurationSupported(
encoder: EncoderInfo,
width: Int,
height: Int,
bitrate: Int,
frameRate: Int
): Boolean {
return width in encoder.widthRange &&
height in encoder.heightRange &&
bitrate in encoder.bitrateRange &&
frameRate * 1000 in encoder.frameRateRange
}
/**
* 自动调整到支持的配置
*/
fun autoAdjust(
targetWidth: Int,
targetHeight: Int,
targetBitrate: Int,
targetFrameRate: Int,
encoder: EncoderInfo
): AdjustedConfig {
val adjustedWidth = adjustToRange(targetWidth, encoder.widthRange, 16)
val adjustedHeight = adjustToRange(targetHeight, encoder.heightRange, 16)
val adjustedBitrate = adjustToRange(targetBitrate, encoder.bitrateRange, 1000)
val adjustedFrameRate = adjustToRange(
targetFrameRate * 1000,
encoder.frameRateRange,
1000
) / 1000
return AdjustedConfig(
adjustedWidth, adjustedHeight,
adjustedBitrate, adjustedFrameRate
)
}
private fun adjustToRange(value: Int, range: IntRange, alignment: Int): Int {
val aligned = (value / alignment) * alignment
return aligned.coerceIn(range)
}
data class AdjustedConfig(
val width: Int,
val height: Int,
val bitrate: Int,
val frameRate: Int
)
}
6.2 使用示例
kotlin
// 查询 AVC 编码器能力
val avcEncoder = MediaCodecCapability.findBestEncoder("video/avc")
avcEncoder?.let {
Log.d("Encoder", "编码器: ${it.name}")
Log.d("Encoder", "分辨率范围: ${it.widthRange} × ${it.heightRange}")
Log.d("Encoder", "码率范围: ${it.bitrateRange}")
Log.d("Encoder", "帧率范围: ${it.frameRateRange}")
// 尝试配置 4K@30fps 编码
val targetConfig = MediaCodecCapability.AdjustedConfig(
3840, 2160, 30_000_000, 30
)
if (!MediaCodecCapability.isConfigurationSupported(
it, 3840, 2160, 30_000_000, 30)) {
// 自动调整
val adjusted = MediaCodecCapability.autoAdjust(
3840, 2160, 30_000_000, 30, it
)
Log.w("Encoder", "4K 不支持,自动降级为: "
+ "${adjusted.width}×${adjusted.height} @ ${adjusted.frameRate}fps, "
+ "${adjusted.bitrate / 1_000_000}Mbps")
}
}
七、完整编码流程与 Demo
7.1 Surface 模式(推荐,性能最优)
kotlin
/**
* SurfaceVideoEncoder:Surface 模式的视频编码器
* 特点:
* 1. 无需手动处理 YUV 数据
* 2. 数据直接从 Surface 传入编码器(零拷贝)
* 3. 适合 Camera 实时编码、屏幕录制
* 4. 性能最优,功耗最低
*/
class SurfaceVideoEncoder(
private val context: Context,
private val width: Int,
private val height: Int,
private val bitrate: Int,
private val frameRate: Int,
private val outputPath: String
) {
private var encoder: MediaCodec? = null
private var muxer: MediaMuxer? = null
private var videoTrackIndex = -1
private var isMuxerStarted = false
private var isRunning = false
private var encoderThread: Thread? = null
private var inputSurface: Surface? = null
// ===== 步骤 1:创建编码器 =====
fun prepare(): Boolean {
val mime = MediaFormat.MIMETYPE_VIDEO_AVC
// 创建编码器
encoder = MediaCodec.createEncoderByType(mime)
// ===== 步骤 2:构建 MediaFormat =====
val format = MediaFormat.createVideoFormat(mime, width, height).apply {
setInteger(KEY_BIT_RATE, bitrate)
setInteger(KEY_FRAME_RATE, frameRate)
setInteger(KEY_I_FRAME_INTERVAL, 1) // 每秒 1 个 I 帧
setInteger(KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(KEY_BITRATE_MODE, MediaFormat.BITRATE_MODE_CBR)
// Profile / Level(自动选择)
setInteger(KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh)
setInteger(KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel41)
// 最大输入缓冲(自动)
setLong(KEY_MAX_INPUT_SIZE, 0)
// I 帧前添加 SPS/PPS
setInteger(KEY_PREPEND_HEADER_TO_SYNC_FRAMES, 1)
}
// ===== 步骤 3:配置编码器 =====
encoder!!.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// ===== 步骤 4:获取输入 Surface =====
// Surface 创建后,可以直接向其绘制(Canvas/Camera/EglCore)
inputSurface = encoder!!.createInputSurface()
encoder!!.start()
// ===== 步骤 5:创建 MediaMuxer =====
muxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
isRunning = true
// ===== 步骤 6:启动编码线程 =====
encoderThread = Thread { encodeLoop() }
encoderThread?.start()
return true
}
/**
* 获取输入 Surface,供 CameraX / Canvas / OpenGL ES 使用
*/
fun getInputSurface(): Surface? = inputSurface
// ===== 编码输出循环 =====
private fun encodeLoop() {
val bufferInfo = MediaCodec.BufferInfo()
val frameIntervalUs = 1_000_000L / frameRate
while (isRunning) {
// 取输出缓冲
val outputBufIndex = encoder!!.dequeueOutputBuffer(bufferInfo, 10_000)
when {
outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
// 无可用缓冲,继续等待
}
outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
// 编码器格式改变(第一个 buffer 时触发)
val newFormat = encoder!!.outputFormat
Log.d("Encoder", "输出格式: $newFormat")
// 添加视频轨道
videoTrackIndex = muxer!!.addTrack(newFormat)
muxer!!.start()
isMuxerStarted = true
}
outputBufIndex >= 0 -> {
val encodedData = encoder!!.getOutputBuffer(outputBufIndex)!!
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG == 0) {
// 普通编码帧
if (isMuxerStarted && bufferInfo.size > 0) {
synchronized(muxer!!) {
muxer!!.writeSampleData(videoTrackIndex, encodedData, bufferInfo)
}
}
}
encoder!!.releaseOutputBuffer(outputBufIndex, false)
}
}
}
}
/**
* 发送帧到 Surface(Camera 实时编码时调用)
* 时间戳单位:微秒(microseconds)
*/
fun frameAvailable(timestampUs: Long) {
// Surface 由 Camera 或 Canvas 绘制,无需手动处理
// Camera 自动处理 buffer 交换
}
/**
* 停止编码
*/
fun stop() {
isRunning = false
// 发送 EOS 信号
encoderThread?.join(2000)
encoderThread = null
try {
encoder?.stop()
encoder?.release()
encoder = null
muxer?.stop()
muxer?.release()
muxer = null
inputSurface?.release()
inputSurface = null
} catch (e: Exception) {
Log.e("Encoder", "释放失败", e)
}
}
}
7.2 ByteBuffer 模式(手动 YUV 填充)
kotlin
/**
* ByteBufferVideoEncoder:ByteBuffer 模式的视频编码器
* 适用于:非 Camera 源(自定义图像生成、测试)
*/
class ByteBufferVideoEncoder(
private val context: Context,
private val width: Int,
private val height: Int,
private val bitrate: Int,
private val frameRate: Int,
private val outputPath: String
) {
private var encoder: MediaCodec? = null
private var muxer: MediaMuxer? = null
private var videoTrackIndex = -1
private var isMuxerStarted = false
private var isRunning = false
private var encodeThread: Thread? = null
private var presentationTimeUs = 0L
private val frameIntervalUs = 1_000_000L / 30
private val yuvBuffer = ByteArray(width * height * 3 / 2)
fun prepare(): Boolean {
val mime = MediaFormat.MIMETYPE_VIDEO_AVC
encoder = MediaCodec.createEncoderByType(mime)
// ===== 查询可用色彩格式 =====
val caps = encoder!!.getCapabilitiesForType(mime)
val colorFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// API 30+:优先使用 YUV420Flexible
if (caps.isFormatSupported(MediaFormat.createVideoFormat(mime, width, height)
.also { it.setInteger(KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) }
)) {
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
} else if (caps.isFormatSupported(MediaFormat.createVideoFormat(mime, width, height)
.also { it.setInteger(KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) }
)) {
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
} else {
caps.colorFormats.firstOrNull { isYuvFormat(it) }
?: MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
}
} else {
// API 21-29:手动选择
if (caps.colorFormats.contains(
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar)) {
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
} else {
caps.colorFormats.firstOrNull { isYuvFormat(it) }
?: MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
}
}
Log.d("Encoder", "选择的色彩格式: 0x${colorFormat.toString(16)}")
val format = MediaFormat.createVideoFormat(mime, width, height).apply {
setInteger(KEY_BIT_RATE, bitrate)
setInteger(KEY_FRAME_RATE, frameRate)
setInteger(KEY_I_FRAME_INTERVAL, 1)
setInteger(KEY_COLOR_FORMAT, colorFormat)
setInteger(KEY_BITRATE_MODE, MediaFormat.BITRATE_MODE_CBR)
setInteger(KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh)
setInteger(KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel41)
setLong(KEY_MAX_INPUT_SIZE, (width * height * 3 / 2).toLong())
setInteger(KEY_PREPEND_HEADER_TO_SYNC_FRAMES, 1)
}
encoder!!.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
encoder!!.start()
muxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
isRunning = true
encodeThread = Thread { encodeLoop() }
encodeThread?.start()
return true
}
private fun isYuvFormat(cf: Int): Boolean {
return cf in listOf(
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
)
}
// ===== ByteBuffer 模式编码循环 =====
private fun encodeLoop() {
val bufferInfo = MediaCodec.BufferInfo()
var inputDone = false
while (isRunning) {
// A. 填充输入缓冲
if (!inputDone) {
val inputBufIndex = encoder!!.dequeueInputBuffer(10_000)
if (inputBufIndex >= 0) {
// ===== 步骤:获取 YUV 数据(这里用测试图案代替)=====
val inputBuf = encoder!!.getInputBuffer(inputBufIndex)!!
inputBuf.clear()
// 填充 YUV 数据(NV12 或 I420)
fillTestPattern(inputBuf, width, height, presentationTimeUs)
encoder!!.queueInputBuffer(
inputBufIndex,
0, // offset
yuvBuffer.size, // size
presentationTimeUs,
0 // flags
)
presentationTimeUs += frameIntervalUs
}
}
// B. 取输出缓冲
val outputBufIndex = encoder!!.dequeueOutputBuffer(bufferInfo, 10_000)
when {
outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
val newFormat = encoder!!.outputFormat
videoTrackIndex = muxer!!.addTrack(newFormat)
muxer!!.start()
isMuxerStarted = true
}
outputBufIndex >= 0 -> {
val outputBuf = encoder!!.getOutputBuffer(outputBufIndex)!!
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG == 0) {
if (isMuxerStarted && bufferInfo.size > 0) {
synchronized(muxer!!) {
muxer!!.writeSampleData(videoTrackIndex, outputBuf, bufferInfo)
}
}
}
encoder!!.releaseOutputBuffer(outputBufIndex, false)
}
}
}
// 处理剩余输出
drainEncoder()
}
/**
* 填充测试图案(彩色条纹)
*/
private fun fillTestPattern(buf: ByteBuffer, w: Int, h: Int, timeUs: Long) {
// NV12 格式:Y + UV 交错
// Y: w × h 字节
// UV: w × h/2 字节(U 和 V 交错)
Arrays.fill(yuvBuffer, 0.toByte())
// Y 分量:随帧变化(条纹动画)
val stripeIndex = ((timeUs / 100_000) % 6).toInt()
for (y in 0 until h) {
for (x in 0 until w) {
val value = if ((x / 64) % 2 == stripeIndex) 200 else 50
yuvBuffer[y * w + x] = value.toByte()
}
}
// UV 分量:固定灰色
for (y in 0 until h / 2) {
for (x in 0 until w / 2) {
yuvBuffer[w * h + y * w + x * 2] = 128 // U
yuvBuffer[w * h + y * w + x * 2 + 1] = 128 // V
}
}
buf.put(yuvBuffer)
}
private fun drainEncoder() {
val bufferInfo = MediaCodec.BufferInfo()
while (true) {
val outputBufIndex = encoder!!.dequeueOutputBuffer(bufferInfo, 10_000)
if (outputBufIndex >= 0) {
val outputBuf = encoder!!.getOutputBuffer(outputBufIndex)!!
if (bufferInfo.size > 0 && isMuxerStarted) {
synchronized(muxer!!) {
muxer!!.writeSampleData(videoTrackIndex, outputBuf, bufferInfo)
}
}
encoder!!.releaseOutputBuffer(outputBufIndex, false)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break
}
} else if (outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
break
}
}
}
/**
* 编码一帧图像(供外部调用)
*/
fun encodeFrame(argbData: IntArray) {
// RGB → NV12
rgbToNv12(argbData, yuvBuffer, width, height)
// 获取输入缓冲并写入
val inputBufIndex = encoder!!.dequeueInputBuffer(0)
if (inputBufIndex >= 0) {
val inputBuf = encoder!!.getInputBuffer(inputBufIndex)!!
inputBuf.clear()
inputBuf.put(yuvBuffer)
encoder!!.queueInputBuffer(
inputBufIndex, 0, yuvBuffer.size,
presentationTimeUs, 0
)
presentationTimeUs += frameIntervalUs
}
}
private fun rgbToNv12(rgb: IntArray, yuv: ByteArray, w: Int, h: Int) {
val frameSize = w * h
var yi = 0
var uvi = frameSize
for (j in 0 until h) {
for (i in 0 until w) {
val px = rgb[j * w + i]
val r = (px shr 16) and 0xFF
val g = (px shr 8) and 0xFF
val b = px and 0xFF
val y = ((66 * r + 129 * g + 25 * b + 128) shr 8) + 16
yuv[yi++] = y.coerceIn(0, 255).toByte()
if (j % 2 == 0 && i % 2 == 0) {
val u = ((-38 * r - 74 * g + 112 * b + 128) shr 8) + 128
val v = ((112 * r - 94 * g - 18 * b + 128) shr 8) + 128
yuv[uvi++] = u.coerceIn(0, 255).toByte()
yuv[uvi++] = v.coerceIn(0, 255).toByte()
}
}
}
}
fun stop() {
isRunning = false
encodeThread?.join(2000)
drainEncoder()
encoder?.stop()
encoder?.release()
encoder = null
muxer?.stop()
muxer?.release()
muxer = null
}
}
7.3 带动态码率调整的完整 Activity
kotlin
/**
* AdaptiveEncoderActivity:带动态码率调整的视频编码 Activity
* 演示:
* 1. 根据网络带宽自适应码率
* 2. 根据编码器负载自适应码率
* 3. 参数动态修改(码率/帧率/分辨率)
*/
class AdaptiveEncoderActivity : AppCompatActivity() {
private lateinit var encoder: SurfaceVideoEncoder
private lateinit var surfaceView: SurfaceView
private var canvas: Canvas? = null
// 编码参数
private var currentBitrate = 4_000_000 // 4 Mbps
private var currentFrameRate = 30
private var currentWidth = 1280
private var currentHeight = 720
// 帧计数器(用于 FPS 统计)
private var frameCount = 0
private var lastFpsTime = System.currentTimeMillis()
private var currentFps = 0f
private val handler = Handler(Looper.getMainLooper())
private val renderRunnable = object : Runnable {
override fun run() {
renderFrame()
handler.postDelayed(this, (1000 / currentFrameRate).toLong())
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
surfaceView = SurfaceView(this)
setContentView(surfaceView)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
initializeEncoder()
}
override fun surfaceChanged(holder: SurfaceHolder, f: Int, w: Int, h: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {
encoder.stop()
}
})
}
private fun initializeEncoder() {
val outputPath = "${cacheDir}/encoded_${System.currentTimeMillis()}.mp4"
encoder = SurfaceVideoEncoder(
this, currentWidth, currentHeight,
currentBitrate, currentFrameRate, outputPath
)
if (encoder.prepare()) {
// 将编码器输入 Surface 配置到 SurfaceView
encoder.getInputSurface()?.let { surface ->
// Canvas 绘制模式(也可对接 CameraX)
val sf = surface.lockCanvas(null)
sf.drawColor(Color.BLACK)
surface.unlockCanvasAndPost(sf)
}
handler.post(renderRunnable)
}
}
private fun renderFrame() {
val surface = encoder.getInputSurface() ?: return
val sf = surface.lockCanvas(null) ?: return
try {
// 绘制内容
sf.drawColor(Color.HSBtoRGB(
(System.currentTimeMillis() % 10000 / 10000f), 0.8f, 0.8f))
val paint = Paint().apply {
color = Color.WHITE
textSize = 40f
}
sf.drawText(
"${currentWidth}×${currentHeight} @ ${currentBitrate / 1_000_000}Mbps FPS:${currentFps.toInt()}",
20f, 60f, paint)
// 帧计数
frameCount++
val now = System.currentTimeMillis()
if (now - lastFpsTime >= 1000) {
currentFps = frameCount * 1000f / (now - lastFpsTime)
frameCount = 0
lastFpsTime = now
// 每秒评估是否需要调整码率
evaluateBitrateAdaptation()
}
} finally {
surface.unlockCanvasAndPost(sf)
}
}
/**
* 动态码率调整策略
* 实际项目中可接入网络带宽探测(SpeedTest)结果
*/
private fun evaluateBitrateAdaptation() {
// 简单策略:FPS 低于目标 80% → 降低码率
val targetFps = currentFrameRate
val fpsRatio = currentFps / targetFps
when {
fpsRatio < 0.7 && currentBitrate > 1_000_000 -> {
// 编码器跟不上,降低码率和分辨率
adjustEncoding(bitrate = (currentBitrate * 0.7).toInt(),
width = (currentWidth * 0.75).toInt() and 0xFFFFFE,
height = (currentHeight * 0.75).toInt() and 0xFFFFFE)
Log.d("Encoder", "码率下降: ${currentBitrate / 1_000_000}Mbps "
+ "→ ${(currentBitrate * 0.7 / 1_000_000).toInt()}Mbps")
}
fpsRatio > 0.95 && currentFps > targetFps * 0.95 -> {
// 编码器负载低,可提高质量
if (currentBitrate < 12_000_000) {
adjustEncoding(bitrate = (currentBitrate * 1.2).toInt())
Log.d("Encoder", "码率上升: ${currentBitrate / 1_000_000}Mbps")
}
}
}
}
private fun adjustEncoding(
bitrate: Int = currentBitrate,
frameRate: Int = currentFrameRate,
width: Int = currentWidth,
height: Int = currentHeight
) {
currentBitrate = bitrate
currentFrameRate = frameRate
currentWidth = width
currentHeight = height
// 重新配置编码器(需要在 Surface 模式下重建)
encoder.stop()
val outputPath = "${cacheDir}/encoded_${System.currentTimeMillis()}.mp4"
encoder = SurfaceVideoEncoder(this, width, height, bitrate, frameRate, outputPath)
encoder.prepare()
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(renderRunnable)
encoder.stop()
}
}
八、MediaMuxer:编码输出封装
8.1 MediaMuxer 参数与轨道添加
kotlin
// ===== 创建 MediaMuxer =====
val muxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// 输出格式:
// MUXER_OUTPUT_MPEG_4:MP4(默认,最通用)
// MUXER_OUTPUT_WEBM:WebM(VP8/VP9/Opus)
// MUXER_OUTPUT_3GPP:3GP(低码率手机视频)
// ===== 添加视频轨道 =====
val videoFormat = encoder.outputFormat // 从编码器获取实际格式
val videoTrackIndex = muxer.addTrack(videoFormat)
// ===== 添加音频轨道 =====
val audioFormat = audioEncoder.outputFormat
val audioTrackIndex = muxer.addTrack(audioFormat)
// ===== 启动 =====
muxer.start()
// ===== 写入数据(音视频分开写)=====
muxer.writeSampleData(videoTrackIndex, videoBuf, videoBufferInfo)
muxer.writeSampleData(audioTrackIndex, audioBuf, audioBufferInfo)
// ===== 停止 =====
muxer.stop()
muxer.release()
8.2 音视频同步写入
kotlin
// 音视频同步的核心:确保 DTS/PTS 顺序正确
data class MuxerFrame(
val trackIndex: Int,
val buffer: ByteBuffer,
val info: MediaCodec.BufferInfo
)
class SyncMuxer(outputPath: String) {
private val muxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
private var isStarted = false
private var videoTrack = -1
private var audioTrack = -1
fun addTrack(mime: String, format: MediaFormat): Int {
val trackIdx = muxer.addTrack(format)
if (mime.startsWith("video/")) videoTrack = trackIdx
if (mime.startsWith("audio/")) audioTrack = trackIdx
return trackIdx
}
fun start() {
muxer.start()
isStarted = true
}
fun writeFrame(frame: MuxerFrame) {
if (!isStarted) return
synchronized(muxer) {
muxer.writeSampleData(frame.trackIndex, frame.buffer, frame.info)
}
}
// 按 PTS 排序后写入(确保解码顺序正确)
private val pendingFrames = PriorityQueue(compareBy<MuxerFrame> {
it.info.presentationTimeUs
})
fun writeFrameSorted(frame: MuxerFrame) {
pendingFrames.add(frame)
while (pendingFrames.isNotEmpty() &&
pendingFrames.peek().info.presentationTimeUs <= getMinPts()) {
val oldest = pendingFrames.poll()
writeFrame(oldest)
}
}
private var minVideoPts = Long.MAX_VALUE
private var minAudioPts = Long.MAX_VALUE
private fun getMinPts(): Long {
return minOf(minVideoPts, minAudioPts)
}
}
九、常见问题与解决方案
9.1 问题诊断树
编码器初始化失败(createEncoderByType 返回 null)
│
├─ 原因 1:设备不支持该 MIME 类型
│ └─ 解决:使用 MediaCodecCapability.findAllEncoders() 查询可用编码器
│
├─ 原因 2:设备被其他应用占用(编码器独占)
│ └─ 解决:关闭其他相机/录制应用,重试
│
└─ 原因 3:编码器实例数超限
└─ 解决:检查 caps.maxSupportedInstances,等待资源释放
9.2 编码质量差
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 块效应明显 | 码率过低 | 提高 BIT_RATE 或切换 CQ 模式 |
| 马赛克/花屏 | 码率模式不匹配 | 运动场景用 VBR/CBR |
| I 帧卡顿 | I 帧间隔太长或 I 帧过大 | 减小 I_FRAME_INTERVAL,增加码率 |
| 色彩失真 | 色彩格式不匹配 | 确认编码器支持的 COLOR_FORMAT |
| 画面模糊 | 分辨率与码率不匹配 | 4K 需要 ≥ 25 Mbps |
9.3 编码器参数冲突
kotlin
// 常见错误 1:码率超出编码器能力
val caps = encoder.videoCapabilities
val clampedBitrate = caps.bitrateRange.clamp(bitrate)
videoFormat.setInteger(KEY_BIT_RATE, clampedBitrate)
// 常见错误 2:分辨率不对齐(宏块要求 16 对齐)
val alignedW = (width / 16) * 16
val alignedH = (height / 16) * 16
videoFormat.setInteger(KEY_WIDTH, alignedW)
videoFormat.setInteger(KEY_HEIGHT, alignedH)
// 常见错误 3:Surface 已销毁后仍尝试操作
inputSurface?.let { surf ->
if (surf.isValid) {
// 安全操作
}
}
9.4 内存溢出(OOM)
kotlin
// InputBuffer 大小过大 → OOM
// 解决:设置合理的 KEY_MAX_INPUT_SIZE
val safeInputSize = minOf(
width * height * 3 / 2, // I420 大小
2 * 1024 * 1024 // 最大 2MB
)
videoFormat.setLong(KEY_MAX_INPUT_SIZE, safeInputSize.toLong())
// ByteBuffer 模式下避免重复分配
private val reusableBuffer = ByteArrayBuffer(width * height * 3 / 2)
十、总结:参数速查总表
10.1 视频编码参数速查
| 参数 | KEY 常量 | 类型 | 必需 | 典型值 | 备注 |
|---|---|---|---|---|---|
| MIME | --- | String | ✅ | video/avc |
决定编解码格式 |
| 宽度 | KEY_WIDTH |
Int | ✅ | 1920 | 需 16 的倍数 |
| 高度 | KEY_HEIGHT |
Int | ✅ | 1080 | 需 16 的倍数 |
| 码率 | KEY_BIT_RATE |
Int | ✅ | 5--15 Mbps | 场景决定 |
| 帧率 | KEY_FRAME_RATE |
Int | ✅ | 30 | 建议值,非强制 |
| I 帧间隔 | KEY_I_FRAME_INTERVAL |
Int | ✅ | 1--5 | 秒为单位 |
| 色彩格式 | KEY_COLOR_FORMAT |
Int | Surface 模式✅ | COLOR_FormatSurface |
零拷贝优先 |
| Profile | KEY_PROFILE |
Int | 推荐 | AVCProfileHigh |
兼容性/质量 |
| Level | KEY_LEVEL |
Int | 自动 | AVCLevel4 |
设备自动降级 |
| 码率模式 | KEY_BITRATE_MODE |
Int | 推荐 | BITRATE_MODE_CBR |
API 29+ |
| 最大输入 | KEY_MAX_INPUT_SIZE |
Long | 推荐 | W×H×1.5 | 防止 OOM |
| 预处理头部 | KEY_PREPEND_HEADER_TO_SYNC_FRAMES |
Int | 推荐 | 1 | API 23+ |
| 复杂度 | KEY_COMPLEXITY |
Int | 可选 | 2--4 | 速度/质量权衡 |
| 帧内刷新 | KEY_INTRA_REFRESH_PERIOD |
Long | RTC 推荐 | 帧率×2 | API 23+ |
| 时域分层 | KEY_TEMPORAL_LAYERING |
Int | 流媒体推荐 | BTBL_* |
API 31+ |
| 重复帧 | KEY_REPEAT_PREVIOUS_FRAME_AFTER |
Long | 慢动作 | N×帧长 | 120fps 拍摄 |
| HDR 编码 | FEATURE_HdrEncoding |
Boolean | HDR 录制 | true | API 31+ |
10.2 音频编码参数速查
| 参数 | KEY 常量 | 类型 | 必需 | 典型值 |
|---|---|---|---|---|
| MIME | --- | String | ✅ | audio/mp4a-latm |
| 采样率 | KEY_SAMPLE_RATE |
Int | ✅ | 44100 / 48000 |
| 声道数 | KEY_CHANNEL_COUNT |
Int | ✅ | 1 / 2 |
| 码率 | KEY_BIT_RATE |
Int | ✅ | 64--320 kbps |
| AAC Profile | KEY_AAC_PROFILE |
Int | AAC 必需 | AACObjectLC |
| 最大输入 | KEY_MAX_INPUT_SIZE |
Int | 推荐 | 4096 |
| PCM 编码 | KEY_PCM_ENCODING |
Int | PCM 必需 | PCM_16BIT |
10.3 推荐配置场景指南
| 场景 | 编码器 | 分辨率 | 码率 | 帧率 | 码率模式 | I 帧 |
|---|---|---|---|---|---|---|
| 4K 电影录制 | H.264 High | 3840×2160 | 40--60 Mbps | 30 | CQ | 2s |
| 1080p 日常录制 | H.264 High | 1920×1080 | 12--20 Mbps | 30 | VBR | 2s |
| 720p 直播推流 | H.264 High | 1280×720 | 4--8 Mbps | 30 | CBR | 1s |
| 1080p 视频通话 | H.264 Baseline | 1920×1080 | 2--4 Mbps | 30 | CBR | 1s |
| 720p 视频通话 | H.264 Baseline | 1280×720 | 1.5--3 Mbps | 30 | CBR | 0.5s |
| 屏幕录制 | H.264 High | 屏幕分辨率 | 8--20 Mbps | 屏幕帧率 | CQ | 5s |
| HEVC 高效录制 | HEVC Main | 1920×1080 | 4--8 Mbps | 30 | VBR | 2s |
| 语音通话 | Opus | 48kHz | 32 kbps | --- | --- | --- |
| 高品质音频 | AAC-LC | 48kHz | 256 kbps | --- | --- | --- |
十一、参考文献
| # | 文档 | 出处 |
|---|---|---|
| 1 | **MediaCodec | Android Developers** |
| 2 | **MediaFormat | Android Developers** |
| 3 | VideoEncoderFeatures | Android Developers |
| 4 | H.264/AVC标准 | ITU-T Rec. H.264 |
| 5 | H.265/HEVC标准 | ITU-T Rec. H.265 |
| 6 | MediaCodecInfo.CodecCapabilities | Android Developers |
| 7 | Android 媒体性能优化指南 | Android Developers |
| 8 | MediaMuxer | Android Developers |