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到本地
三、核心流程
整个通话链路如下:
- openCamera() → 打开 Camera1,设置预览尺寸、帧率、对焦、旋转角度
- onPreviewFrame() → 拿到原始 YUV 数据
- YUV 旋转 / 格式转换 → 适配设备方向与编码器要求
- encoderH264() → 送入 H.264 编码器
- startAudioEncoder() → 启动麦克风 + AAC 编码
- onVideoEncoded / onAudioEncoded → 编码完成回调
- 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 ()
这是视频数据入口,所有画面都从这里来。
核心工作:
- 判断是否正在推流
- 对 YUV 数据旋转、格式转换
- 交给
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 ()
安全释放顺序(非常重要,避免崩溃):
- 标记
isReleasing = true - 停止预览回调
- 停止编码器
- 释放 Camera
- 清空状态
kotlin
fun closeCamera() {
isReleasing = true
mCamera.setPreviewCallback(null)
mCamera.stopPreview()
mVideoEncoder.release()
mAudioEncoder.stopAudio()
mCamera.release()
}
五、亮点
- Camera1 全机型兼容(教育硬件 / 低配设备首选)
- YV12/NV21 自动转换,适配不同硬件编码器
- 多方向 YUV 旋转,解决各种设备角度问题
- 音视频分离编码、独立控制
- 支持多人同时观看(多访客)
- 动态码率自适应,弱网不掉线
- 安全释放机制,避免崩溃
- 渠道差异化配置(zgll 等设备专用)
- 本地调试录制,方便定位问题
- 对接腾讯 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
}