You need to enable JavaScript to run this app. 使用MediaCodec inputSurface与MediaMuxer录制的视频为何在华为设备无法播放?
优惠活动
大模型
产品
解决方案
定价
更多

使用MediaCodec inputSurface与MediaMuxer录制的视频为何在华为设备无法播放?

华为设备上MediaCodec录制视频兼容性异常问题
  • 基础实现:采用MediaCodec.inputSurface与MediaMuxer组合录制安卓视图,该功能在绝大多数非华为设备上运行正常
  • 华为端异常表现:
    • 生成的视频文件无法在华为设备本机播放,仅存在约1/10的概率生成可正常播放的文件
    • 所有生成的视频(包括在华为端无法播放的损坏文件)均可在Mac笔记本上正常解码播放
  • 问题覆盖范围:多名用户反馈该问题出现在多款华为机型上,可在HUAWEI P8 lite 2017(Android 7.0)设备上复现,搭载任意Android版本的新款华为机型也会触发同类异常
  • 排查支撑:已留存损坏视频、正常视频的对照样本用于定位差异

核心录制逻辑代码

代码定义的执行流程:

  1. 将画布内容绘制为位图
  2. 提取位图像素值转换为YUV格式
  3. 将位图像素作为一帧写入MediaCodec
  4. 将MediaCodec编码输出的数据写入MediaMuxer,最终生成视频文件
/**
 * Stages:
 * 1. Draw canvas to bitmap
 * 2. Take bitmap pixels and convert them to YUV
 * 3. Write bitmap pixels as a frame to MediaCodec
 * 4. Take mediaCodec and write to mediaMuxer to receive file
 */

class VideoEncoder(
    val width: Int,
    val height: Int,
    val frameRate: Int,
    val file: File,
    val durationUs: Long,
    val handler: Handler,
    val videoRecordingFinished: () -> Unit,
    val onError: (MediaCodec.CodecException) -> Unit
) : KoinComponent {

    var mediaMuxer: MediaMuxer? = null
    var videoCodec: MediaCodec? = null
    var videoTrackIndex = 0

    var surface: Surface? = null
    val videoBufferInfo by lazy { MediaCodec.BufferInfo() }
    var writingVideoFinished = false

    private var currentFrame = 0

    var audioEncoder: AudioEncoder? = null

    var writingAudioFinished: Boolean
        get() = audioEncoder?.writingAudioFinished ?: true
        set(value) {
            audioEncoder?.writingAudioFinished = value
        }

    var videoFormatInited: Boolean = false

    val allFormatsInited: Boolean
        get() = videoFormatInited && (audioEncoder?.audioFormatInited != false)


    private val pendingVEncoderInfos = LinkedList<MediaCodec.BufferInfo>()
    private val pendingVEncoderIndices = LinkedList<Int>()

    val logger: KLogger by inject {
        parametersOf("video-encoder")
    }

    private fun createVideoFormat(mimeType: String, desiredColorFormat: Int): MediaFormat {
        val mediaFormat =
            MediaFormat.createVideoFormat(mimeType, width, height)
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, ENCODING_VIDEO_BITRATE)
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, this.frameRate)
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, desiredColorFormat)

        return mediaFormat
    }

    private fun findCorrectVideoFormat(): MediaFormat {

        val mimeType = POSSIBLE_MIME_TYPES[0]
        val desiredColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
        val mediaFormat = createVideoFormat(mimeType, desiredColorFormat)

        val encoderForFormat =
            MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(mediaFormat)

        if (encoderForFormat == null) {
            logger.info { "encoderForFormatIsNull!!! width = $width, height = $height" }

            videoCodec = MediaCodec.createEncoderByType(mimeType)
        } else {
            videoCodec = MediaCodec.createByCodecName(encoderForFormat)
        }
        val codecInfo = videoCodec!!.codecInfo

        if (codecInfo.isEncoder && codecInfo.supportedTypes.contains(mimeType) &&
            codecInfo.getCapabilitiesForType(mimeType).colorFormats
                .contains(desiredColorFormat)
        ) {

        } else {
            throw IllegalStateException("MediaCodec is wrong = ${codecInfo}")
        }

        val errorMessage = checkIsColorFormatSupported(mediaFormat, desiredColorFormat, mimeType)
        if (errorMessage != null)
            throw IllegalStateException(errorMessage)

        return mediaFormat
    }

    //return error message if false
    fun checkIsColorFormatSupported(
        mediaFormat: MediaFormat,
        desiredColorFormat: Int,
        mimeType: String
    ): String? {
        var colorFormats = videoCodec!!.codecInfo.getCapabilitiesForType(mimeType).colorFormats
        var colorFormatSize = colorFormats.size
        var counterColorFormat = 0
        val colorFormatCorrect: Boolean
        while (true) {
            if (counterColorFormat >= colorFormatSize) {
                colorFormatCorrect = false
                break
            }
            if (colorFormats[counterColorFormat] == desiredColorFormat) {
                colorFormatCorrect = true
                break
            }
            ++counterColorFormat
        }

        if (!colorFormatCorrect) {
            var message = "NO COLOR FORMAT COMPATIBLE\\n$mediaFormat"
            colorFormats = videoCodec!!.codecInfo.getCapabilitiesForType(mimeType).colorFormats
            colorFormatSize = colorFormats.size
            counterColorFormat = 0
            while (counterColorFormat < colorFormatSize) {
                val sb = StringBuilder()
                sb.append(message)
                sb.append("\\n")
                sb.append(colorFormats[counterColorFormat])
                message = sb.toString()
                logger.debug { message }
                ++counterColorFormat
            }
            return message
        }

        return null

    }

    private fun printVideoCodecInfo() {
        logger.debug {
            val json = JSONObject()
            json.put("codec_name", videoCodec!!.name)
            json.put("codec_info_name", videoCodec!!.codecInfo.name)
            json.put("codec_supported_types", videoCodec!!.codecInfo.supportedTypes)
            json.put("output_width", width)
            json.put("output_height", height)
            json.toString()
        }
    }

    @Throws(Exception::class)
    fun initialize(videoAsyncEncoder: Boolean) {

        val filePath = file.canonicalPath

        val mediaFormat = findCorrectVideoFormat()

        printVideoCodecInfo()

        if (videoAsyncEncoder) {
            videoCodec!!.setCallback(object : MediaCodec.Callback() {
                override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {

                }

                override fun onOutputBufferAvailable(
                    codec: MediaCodec,
                    index: Int,
                    info: MediaCodec.BufferInfo
                ) {

                    pendingVEncoderIndices.add(index)
                    pendingVEncoderInfos.add(info)

                    if (allFormatsInited)
                        checkVideoOutputAvailable()
                }

                override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
                    writingVideoFinished = true
                    e.printDebug()
                    onError.invoke(e)
                }

                override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
                    onVideoFormatChanged(format)
                }

            }, handler)
        }

        videoCodec!!.configure(
            mediaFormat, null, null,
            MediaCodec.CONFIGURE_FLAG_ENCODE
        )

        surface = videoCodec!!.createInputSurface()
        mediaMuxer = MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
    }

    fun initAudio(
        path: String,
        startTimeUs: Long,
        volume: Int,
        audioRecordingFinished: () -> Unit
    ) {
        audioEncoder = AudioEncoder(
            mediaMuxer!!,
            handler,
            durationUs,
            ::checkFormatsInited,
            audioRecordingFinished
        )
        audioEncoder!!.initAudio(path, startTimeUs, volume)
        audioEncoder!!.startAudioCodec()
    }

    fun canWriteAudio() {
        audioEncoder?.canWriteAudio()
    }

    fun getCurrentAudioTime() = audioEncoder?.getCurrentAudioTime()

    private fun onVideoFormatChanged(format: MediaFormat) {
        videoTrackIndex =
            mediaMuxer!!.addTrack(format)
        videoFormatInited = true
        checkFormatsInited()
    }


    fun checkFormatsInited() {
        if (allFormatsInited) {
            mediaMuxer!!.start()
            checkVideoOutputAvailable()
        }
    }


    @Throws(IllegalStateException::class)
    fun writeToMuxerSyncMode(currentFrame: Int = -1): Boolean {
        var success = false

        while (videoCodec != null && mediaMuxer != null) {

            val outputBufferIndex = videoCodec!!.dequeueOutputBuffer(videoBufferInfo, 0L)

            logger.info {
                "writeToMuxer, outputBufferIndex = ${outputBufferIndex}, bufferFlag = ${videoBufferInfo.flags}," +
                        " presentationTime = ${((currentFrame * 1000000L) / frameRate)}," +
                        " bufferInfo.size ${videoBufferInfo.size}, bufferInfo.offset ${videoBufferInfo.offset}"
            }

            if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                onVideoFormatChanged(videoCodec!!.outputFormat)
            } else {
                if (outputBufferIndex < 0) {
                    return success
                }
                success = true

                val bufferInfo = videoBufferInfo

                if (bufferInfo.offset >= 0 && bufferInfo.size > 0) {
                    val outputBuffer = videoCodec!!.getOutputBuffer(outputBufferIndex)!!

                    outputBuffer.position(this.videoBufferInfo.offset)
                    outputBuffer.limit(bufferInfo.offset + bufferInfo.size)

                    if (currentFrame != -1) {
                        if (videoBufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG)
                            success = false
                        else
                            bufferInfo.presentationTimeUs = (currentFrame * 1000000L) / frameRate
                    }

                    mediaMuxer!!.writeSampleData(
                        videoTrackIndex,
                        outputBuffer,
                        this.videoBufferInfo
                    )
                }
                videoCodec!!.releaseOutputBuffer(outputBufferIndex, false)

                if (bufferInfo.flags.and(MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    return success
                }
            }
        }
        return success
    }

    private fun onVideoWritingFinished() {
        writingVideoFinished = true
        videoRecordingFinished.invoke()
    }

    private fun checkVideoOutputAvailable() {
        while (pendingVEncoderIndices.size > 0 &&
            pendingVEncoderInfos.size > 0 && videoCodec != null
        ) {

            val index = pendingVEncoderIndices.removeFirst()
            val info = pendingVEncoderInfos.removeFirst()

            onVideoOutputAvailable(videoCodec!!, index, info)
        }
    }

    private fun onVideoOutputAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {

        if (videoCodec == null)
            return

        if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {

            codec.releaseOutputBuffer(index, false)
            onVideoWritingFinished()

        } else {
            val outputBuffer = codec.getOutputBuffer(index)!!

            outputBuffer.position(info.offset)
            outputBuffer.limit(info.offset + info.size)

            info.presentationTimeUs = (currentFrame * 1000000L) / frameRate

            if (info.flags != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                currentFrame++
            }

            logger.info {
                "videoOutputAvailable time ${info.presentationTimeUs}, flags ${info.flags}," +
                        " size ${info.size}, offset ${info.offset}"
            }

            mediaMuxer!!.writeSampleData(
                videoTrackIndex,
                outputBuffer, info
            )

            codec.releaseOutputBuffer(index, false)
        }
    }

    fun startVideoCodec() {
        videoCodec?.start()
    }

    fun stop() {

        audioEncoder?.stop()

        pendingVEncoderInfos.clear()
        pendingVEncoderIndices.clear()
        surface?.release()
        surface = null

        if (videoCodec != null) {
            try {
                videoCodec?.stop()
            } catch (e: IllegalStateException) {
            } finally {
                videoCodec?.release()
                videoCodec = null
            }
        }
        if (mediaMuxer != null) {
            try {
                mediaMuxer?.release()
            } catch (e: IllegalStateException) {
                logger.error(e)
            } finally {
                mediaMuxer = null
            }
        }
    }

    fun sendEndOfStreamSurface() {
        videoCodec?.signalEndOfInputStream()
        if (!ThreadRecord.VIDEO_CODEC_ASYNC) {
            onVideoWritingFinished()
        }
    }

    companion object {
        const val ENCODING_VIDEO_BITRATE = 12000000
        val POSSIBLE_MIME_TYPES = arrayOf("video/avc", "video/hevc", "video/x-vnd.on2.vp8")
    }
}

内容的提问来源于stack exchange,提问作者vkrott

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.23 12:36:03