本文讲H.264 硬编码实现(MediaCodec)配合 Camera 采集 NV12/YUV 数据编码,实现视频通话、实时推流。
一、类整体概述
BaseVideoEncoder YUV 原始帧 → MediaCodec 硬件编码 → H264 裸流输出, 核心能力:
- 自动择优选择平台专用 H264 硬编码器(高通 / MTK / 海思 / Goke)
- 自动适配编码器支持的 YUV 格式(NV12/YP420)
- 动态实时调整码率、帧率(弱网动态降码逻辑)
- 首帧强制 I 帧、周期性主动请求关键帧,保证 P2P 拉流首帧可解码
- SPS/PPS 缓存拼接,关键帧自动带上解码头,不用外部拼接
- 单线程串行编码,加锁保护 MediaCodec,停止时安全释放防崩溃
入参:VideoEncodeParam(分辨率、码率、帧率、I 帧间隔) 输入:ByteArray 格式 NV12/YUV 图像数据 输出:H264 裸流,通过回调onVideoEncoded抛出。
二、成员变量说明
kotlin
private var mMediaCodec: MediaCodec? = null //H264编码器实例
private var mEncoderListener: OnEncodeListener? = null //编码数据回调
private var mExecutor = Executors.newSingleThreadExecutor() //单线程串行编码
private var seq = 0L //编码帧序号
private var startTimeUs: Long = 0 //基准时间戳,统一pts
@Volatile private var isStopping = false //原子停止标记,停止后丢弃新帧
private val codecLock = Object() //编码器操作同步锁
- 单线程:防止多线程并发入帧导致 MediaCodec 状态异常、花屏崩溃;
- isStopping:stop 期间拒绝新数据入队,避免释放后仍在编码;
- codecLock:启停 / 编码互斥,防止 stop 和 encode 并发操作 Codec。
三、启动流程 start ()
kotlin
override fun start() {
initMediaCodec()
}
启动链路:start()->initMediaCodec()->initMediaCodecInfo()选编码器→initVideoMediaFormat()配置参数→configure+start codec
1. initMediaCodecInfo ():硬件编码器择优逻辑【重点】
优先厂商定制硬编码器 > 通用软 / 硬编码器 预选白名单:
- OMX.qcom.video.encoder.avc(高通)
- OMX.MTK.VIDEO.ENCODER.AVC(联发科)
- OMX.hisi.video.encoder.avc(海思)
- 0MX.goke.video.encoder.avc(国科 GOKE,项目定制)
- c2.android.avc.encoder(Android 标准 C2 编码)
逻辑:
- 优先匹配 GOKE 定制编码器(项目 zgll 硬件专用);
- 遍历系统全部 Codec,命中白名单直接选用硬件编码器;
- 无指定硬编则选用系统通用 AVC 编码器兜底。
优势:充分利用 SOC 硬件加速,降低 CPU 占用,硬件设备必用。
2. initVideoMediaFormat () 编码参数配置
markdown
MediaFormat配置项:
1. MIMETYPE_VIDEO_AVC:H264编码
2. width/height:编码分辨率
3. KEY_BIT_RATE:初始码率
4. KEY_FRAME_RATE:帧率
5. KEY_I_FRAME_INTERVAL:I帧间隔(秒)
6. KEY_COLOR_FORMAT:默认NV12(YUV420SemiPlanar)
7. BITRATE_MODE_VBR:可变码率(动态码率升降依赖)
8. AVCProfileBaseline+Level31:全设备兼容基线规格
3. initMediaCodec 动态兼容色彩格式
从选中编码器的能力列表读取支持的 YUV 格式:
- 若编码器不支持 NV12,自动降级为 YUV420Planar (YV12);
- 避免因格式不匹配导致 configure 报错、黑屏无法编码。
四、核心编码入口:encoderH264 (data:ByteArray)
外部 Camera 回调 YUV 数据后调用此方法,整套编码逻辑在单线程池执行。
整体步骤
- 校验:空数据 / 正在停止直接丢弃;zgll 硬件额外校验 YUV 字节长度(wh3/2);
- 首帧 & 每 30 帧主动请求 I 帧:
PARAMETER_KEY_REQUEST_SYNC_FRAME,保证断流重连收到关键帧可解码; - dequeueInputBuffer 获取可用输入 buffer,写入 NV12/YUV 数据;
- 基于基准时间生成 PTS 时间戳,入队 Codec;
- 循环拉取 Codec 输出 H264 码流;
- 缓存 SPS/PPS(首帧 0pts 配置信息),关键帧自动拼接 SPS+PPS+I 帧裸流;
- 通过 listener 回调 H264 数据、pts、序号、是否关键帧。
SPS/PPS 自动拼接(项目关键优化)
H264 裸流 SPS/PPS 单独输出,播放器缺少则无法解码:
- 缓存首帧分离输出的 SPS/PPS;
- 遇到 I 帧时自动拼在帧头部一起抛出;
- 上层推流无需手动拼接,简化业务代码。
五、动态码率 / 帧率调整接口(弱网自适应)
scss
setVideoBitRate(bitRate:Int) //运行中实时改码率
setVideoFrameRate(frameRate:Int) //运行中实时改帧率
实现原理:通过MediaCodec.PARAMETER_KEY_VIDEO_BITRATE运行时动态下发参数,不用重启编码器,配合上层网络水位检测实现弱网自动降码,网络恢复升码。 入参做边界限制:码率 / 帧率限制在预设区间,避免参数越界异常。
六、stop&release 安全释放逻辑
kotlin
override fun stop(){
isStopping=true; //先标记停编,新帧丢弃
//1. 关闭线程池,等待正在执行任务结束,超时强制shutdownNow
//2. 加锁停止+释放MediaCodec,捕获异常防止崩溃
//3. 重置seq、时间戳,重建线程池支持下次复用
}
七、亮点
- 厂商硬编码优选:适配高通 / MTK / 海思 / GOKE 多款教育硬件芯片,最大化硬编性能;
- YUV 格式自动兼容:动态查询 Codec 能力,NV12/YV12 自动切换,适配不同相机输出格式;
- 自动 SPS/PPS 拼接:关键帧自带解码参数,上层推流零处理;
- 运行时动态码率 / 帧率:无需重启编码器,适配动态码率策略;
- 主动关键帧机制:首帧 + 周期 I 帧,P2P 通话快速起播;
- 线程 + 锁安全模型:单线程编码 + 同步锁 + 停止标记,极低 ANR / 崩溃概率;
- 硬件尺寸校验:zgll 机型 YUV 长度校验,提前拦截异常帧。
kotlin
open class BaseVideoEncoder(private val videoEncodeParam: VideoEncodeParam) : IBaseEncoder {
private var mMediaCodec: MediaCodec? = null //视频编码器
private var mEncoderListener: OnEncodeListener? = null
private var mExecutor = Executors.newSingleThreadExecutor()
private var seq = 0L
private var startTimeUs: Long = 0 // 添加基准时间
@Volatile private var isStopping = false // 新增:停止标志
private val codecLock = Object()
override fun start() {
try {
initMediaCodec()
} catch (e: IOException) {
e.printStackTrace()
LogUtil.e(TAG, "start " + e.message)
throw RuntimeException(e)
}
}
/**
* 初始化 视频 MediaFormat
*/
override fun initVideoMediaFormat(): MediaFormat {
return MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC,
videoEncodeParam.width,
videoEncodeParam.height
).apply {
setInteger(MediaFormat.KEY_BIT_RATE, videoEncodeParam.bitRate)
setInteger(MediaFormat.KEY_FRAME_RATE, videoEncodeParam.frameRate)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, videoEncodeParam.iFrameInterval)
// 使用 NV12 (COLOR_FormatYUV420SemiPlanar)
setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar)
setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)
// 设置Profile和Level确保兼容性
setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline)
setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31)
}
}
/**
* 创建 视频编码器
*/
open fun initMediaCodec(mediaCodecInfo: MediaCodecInfo? = initMediaCodecInfo()) {
if (mediaCodecInfo == null) {
LogUtil.e(TAG, "无法找到支持的视频编码器类型")
return
}
try {
mMediaCodec = MediaCodec.createByCodecName(mediaCodecInfo.name)
// 查询实际支持的颜色格式
val capabilities = mediaCodecInfo.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC)
val supportedColorFormats = capabilities.colorFormats
LogUtil.d(TAG, "Encoder ${mediaCodecInfo.name} supports formats: ${supportedColorFormats.joinToString()}")
// 创建 MediaFormat 时选择支持的颜色格式
val format = initVideoMediaFormat().apply {
// 如果编码器不支持配置的格式,选择一个支持的
val configuredFormat = getInteger(MediaFormat.KEY_COLOR_FORMAT)
if (!supportedColorFormats.contains(configuredFormat)) {
val fallbackFormat = supportedColorFormats.find {
it == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar ||
it == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
} ?: supportedColorFormats.firstOrNull()
fallbackFormat?.let {
setInteger(MediaFormat.KEY_COLOR_FORMAT, it)
// LogUtil.w(TAG, "Fallback to color format: $it")
}
}
}
mMediaCodec?.configure(
format,
null,
null,
MediaCodec.CONFIGURE_FLAG_ENCODE
)
mMediaCodec?.start()
} catch (e: IOException) {
LogUtil.e(TAG, "创建编码器异常.." + e.message)
e.printStackTrace()
throw java.lang.RuntimeException(e)
}
}
/**
* 初始化 视频编码器 获取编码器相关信息
* 优先选择硬件编码器(hardwareEncoderList) 如果没有找到硬件编码器,则选择 H.264(AVC)软件编码器作为默认
*/
open fun initMediaCodecInfo(): MediaCodecInfo? {
val hardwareEncoderList =
mutableListOf(
"OMX.MTK.VIDEO.ENCODER.AVC",
"c2.android.avc.encoder",
"OMX.qcom.video.encoder.avc",
"OMX.hisi.video.encoder.avc"
)
val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
val codecInfoArray = codecList.codecInfos
for (codecInfo in codecInfoArray) {
for (type in codecInfo.supportedTypes) {
if (type.startsWith("video/")) {
Log.e("lee", "codecInfo--->" + codecInfo.name)
if (codecInfo.name == "0MX.goke.video.encoder.avc") {
return codecInfo
}
}
}
}
for (codecInfo in codecInfoArray) {
if (codecInfo.isEncoder) {
for (type in codecInfo.supportedTypes) {
if (type.startsWith("video/")) {
val capabilities = codecInfo.getCapabilitiesForType(type)
val supportColorFormats = Arrays.toString(capabilities.colorFormats)
if (hardwareEncoderList.contains(codecInfo.name)) {
LogUtil.d(
TAG,
"hardwareEncoderName=${codecInfo.name} supportColorFormats=$supportColorFormats "
)
capabilities.profileLevels.forEach {
LogUtil.d(
TAG,
"supportProfileLevels level=${it.level} profile=${it.profile}"
)
}
return codecInfo
} else if (MediaFormat.MIMETYPE_VIDEO_AVC == type) {
LogUtil.d(
TAG,
"softwareEncoderName=${codecInfo.name} supportColorFormats=$supportColorFormats "
)
capabilities.profileLevels.forEach {
LogUtil.d(
TAG,
"supportProfileLevels level=${it.level} profile=${it.profile}"
)
}
return codecInfo
}
}
}
}
}
// val hardwareEncoderList =
// mutableListOf("OMX.MTK.VIDEO.ENCODER.AVC", "c2.android.avc.encoder", "OMX.qcom.video.encoder.avc", "OMX.hisi.video.encoder.avc")
// val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
// val codecInfoArray = codecList.codecInfos
// for (codecInfo in codecInfoArray) {
// if (codecInfo.isEncoder) {
// for (type in codecInfo.supportedTypes) {
// if (type.startsWith("video/")) {
// val capabilities = codecInfo.getCapabilitiesForType(type)
// val supportColorFormats = Arrays.toString(capabilities.colorFormats)
// if (hardwareEncoderList.contains(codecInfo.name)) {
// LogUtil.d(TAG, "hardware encoderName:${codecInfo.name} support colorFormats:$supportColorFormats")
// return codecInfo
// } else if (MediaFormat.MIMETYPE_VIDEO_AVC == type) {
// LogUtil.d(TAG, "software encoderName:${codecInfo.name} support colorFormats:$supportColorFormats")
// return codecInfo
// }
// }
// }
// }
// }
return null
}
override fun setEncoderListener(onEncodeListener: OnEncodeListener) {
mEncoderListener = onEncodeListener
}
override fun stop() {
isStopping = true // 先标记停止,阻止新任务
// 等待 executor 中的任务完成
try {
mExecutor.shutdown()
if (!mExecutor.awaitTermination(2, TimeUnit.SECONDS)) {
mExecutor.shutdownNow()
}
} catch (e: InterruptedException) {
mExecutor.shutdownNow()
}
synchronized(codecLock) {
try {
mMediaCodec?.stop()
mMediaCodec?.release()
} catch (e: Exception) {
LogUtil.e(TAG, "Error stopping codec: ${e.message}")
} finally {
mMediaCodec = null
startTimeUs = 0
seq = 0
}
}
// 重建 executor 供下次使用
if (mExecutor.isShutdown) {
mExecutor = Executors.newSingleThreadExecutor()
}
isStopping = false
}
override fun release() {
mExecutor.shutdown()
}
// BaseVideoEncoder.kt
override fun encoderH264(data: ByteArray) {
if (data.isEmpty() || isStopping) return // 停止中拒绝新数据
// 使用 try-catch 包裹,防止异常扩散
val task = Runnable {
if (isStopping) return@Runnable
synchronized(codecLock) {
if (mMediaCodec == null || isStopping) return@Runnable
try {
// ... 原有编码逻辑 ...
mExecutor.submit {
mMediaCodec?.let { codec ->
// 修改:首帧(seq=0)必须强制关键帧,且确保 SPS/PPS 已输出
if (seq == 0L) {
// 在编码前请求关键帧
val params = Bundle()
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
codec.setParameters(params)
// LogUtil.d(TAG, "Force key frame for first frame")
} else if (seq % 30 == 0L) {
// 后续每 30 帧请求关键帧
val params = Bundle()
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
codec.setParameters(params)
}
val inputBufferIndex = codec.dequeueInputBuffer(-1)
if (inputBufferIndex >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferIndex)
inputBuffer?.let { buffer ->
// 关键:确保缓冲区被完全清空并正确填充
buffer.clear()
// 检查缓冲区容量是否足够
// if (buffer.remaining() < data.size) {
// LogUtil.e(TAG, "Input buffer too small! Remaining: ${buffer.remaining()}, Need: ${data.size}")
// }
buffer.put(data)
val presentationTimeUs = if (startTimeUs == 0L) {
startTimeUs = System.nanoTime() / 1000
0L
} else {
System.nanoTime() / 1000 - startTimeUs
}
codec.queueInputBuffer(
inputBufferIndex,
0,
data.size,
presentationTimeUs,
0
)
// LogUtil.d(TAG, "Queued input buffer: size=${data.size}, pts=$presentationTimeUs")
}
}
// 立即尝试获取输出
val bufferInfo = MediaCodec.BufferInfo()
var outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0)
// LogUtil.d(TAG, "Dequeue output: index=$outputBufferIndex, flags=${bufferInfo.flags}")
// 输出处理:保存 SPS/PPS,与第一个关键帧合并发送
var spsPpsData: ByteArray? = null
while (outputBufferIndex >= 0) {
val outputBuffer = codec.getOutputBuffer(outputBufferIndex)
outputBuffer?.let { buffer ->
val outData = ByteArray(bufferInfo.size)
buffer.get(outData)
val isKeyFrame = (bufferInfo.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0
// 保存 SPS/PPS (通常是前几个字节,非关键帧但 pts=0)
if (seq == 0L && !isKeyFrame && outData.size < 100) {
spsPpsData = outData
// LogUtil.d(TAG, "Save SPS/PPS: size=${outData.size}")
} else {
// 如果是关键帧且有 SPS/PPS 缓存,合并发送
var finalData = outData
if (isKeyFrame && spsPpsData != null) {
finalData = spsPpsData!! + outData
spsPpsData = null // 清空缓存
// LogUtil.d(TAG, "Merge SPS/PPS with key frame: final size=${finalData.size}")
}
mEncoderListener?.onVideoEncoded(
finalData,
bufferInfo.presentationTimeUs / 1000,
seq,
isKeyFrame
)
seq++
if (seq < 0) seq = 0 // 防止溢出
}
}
codec.releaseOutputBuffer(outputBufferIndex, false)
outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0)
}
}
}
} catch (e: IllegalStateException) {
// MediaCodec 状态异常,安全忽略
LogUtil.w(TAG, "Codec state error during encode: ${e.message}")
} catch (e: Exception) {
LogUtil.e(TAG, "encoderH264 error: ${e.message}")
}
}
}
try {
mExecutor.execute(task)
} catch (e: RejectedExecutionException) {
LogUtil.w(TAG, "Executor shutdown, skip frame")
}
if(BuildConfig.FLAVOR=="zgll"){
// 验证数据大小:640 * 480 * 1.5 = 460800
val expectedSize = videoEncodeParam.width * videoEncodeParam.height * 3 / 2
if (data.size != expectedSize) {
// LogUtil.e(TAG, "Data size error! Expected: $expectedSize, Got: ${data.size}, " +
// "width=${videoEncodeParam.width}, height=${videoEncodeParam.height}")
return
}
}
}
override fun setVideoBitRate(bitRate: Int) {
val nowBitrate = videoEncodeParam.bitRate
val targetBitrate = bitRate.coerceIn(
videoEncodeParam.getBitRateRange().first,
videoEncodeParam.getBitRateRange().last
)
if (nowBitrate == targetBitrate) {
return
}
/* // 计算比特率变化的百分比,避免剧烈变化
val changeRatio = kotlin.math.abs(nowBitrate - targetBitrate).toFloat() / nowBitrate
if (changeRatio > 0.2f) { // 限制最大变动幅度
videoEncodeParam.bitRate = (nowBitrate * 1.2).toInt().coerceAtMost(targetBitrate)
} else {
videoEncodeParam.bitRate = targetBitrate
}*/
videoEncodeParam.bitRate = targetBitrate
val params = Bundle()
params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, videoEncodeParam.bitRate)
mMediaCodec?.setParameters(params)
}
override fun setVideoFrameRate(frameRate: Int) {
val nowFrameRate = videoEncodeParam.frameRate
val targetFrameRate = nowFrameRate.coerceIn(
videoEncodeParam.getFrameRateRange().first,
videoEncodeParam.getFrameRateRange().last
)
if (nowFrameRate == targetFrameRate) {
return
}
videoEncodeParam.frameRate = frameRate
val params = Bundle()
params.putInt(MediaFormat.KEY_FRAME_RATE, frameRate)
mMediaCodec?.setParameters(params)
}
override fun getVideoBitRate(): Int = videoEncodeParam.bitRate;
override fun getVideoFrameRate(): Int = videoEncodeParam.frameRate;
override fun getBitRateInterval() = videoEncodeParam.getBitRateRange()
companion object {
private const val TAG = "BaseVideoEncoder"
}
}