Android 音视频通话核心 —— MediaCodec H.264 硬编码,SPS/PPS 合并与动态码率,视频编码全解析

本文讲H.264 硬编码实现(MediaCodec)配合 Camera 采集 NV12/YUV 数据编码,实现视频通话、实时推流。

一、类整体概述

BaseVideoEncoder YUV 原始帧 → MediaCodec 硬件编码 → H264 裸流输出, 核心能力:

  1. 自动择优选择平台专用 H264 硬编码器(高通 / MTK / 海思 / Goke)
  2. 自动适配编码器支持的 YUV 格式(NV12/YP420)
  3. 动态实时调整码率、帧率(弱网动态降码逻辑)
  4. 首帧强制 I 帧、周期性主动请求关键帧,保证 P2P 拉流首帧可解码
  5. SPS/PPS 缓存拼接,关键帧自动带上解码头,不用外部拼接
  6. 单线程串行编码,加锁保护 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 编码)

逻辑:

  1. 优先匹配 GOKE 定制编码器(项目 zgll 硬件专用);
  2. 遍历系统全部 Codec,命中白名单直接选用硬件编码器;
  3. 无指定硬编则选用系统通用 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 数据后调用此方法,整套编码逻辑在单线程池执行

整体步骤

  1. 校验:空数据 / 正在停止直接丢弃;zgll 硬件额外校验 YUV 字节长度(wh3/2);
  2. 首帧 & 每 30 帧主动请求 I 帧:PARAMETER_KEY_REQUEST_SYNC_FRAME,保证断流重连收到关键帧可解码;
  3. dequeueInputBuffer 获取可用输入 buffer,写入 NV12/YUV 数据;
  4. 基于基准时间生成 PTS 时间戳,入队 Codec;
  5. 循环拉取 Codec 输出 H264 码流;
  6. 缓存 SPS/PPS(首帧 0pts 配置信息),关键帧自动拼接 SPS+PPS+I 帧裸流;
  7. 通过 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、时间戳,重建线程池支持下次复用
}

七、亮点

  1. 厂商硬编码优选:适配高通 / MTK / 海思 / GOKE 多款教育硬件芯片,最大化硬编性能;
  2. YUV 格式自动兼容:动态查询 Codec 能力,NV12/YV12 自动切换,适配不同相机输出格式;
  3. 自动 SPS/PPS 拼接:关键帧自带解码参数,上层推流零处理;
  4. 运行时动态码率 / 帧率:无需重启编码器,适配动态码率策略;
  5. 主动关键帧机制:首帧 + 周期 I 帧,P2P 通话快速起播;
  6. 线程 + 锁安全模型:单线程编码 + 同步锁 + 停止标记,极低 ANR / 崩溃概率;
  7. 硬件尺寸校验: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"
    }


}

相关推荐
plainGeekDev1 小时前
Fragment 手动跳转 → Navigation 组件
android·java·kotlin
plainGeekDev1 小时前
XML 主题 → Compose Material3 主题
android·java·kotlin
Kapaseker2 小时前
Rust 是如何干掉空指针的
rust·kotlin
消失的旧时光-19433 小时前
Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?
java·kotlin·async·launch·withcontext·deferred
修行者对6663 小时前
Kotlin学习笔记(1)
kotlin
Refrain_zc17 小时前
Android 音视频通话核心 —— 音频解码(AAC → PCM → 播放)完整解析
kotlin
Refrain_zc17 小时前
Android 音视频通话核心 —— Camera 采集 + 音视频编码调度
kotlin
plainGeekDev21 小时前
AlertDialog → DialogFragment
android·java·kotlin
Meteors.1 天前
Kotlin协程序使用技巧和应用场景
android·开发语言·kotlin