Android 音视频通话核心 —— Camera 采集 + 音视频编码调度

Camera1 + YUV 处理 + H.264/AAC 编码 + 弱网码率自适应 + 腾讯 IoT SDK 推流

本文解析 CameraRecorder2 核心类,它是音视频通话、视频对讲、教育硬件、IoT 视频通话总调度管理器

职责:相机打开 → 预览数据回调 → YUV 旋转 / 格式转换 → 视频编码 → 音频编码 → 实时发送数据给 SDK

一、这个类是干嘛的?

CameraRecorder2音视频通话的核心调度器,负责:

  • 管理 Camera1 相机采集
  • 管理 视频编码器(H.264)
  • 管理 音频编码器(AAC)
  • 处理 YUV 数据格式转换、旋转、镜像
  • 对接 腾讯 IoT Video SDK 发送音视频数据
  • 支持动态码率自适应、多访客、本地录制调试

二、核心成员变量解释

java 复制代码
// 相机参数
private var mCameraId = 前置摄像头ID
private var mVideoWidth / mVideoHeight = 预览分辨率
private var mCamera: Camera? = null       // Camera1 硬件相机

// 编码器
private var mVideoEncoder: BaseVideoEncoder? = null  // H.264 编码器
private var mAudioEncoder: BaseAudioEncoder? = null // AAC 编码器

// 通话状态
private var isMuted = false        // 静音
private var mIsRecording = false   // 是否正在编码推流
private var mVisitorInfo: Map      // 多访客管理(支持多人同时观看)
private var videoResType = 0       // 分辨率类型

// 调试
private var isSaveRecord = false   // 是否保存H264/AAC到本地

三、核心流程

整个通话链路如下:

  1. openCamera() → 打开 Camera1,设置预览尺寸、帧率、对焦、旋转角度
  2. onPreviewFrame() → 拿到原始 YUV 数据
  3. YUV 旋转 / 格式转换 → 适配设备方向与编码器要求
  4. encoderH264() → 送入 H.264 编码器
  5. startAudioEncoder() → 启动麦克风 + AAC 编码
  6. onVideoEncoded / onAudioEncoded → 编码完成回调
  7. sendAvtVideoData / sendAvtAudioData → 通过腾讯 IoT SDK 发送出去

四、逐模块代码详解

1. 打开相机:openCamera ()

作用:初始化 Camera1,配置预览、方向、帧率、格式。

ini 复制代码
mCamera = Camera.open(mCameraId)

重点配置:

  • setDisplayOrientation (角度) :修正相机预览方向
  • setPreviewSize (宽,高) :设置预览分辨率
  • previewFormat = NV21 / YV12:相机输出格式(Android 标准)
  • setPreviewFpsRange:设置预览帧率(15fps / 30fps)
  • FOCUS_MODE_CONTINUOUS_PICTURE:连续自动对焦

特殊适配:zgll 设备

ini 复制代码
if(BuildConfig.FLAVOR=="zgll"){
    previewSize = 640x480
    previewFormat = YV12
    fps = 30000-30000 → 固定30帧
}

2. 开始录制推流:startRecording ()

作用:延迟 700ms 启动音视频编码,保证 P2P 连接已建立。

scss 复制代码
Handler().postDelayed({
    startVideoEncoder()
    startAudioEncoder()
    mIsRecording = true
}, 700)

3. 视频编码启动:startVideoEncoder ()

根据渠道配置编码器:

  • 分辨率:640x480(zgll)/ 自适应
  • 帧率:15 / 30
  • 码率:根据分辨率自动计算
  • I 帧间隔:2 秒
ini 复制代码
val encodeParam = VideoEncodeParam().apply {
    width = encodeWidth
    height = encodeHeight
    frameRate = mVideoFrameRate
    bitRate = getAverageBitRate()
    iFrameInterval = 2
}

编码器支持多种实现:

复制代码
VideoEncoder1 / 2 / 3 / BaseVideoEncoder

4. 音频编码启动:startAudioEncoder ()

使用之前的 BaseAudioEncoder,做:

  • 麦克风采集
  • AEC 回声消除
  • NS 噪声抑制
  • AGC 自动增益
  • AAC 编码
scss 复制代码
mAudioEncoder = BaseAudioEncoder()
mAudioEncoder.startAudio()

5. 相机预览回调:onPreviewFrame ()

这是视频数据入口,所有画面都从这里来。

核心工作:

  1. 判断是否正在推流
  2. 对 YUV 数据旋转、格式转换
  3. 交给 encoderH264() 编码
kotlin 复制代码
override fun onPreviewFrame(data: ByteArray, camera: Camera) {
    // 旋转、格式转换
    val processedData = rotateYUV90(...) 或 yv12ToNv12(...)
    // 送入编码器
    mVideoEncoder.encoderH264(processedData)
}

6. YUV 格式转换与旋转(高频面试 / 踩坑点)

(1)YV12 → NV12

kotlin 复制代码
private fun yv12ToNv12(yv12: ByteArray, width: Int, height: Int): ByteArray
  • YV12:Y 单独 → V 平面 → U 平面
  • NV12:Y 单独 → UV 交错 编码器大多要求 NV12

(2)NV12 / YV12 旋转

支持:

  • rotateYUV90()
  • rotateYUV180()
  • rotateYUV270()
  • rotateNV12_90()
  • rotateNV12_270()
  • rotateNV12_180()

用于:

  • 前后置摄像头方向修正
  • 视频画面角度修正

7. 编码完成回调

音频编码完成

kotlin 复制代码
override fun onAudioEncoded(datas: ByteArray, pts: Long, seq: Long) {
    VideoNativeInterface.sendAvtAudioData(...)
}

视频编码完成

kotlin 复制代码
override fun onVideoEncoded(datas: ByteArray, pts: Long, seq: Long, isKeyFrame: Boolean) {
    VideoNativeInterface.sendAvtVideoData(...)
}

作用:把 H.264 帧 + AAC 帧 发送给腾讯 IoT SDK 实现实时通话。


8. 动态码率自适应(弱网优化)

类:AdapterBitRateTask 每 1 秒检测一次网络状态:

  • 根据 send buffer 水线 升降码率
  • 根据 网络发送速度 升降码率
  • 网络差 → 降码率、降帧率
  • 网络好 → 升码率、升帧率
scss 复制代码
mVideoEncoder.setVideoBitRate(new_video_rate)
mVideoEncoder.setVideoFrameRate(new_frame_rate)

9. 释放资源:closeCamera () + release ()

安全释放顺序(非常重要,避免崩溃):

  1. 标记 isReleasing = true
  2. 停止预览回调
  3. 停止编码器
  4. 释放 Camera
  5. 清空状态
kotlin 复制代码
fun closeCamera() {
    isReleasing = true
    mCamera.setPreviewCallback(null)
    mCamera.stopPreview()
    mVideoEncoder.release()
    mAudioEncoder.stopAudio()
    mCamera.release()
}

五、亮点

  1. Camera1 全机型兼容(教育硬件 / 低配设备首选)
  2. YV12/NV21 自动转换,适配不同硬件编码器
  3. 多方向 YUV 旋转,解决各种设备角度问题
  4. 音视频分离编码、独立控制
  5. 支持多人同时观看(多访客)
  6. 动态码率自适应,弱网不掉线
  7. 安全释放机制,避免崩溃
  8. 渠道差异化配置(zgll 等设备专用)
  9. 本地调试录制,方便定位问题
  10. 对接腾讯 IoT SDK,可直接上线视频通话
scss 复制代码
class CameraRecorder2 : PreviewCallback, OnEncodeListener {
    private var mCameraId = getCameraFacingFrontId()
    private var mVideoWidth = getCameraPreviewWidthSize()
    private var mVideoHeight = getCameraPreviewHeightSize()
    private var mVideoFrameRate = 15
    private var mCamera: Camera? = null
    private var mVideoEncoder: BaseVideoEncoder? = null
    private var mAudioEncoder: BaseAudioEncoder? = null
    private var isMuted = false
    private var mIsRecording = false
    private val mVisitorInfo: MutableMap<Int, Int> = HashMap(MaxVisitors)

    @VideoResType
    private var videoResType = 0
    private var visitor = 0
    private var isRunning = false

    // for test only
    private var isSaveRecord = false
    private var mSaveAudioAndVideoManager: SaveAudioAndVideoManager? = null
    @Volatile private var isReleasing = false  // 新增释放标志
    /**
     * 保存 相机预览的视频和 编码后的视频到本地 调试时使用
     */
    fun isSaveRecord(isSaveRecord: Boolean) {
        this.isSaveRecord = isSaveRecord
        mSaveAudioAndVideoManager = SaveAudioAndVideoManager()
        mSaveAudioAndVideoManager?.startSavingFrames()
    }

    fun openCamera(surfaceTexture: SurfaceTexture?, context: Context?) {
        if(BuildConfig.FLAVOR=="zgll"){
            mVideoFrameRate = 30
            val (alignedW, alignedH) = ensureSizeAligned(640, 480)
            mVideoWidth = alignedW  // 640
            mVideoHeight = alignedH // 480
        }
        try {
            mCamera = Camera.open(mCameraId)
            mCamera?.apply {
                // 设置方向
//                val displayOrientation = if (LauncherDataUtil.getApkv() == "znfyyouerv5") {
//                    getCameraAngle()
//                } else {
//                    CameraUtils.getDisplayOrientation(context, mCameraId, getCameraAngle())
//                }
                setDisplayOrientation(getCameraAngle())
                parameters = getParameters().apply {
                    if(BuildConfig.FLAVOR=="zgll"){
                        setPreviewSize(mVideoWidth, mVideoHeight)
                        previewFormat = ImageFormat.YV12  // 或者尝试 ImageFormat.NV21
                        // 帧率必须设为 30000,30000
                        setPreviewFpsRange(30000, 30000)
                    }else{
                        // 先获取支持的预览尺寸,选择最接近目标尺寸的一个
                        val optimalSize = getOptimalPreviewSize(
                            parameters.supportedPreviewSizes,
                            mVideoWidth,
                            mVideoHeight
                        )
                        optimalSize?.let {
                            mVideoWidth = it.width  // 更新实际使用的尺寸
                            mVideoHeight = it.height
                            LogUtil.w(TAG, "Selected preview size: ${it.width}x${it.height}")
                        }
                        setPreviewSize(mVideoWidth, mVideoHeight)
                        // 预览格式保持 NV21(这是 Android 相机标准输出)
                        previewFormat = ImageFormat.NV21
                        // 使用新的 FPS 设置方式
                        val targetFps = mVideoFrameRate * 1000
                        val supportedFpsRanges = parameters.supportedPreviewFpsRange
                        val optimalFpsRange = supportedFpsRanges.find {
                            it[0] <= targetFps && it[1] >= targetFps
                        } ?: supportedFpsRanges.lastOrNull()

                        optimalFpsRange?.let {
                            setPreviewFpsRange(it[0], it[1])
                            LogUtil.w(TAG, "Set FPS range: ${it[0]}-${it[1]}")
                        }
                    }
                    if (parameters.supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
                        parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
                    } else if (parameters.supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
                        parameters.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
                    }
                    parameters.supportedPreviewSizes.forEach {
                        LogUtil.w(TAG, " Camera.Parameters.supportedPreviewSize() width=" + it.width + "  height=" + it.height)
                    }
                    LogUtil.w(TAG, " Camera.Parameters.supportedPreviewFormats()" + parameters.supportedPreviewFormats)
                }
                setPreviewTexture(surfaceTexture)
                setPreviewCallback(this@CameraRecorder2)
                startPreview()
            }
            isRunning = true
        } catch (e: RuntimeException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    fun closeCamera() {
        isReleasing = true  // 先标记,阻止 onPreviewFrame 提交新数据

        // 停止预览回调
        mCamera?.setPreviewCallback(null)
        mCamera?.stopPreview()

        // 等待一帧时间,确保 onPreviewFrame 完成
        try { Thread.sleep(50) } catch (e: Exception) {}

        mVideoEncoder?.release()  // 现在安全释放编码器
        mAudioEncoder?.stopAudio()

        mCamera?.let {
            it.release()
            mCamera = null
        }
        isRunning = false
        isReleasing = false
    }

    // 新增:标记是否是接听(小程序打进来)
    private var mIsIncomingCall: Boolean = false
    fun startRecording(visitor: Int, res_type: Int, isIncoming: Boolean = false) {
        LogUtil.e(TAG, "startRecording: visitor=$visitor, res_type=$res_type, isIncoming=$isIncoming")

        if (visitor < 0) {
            LogUtil.e(TAG, "Invalid visitor ID: $visitor")
            return
        }
        mIsIncomingCall = isIncoming  // 保存方向

        this.visitor = visitor
        videoResType = res_type

        if (mIsRecording) {
            mVisitorInfo[visitor] = res_type
            return
        }

        mVisitorInfo[visitor] = res_type

        // 延迟 700ms 开始编码,确保 SDK P2P 连接建立完成
        Handler(Looper.getMainLooper()).postDelayed({
            if (mVisitorInfo.isNotEmpty()) {
                startVideoEncoder()
                startAudioEncoder()
                mIsRecording = true
                LogUtil.d(TAG, "start camera recording after delay")
                startBitRateAdapter()
            }
        }, 700)
    }

    /**
     * 开启执行 音频编码
     */
    private fun startAudioEncoder() {
        LogUtil.e("zcc", "getDecoderAudioType() : "+ getDecoderAudioType())
        when (getDecoderAudioType()) {
            1 -> mAudioEncoder = AudioEncoder1()
            else -> mAudioEncoder = BaseAudioEncoder()
        }
        mAudioEncoder?.apply {
            setOnEncodeListener(this@CameraRecorder2)
            setMuted(isMuted)
            startAudio()
        }
    }

    /**
     * 开始 视频编码
     */
    private fun startVideoEncoder() {
        if (BuildConfig.FLAVOR == "zgll") {
            mVideoFrameRate = 30
        }

        if (TweCallStateManager.callMediaType == CallMediaType.Video) {
            // zgll flavor 需要交换宽高(旋转90度后 640x480 -> 480x640)
            val encodeWidth: Int
            val encodeHeight: Int

            if (BuildConfig.FLAVOR == "zgll") {
                // 旋转后宽高互换:640x480 旋转后变为 480x640
                encodeWidth = mVideoHeight   // 480
                encodeHeight = mVideoWidth   // 640
            } else {
                encodeWidth = mVideoWidth
                encodeHeight = mVideoHeight
            }

            val encodeParam = VideoEncodeParam().apply {
                width = encodeWidth
                height = encodeHeight
                frameRate = mVideoFrameRate
                bitRate = getAverageBitRate()
                iFrameInterval = 2
            }

            Log.e(TAG, "VideoEncodeParam: ${encodeWidth}x${encodeHeight}@${mVideoFrameRate}fps, bitrate=${encodeParam.bitRate}, flavor=${BuildConfig.FLAVOR}")

            when (getDecoderType()) {
                1 -> mVideoEncoder = VideoEncoder1(encodeParam)
                2 -> mVideoEncoder = VideoEncoder2(encodeParam)
                3 -> mVideoEncoder = VideoEncoder3(encodeParam)
                else -> mVideoEncoder = BaseVideoEncoder(encodeParam)
            }

            mVideoEncoder?.apply {
                setEncoderListener(this@CameraRecorder2)
                start()
            }
        }
    }

    fun setMuted(muted: Boolean) {
        isMuted = muted
        mAudioEncoder?.setMuted(isMuted)
    }

    fun stopRecording(visitor: Int, res_type: Int) {
        if (!mIsRecording) {
            return
        }
        mVideoEncoder?.stop()
//        mAudioEncoder?.stopRecord()
        mAudioEncoder?.stopAudio()
        mIsRecording = false
        mVisitorInfo.remove(visitor)
        if (mVisitorInfo.isNotEmpty()) return
        LogUtil.d(TAG, "stop camera recording")
        stopBitRateAdapter()
    }

    override fun onAudioEncoded(datas: ByteArray, pts: Long, seq: Long) {
//        LogUtil.d(TAG, "encoded audio data len " + datas.length + " pts " + pts);
        if (mIsRecording) {
            mVisitorInfo.forEach { (visitor, videoResType) ->
                val ret = VideoNativeInterface.getInstance().sendAvtAudioData(datas, pts, seq, visitor, 0, videoResType)
                if (ret != 0) {
                    LogUtil.e(TAG, "sendAudioData to visitor $visitor failed: $ret")
                }
            }
            if (isSaveRecord) {
                mSaveAudioAndVideoManager?.saveLocalEncodedAudio(datas)
            }
        }
    }

    private var stat_cnt = 0
    override fun onVideoEncoded(datas: ByteArray, pts: Long, seq: Long, isKeyFrame: Boolean) {
        // 验证数据有效性
        if (datas.size < 10) {
            LogUtil.e(TAG, "Invalid video data size: ${datas.size}")
            return
        }

        // 检查 H.264 NAL 头
        val isValidH264 = datas[0] == 0x00.toByte() &&
                datas[1] == 0x00.toByte() &&
                datas[2] == 0x00.toByte() &&
                datas[3] == 0x01.toByte()

        if (!isValidH264) {
            LogUtil.e(TAG, "Invalid H.264 data header")
            return
        }

        // 关键:检查 SDK 是否已连接
        if (!mIsRecording || mVisitorInfo.isEmpty()) {
            LogUtil.w(TAG, "SDK not ready, skip frame #$seq")
            return
        }

        // 打印关键帧信息
        if (isKeyFrame || seq % 30 == 0L) {
//            LogUtil.e(TAG, "Video frame #$seq: size=${datas.size}, keyFrame=$isKeyFrame, pts=$pts")
        }

        // 发送数据
        mVisitorInfo.forEach { (visitor, videoResType) ->
            // 检查连接状态
            val iv = VideoNativeInterface.getInstance()
            val bufSize = iv.getSendStreamBuf(visitor, 0, videoResType)

            // 如果缓冲区为 -1 或异常值,说明连接未建立
            if (bufSize < 0) {
//                LogUtil.e(TAG, "SDK connection not ready for visitor $visitor, bufSize=$bufSize")
                return@forEach
            }

            val ret = iv.sendAvtVideoData(datas, pts, seq, isKeyFrame, visitor, 0, videoResType)
            if (ret != 0) {
//                LogUtil.e(TAG, "sendVideoData failed: ret=$ret, visitor=$visitor, size=${datas.size}, keyFrame=$isKeyFrame, bufSize=$bufSize")
            } else if (seq % 30 == 0L) {
//                LogUtil.d(TAG, "sendVideoData success: seq=$seq, size=${datas.size}")
            }

            if (isSaveRecord) {
                mSaveAudioAndVideoManager?.saveLocalEncodedVideo(datas)
            }
        }
    }
    override fun onPreviewFrame(data: ByteArray, camera: Camera) {
        if (!mIsRecording || !mIsRecording || mVideoEncoder == null) return
        if (data.isEmpty()) {
//            LogUtil.e(TAG, "onPreviewFrame data is null")
            return
        }
        val processedData = if (BuildConfig.FLAVOR == "zgll") {
            // YV12 -> NV12 -> 顺时针旋转90度(从竖屏视角看是正的)
            val nv12Data = yv12ToNv12(data, mVideoWidth, mVideoHeight)
            // 根据方向选择旋转:接听用 90,打出去用 270
            if (mIsIncomingCall) {
                rotateNV12_90(nv12Data, mVideoWidth, mVideoHeight)
            } else {
                rotateNV12_270(nv12Data, mVideoWidth, mVideoHeight)
            }
        } else {
            // 其他 flavor 保持原有逻辑
            when (getCameraAngle()) {
                0 -> data
                90 -> rotateYUV90(data, mVideoWidth, mVideoHeight)
                180 -> rotateYUV180(data, mVideoWidth, mVideoHeight)
                270 -> rotateYUV270(data, mVideoWidth, mVideoHeight)
                else -> data
            }
        }
        // 使用 try-catch 防止编码器已停止
        try {
            mVideoEncoder!!.encoderH264(processedData)
        } catch (e: Exception) {
            LogUtil.w(TAG, "Encode failed during release: ${e.message}")
        }
    }

    @DynamicBitRateType
    private val dynamicBitRateType = DynamicBitRateType.INTERNET_SPEED_TYPE

    inner class AdapterBitRateTask : TimerTask() {
        private var exceedLowMark = false
        override fun run() {
            // 暂时不执行任何码率调整,保持固定码率测试
            LogUtil.d(TAG, "Bitrate adapter skipped for testing")
            return

            println("检测时间到:" + System.currentTimeMillis().toDateString())
            mVideoEncoder?.let {
                if (dynamicBitRateType == DynamicBitRateType.WATER_LEVEL_TYPE) {
                    val bufSize = VideoNativeInterface.getInstance().getSendStreamBuf(visitor, 0, videoResType)
                    val p2p_wl_avg = VideoNativeInterface.getInstance().getAvgMaxMin(bufSize)
                    val now_video_rate = it.getVideoBitRate()
                    val now_frame_rate = it.getVideoFrameRate()
                    LogUtil.e(
                        TAG,
                        "WATER_LEVEL_TYPE send_bufsize==$bufSize,now_video_rate==$now_video_rate,avg_index==$p2p_wl_avg,now_frame_rate==$now_frame_rate"
                    )
                    // 降码率
                    // 当发现p2p的水线超过一定值时,降低视频码率,这是一个经验值,一般来说要大于 [视频码率/2]
                    // 实测设置为 80%视频码率 到 120%视频码率 比较理想
                    // 在10组数据中,获取到平均值,并将平均水位与当前码率比对。
                    val video_rate_byte = now_video_rate / 8 * 3 / 4
                    if (p2p_wl_avg > video_rate_byte) {
                        it.setVideoBitRate(now_video_rate / 2)
                        it.setVideoFrameRate(now_frame_rate / 3)
                    } else if (p2p_wl_avg < now_video_rate / 8 / 3) {

                        // 升码率
                        // 测试发现升码率的速度慢一些效果更好
                        // p2p水线经验值一般小于[视频码率/2],网络良好的情况会小于 [视频码率/3] 甚至更低
                        it.setVideoBitRate(now_video_rate + (now_video_rate - p2p_wl_avg * 8) / 5)
                        it.setVideoFrameRate(now_frame_rate * 5 / 4)
                    }
                } else if (dynamicBitRateType == DynamicBitRateType.INTERNET_SPEED_TYPE) {
                    val ivP2pSendInfo = VideoNativeInterface.getInstance().getSendStreamStatus(visitor, 0, videoResType)
                    val bufSize = VideoNativeInterface.getInstance().getSendStreamBuf(visitor, 0, videoResType)
                    val now_video_rate = it.getVideoBitRate()
                    val now_frame_rate = it.getVideoFrameRate()
                    val nowBitRateInterval = it.getBitRateInterval()
                    LogUtil.d(
                        TAG,
                        "INTERNET_SPEED_TYPE bufsize=" + bufSize + " video_rate/8*0.8=" + now_video_rate / 8 * 0.8 + "  video_rate=" + now_video_rate + "  frame_rate=" + now_frame_rate
                    )
                    var new_video_rate = 0
                    var new_frame_rate = 0
                    //判断当前码率/8和网速,如果码率/8大于当前网速,并且两次水位值都大于20k,开始降码率
                    if (ivP2pSendInfo == null) return
                    exceedLowMark = bufSize > 20 * 1024
                    if (ivP2pSendInfo.aveSentRate < now_video_rate.toDouble() / 8 * 0.9 && exceedLowMark) {
                        // 降码率
                        new_video_rate = (now_video_rate * 0.75).toInt()
                        new_frame_rate = now_frame_rate * 4 / 5
                    } else if (bufSize < 20 * 1024) { //当前水位值小于20k,开始升码率
                        if (now_video_rate < nowBitRateInterval.last / 2) {
                            new_video_rate = (now_video_rate * 1.1).toInt()
                            new_frame_rate = now_frame_rate * 5 / 4
                        }
                    } else {
                        return
                    }
                    if (new_video_rate < nowBitRateInterval.first && now_video_rate > nowBitRateInterval.first) {
                        new_video_rate = (now_video_rate * 0.8f).toInt()
                    } else if (new_video_rate > nowBitRateInterval.last && now_video_rate < nowBitRateInterval.first) {
                        new_video_rate = (now_video_rate * 1.1f).toInt()
                    }
                    if (new_video_rate != 0) {
                        it.setVideoBitRate(new_video_rate)
                    }
                    if (new_frame_rate != 0) {
                        it.setVideoFrameRate(new_frame_rate)
                    }
                    LogUtil.d(TAG, "new_video_rate:" + new_video_rate + "  VideoBitRate:" + it.getVideoBitRate())
                }
            }
        }
    }

    private fun startBitRateAdapter() {
        bitRateTimer = Timer().apply {
            schedule(AdapterBitRateTask(), 3000, 1000)
        }
    }

    private fun stopBitRateAdapter() {
        bitRateTimer?.let {
            it.cancel()
            bitRateTimer = null
        }
    }

    fun switchCamera(surfaceTexture: SurfaceTexture?, context: Context?) {
        stopRecording(visitor, videoResType)
        closeCamera()
        mCameraId = if (mCameraId == getCameraBackFrontId()) getCameraFacingFrontId() else getCameraBackFrontId()
        openCamera(surfaceTexture, context)
        startRecording(visitor, videoResType)
    }

    fun switchCameraMirror(surfaceTexture: SurfaceTexture?, context: Context?) {
        stopRecording(visitor, videoResType)
        closeCamera()
        openCamera(surfaceTexture, context)
        startRecording(visitor, videoResType)
        turnCameraMirror()
    }

    fun release() {
        try {
            stopRecording(visitor, videoResType)
            closeCamera()
            mSaveAudioAndVideoManager?.release()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    companion object {
        private const val TAG = "CameraRecorder2"
        private const val MaxVisitors = 4
        private var bitRateTimer: Timer? = null
    }

    /**
     * 旋转 YUV 数据 90° - 用于前置摄像头
     * NV21格式: YYYY... VUVU...
     */
    fun rotateYUV90(data: ByteArray, width: Int, height: Int): ByteArray {
        val ySize = width * height
        val uvHeight = height / 2
        val uvWidth = width / 2
        val rotatedData = ByteArray(data.size)

        // 旋转 Y 平面 (height x width -> width x height)
        var i = 0
        for (x in 0 until width) {
            for (y in height - 1 downTo 0) {
                rotatedData[i++] = data[y * width + x]
            }
        }

        // 旋转 UV 平面(NV21 格式:VU 交错,高度为原图一半)
        for (x in 0 until uvWidth) {
            for (y in uvHeight - 1 downTo 0) {
                // UV平面在Y平面后,每行有width个字节(UV交错)
                val uvPos = ySize + (y * width) + (x * 2)
                if (uvPos + 1 < data.size) {
                    rotatedData[i++] = data[uvPos]     // V
                    rotatedData[i++] = data[uvPos + 1] // U
                }
            }
        }
        return rotatedData
    }

    fun rotateYUV180(data: ByteArray, width: Int, height: Int): ByteArray {
        val ySize = width * height
        val output = ByteArray(data.size)

        // Y 平面 180度旋转 = 完全倒序
        for (i in 0 until ySize) {
            output[ySize - 1 - i] = data[i]
        }

        // UV 平面 180度旋转
        val uvSize = ySize / 2
        for (i in 0 until uvSize) {
            output[ySize + uvSize - 1 - i] = data[ySize + i]
        }

        return output
    }

    fun rotateYUV270(data: ByteArray, width: Int, height: Int): ByteArray {
        val ySize = width * height
        val uvHeight = height / 2
        val uvWidth = width / 2
        val output = ByteArray(data.size)

        // Y 平面 270度旋转
        var i = 0
        for (x in width - 1 downTo 0) {
            for (y in 0 until height) {
                output[i++] = data[y * width + x]
            }
        }

        // UV 平面 270度旋转
        for (x in uvWidth - 1 downTo 0) {
            for (y in 0 until uvHeight) {
                val uvPos = ySize + (y * width) + (x * 2)
                if (uvPos + 1 < data.size) {
                    output[i++] = data[uvPos]
                    output[i++] = data[uvPos + 1]
                }
            }
        }

        return output
    }

    private fun getAverageBitRate(): Int {
        if(BuildConfig.FLAVOR=="zgll"){
            mVideoFrameRate = 30
        }
        // 根据分辨率和帧率计算合适的码率
        val baseBitRate = when {
            mVideoWidth >= 1280 || mVideoHeight >= 720 -> 2_500_000  // 720p
            mVideoWidth >= 640 || mVideoHeight >= 480 -> {
                // 480p: 15fps用1Mbps,30fps用1.5-2Mbps
                if (mVideoFrameRate >= 30) 2_000_000 else 1_500_000
            }
            else -> 800_000  // 更低分辨率
        }

        // zgll  flavor 使用更高码率确保30fps清晰度
        return if (BuildConfig.FLAVOR == "zgll") {
            (baseBitRate * 1.2).toInt().coerceAtMost(2_500_000)
        } else {
            baseBitRate
        }
    }

    /**
     * 选择最优的预览尺寸
     */
    private fun getOptimalPreviewSize(
        sizes: List<Camera.Size>,
        targetWidth: Int,
        targetHeight: Int
    ): Camera.Size? {
        if (sizes.isEmpty()) return null

        val targetRatio = targetWidth.toDouble() / targetHeight
        var optimalSize: Camera.Size? = null
        var minDiff = Double.MAX_VALUE

        for (size in sizes) {
            val ratio = size.width.toDouble() / size.height
            val diff = kotlin.math.abs(ratio - targetRatio)

            // 优先选择比例接近且分辨率不小于目标的尺寸
            if (diff < 0.15) {  // 允许 15% 的比例偏差
                val resolutionDiff = kotlin.math.abs(size.width - targetWidth) +
                        kotlin.math.abs(size.height - targetHeight)
                if (resolutionDiff < minDiff ||
                    (optimalSize == null && size.width >= targetWidth)) {
                    optimalSize = size
                    minDiff = resolutionDiff.toDouble()
                }
            }
        }

        // 如果没找到合适的,选择最大的可用尺寸
        return optimalSize ?: sizes.maxByOrNull { it.width * it.height }
    }

    private fun ensureSizeAligned(width: Int, height: Int): Pair<Int, Int> {
        // Goke 编码器通常需要 16 字节对齐
        val alignedWidth = (width + 15) / 16 * 16
        val alignedHeight = (height + 15) / 16 * 16
        return Pair(alignedWidth, alignedHeight)
    }

    /**
     * YV12 转 NV12
     * YV12: YYYYYYYY VV UU  (V在前,U在后,分开存储)
     * NV12: YYYYYYYY UVUV  (U在前,V在后,交错存储)
     */
    private fun yv12ToNv12(yv12: ByteArray, width: Int, height: Int): ByteArray {
        val ySize = width * height
        val uvSize = ySize / 4  // U和V各1/4
        val nv12 = ByteArray(yv12.size)

        // 复制 Y 平面 (前 ySize 字节)
        System.arraycopy(yv12, 0, nv12, 0, ySize)

        // 转换 UV 平面
        // YV12: V平面在[ySize, ySize+uvSize), U平面在[ySize+uvSize, ySize+2*uvSize)
        // NV12: UV交错,U在前V在后
        var uvIndex = ySize
        val vStart = ySize      // YV12的V平面开始
        val uStart = ySize + uvSize  // YV12的U平面开始

        for (i in 0 until uvSize) {
            nv12[uvIndex++] = yv12[uStart + i]  // U 先
            nv12[uvIndex++] = yv12[vStart + i]  // V 后
        }

        return nv12
    }

    /**
     * NV12 270度旋转(逆时针90度)- 用于修复顺时针旋转问题
     */
    fun rotateNV12_270(nv12: ByteArray, width: Int, height: Int): ByteArray {
        val ySize = width * height
        val uvHeight = height / 2
        val uvWidth = width / 2
        val output = ByteArray(nv12.size)

        // Y 平面 270度旋转 (width x height → height x width)
        var i = 0
        for (x in width - 1 downTo 0) {
            for (y in 0 until height) {
                output[i++] = nv12[y * width + x]
            }
        }

        // UV 平面 270度旋转(NV12的UV是交错存储的)
        val uvOutputStart = ySize
        var uvIndex = uvOutputStart
        for (x in uvWidth - 1 downTo 0) {
            for (y in 0 until uvHeight) {
                val srcPos = ySize + (y * width) + (x * 2)
                if (srcPos + 1 < nv12.size) {
                    output[uvIndex++] = nv12[srcPos]      // U
                    output[uvIndex++] = nv12[srcPos + 1]  // V
                }
            }
        }
        return output
    }

    /**
     * NV12 90度旋转
     */
    fun rotateNV12_90(nv12: ByteArray, width: Int, height: Int): ByteArray {
        val ySize = width * height
        val uvHeight = height / 2
        val uvWidth = width / 2
        val output = ByteArray(nv12.size)

        // Y 平面 90度旋转
        var i = 0
        for (x in 0 until width) {
            for (y in height - 1 downTo 0) {
                output[i++] = nv12[y * width + x]
            }
        }

        // UV 平面 90度旋转
        val uvOutputStart = ySize
        var uvIndex = uvOutputStart
        for (x in 0 until uvWidth) {
            for (y in uvHeight - 1 downTo 0) {
                val srcPos = ySize + (y * width) + (x * 2)
                if (srcPos + 1 < nv12.size) {
                    output[uvIndex++] = nv12[srcPos]      // U
                    output[uvIndex++] = nv12[srcPos + 1]  // V
                }
            }
        }

        return output
    }

    /**
     * NV12 180度旋转
     */
    fun rotateNV12_180(nv12: ByteArray, width: Int, height: Int): ByteArray {
        val ySize = width * height
        val output = ByteArray(nv12.size)

        // Y 平面 180度旋转
        for (i in 0 until ySize) {
            output[ySize - 1 - i] = nv12[i]
        }

        // UV 平面 180度旋转(交错UV需要成对交换)
        val uvSize = ySize / 2
        for (i in 0 until uvSize step 2) {
            output[ySize + uvSize - 2 - i] = nv12[ySize + i]      // U
            output[ySize + uvSize - 1 - i] = nv12[ySize + i + 1]  // V
        }

        return output
    }

    fun getAudioSessionId(): Int = mAudioEncoder?.getAudioSessionId() ?: AudioManager.AUDIO_SESSION_ID_GENERATE
}

相关推荐
plainGeekDev4 小时前
AlertDialog → DialogFragment
android·java·kotlin
Meteors.7 小时前
Kotlin协程序使用技巧和应用场景
android·开发语言·kotlin
黄林晴8 小时前
官方实战指南!Compose 项目无缝迁移 KMP
android·kotlin
plainGeekDev8 小时前
XML Shape/Selector → Kotlin 动态创建
android·java·kotlin
plainGeekDev8 小时前
Java 自定义 View → Kotlin 自定义 View
android·java·kotlin
zhangphil10 小时前
Android Coil 3 extend ImageRequest‘s custom method/function,Kotlin(2)
android·kotlin
Kapaseker10 小时前
五分钟搞定 Compose 用户名密码自动填充
android·kotlin
松仔log10 小时前
Jetpack——DataStore
java·kotlin
眸生10 小时前
基于NeteaseCloudMusicApi的音乐app 支持 DeepSeek 自然语言找歌、批量导入歌单、下载音乐转换成MP3,下载歌词
android·python·kotlin·android studio·音频·fastapi·android jetpack