使用MediaCodec inputSurface与MediaMuxer录制的视频为何在华为设备无法播放?
华为设备上
MediaCodec录制视频兼容性异常问题 - 基础实现:采用
MediaCodec.inputSurface与MediaMuxer组合录制安卓视图,该功能在绝大多数非华为设备上运行正常 - 华为端异常表现:
- 生成的视频文件无法在华为设备本机播放,仅存在约1/10的概率生成可正常播放的文件
- 所有生成的视频(包括在华为端无法播放的损坏文件)均可在Mac笔记本上正常解码播放
- 问题覆盖范围:多名用户反馈该问题出现在多款华为机型上,可在HUAWEI P8 lite 2017(Android 7.0)设备上复现,搭载任意Android版本的新款华为机型也会触发同类异常
- 排查支撑:已留存损坏视频、正常视频的对照样本用于定位差异
核心录制逻辑代码
代码定义的执行流程:
- 将画布内容绘制为位图
- 提取位图像素值转换为YUV格式
- 将位图像素作为一帧写入
MediaCodec - 将
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
相关产品推荐
相关产品推荐

