Android音视频Muxing实现音频循环及格式兼容检测方案问询
Android原生音视频Muxing解决方案(含音频循环、精准截断与格式兼容)
针对你遇到的音视频合并问题,我基于Android原生MediaExtractor和MediaMuxerAPI,给出完整的解决方案,同时解决你提出的所有核心问题:
核心问题逐个击破
1. 实现音频循环写入
当音频Extractor读取到末尾时,需要重置到起始位置,同时累加音频的总时长到时间戳,避免Muxer因重复时间戳报错。关键是维护一个音频累计时间偏移量,每次循环时更新这个值,确保写入的音频样本时间戳是连续递增的。
2. 音频精准截断到视频时长
首先获取视频的总时长,在写入音频样本时,判断当前音频样本的结束时间是否超过视频总时长。如果超过,就调整样本的大小和结束时间,只写入到视频结束的部分,实现精准截断。
3. 预检测设备是否支持输入文件
通过MediaExtractor尝试打开目标文件,如果能成功读取轨道信息,说明设备支持该格式;另外可以结合MediaCodecList查询支持的解码器,验证文件的MIME类型是否能被处理。
4. 动态检测支持的文件类型
遍历MediaCodecList中的所有解码器,收集支持的音频/视频MIME类型;或者更直接的方式:尝试用MediaExtractor打开文件,无异常则视为支持(这种方式更贴合实际处理场景)。
完整修改后的代码
object VideoAndAudioMuxer { @WorkerThread fun joinVideoAndAudio(videoFile: File, audioFile: File, outputFile: File): Boolean { var videoExtractor: MediaExtractor? = null var audioExtractor: MediaExtractor? = null var muxer: MediaMuxer? = null try { // 1. 预检测文件是否支持 if (!isFileSupported(videoFile) || !isFileSupported(audioFile)) { return false } // 获取视频和音频的总时长(毫秒转微秒) val videoDurationUs = getMediaDurationUs(videoFile) val audioDurationUs = getMediaDurationUs(audioFile) if (videoDurationUs <= 0 || audioDurationUs <= 0) { return false } outputFile.delete() outputFile.createNewFile() muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) val sampleSize = 256 * 1024 // 初始化视频Extractor videoExtractor = MediaExtractor().apply { setDataSource(videoFile.absolutePath) selectTrack(0) seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) } val videoFormat = videoExtractor.getTrackFormat(0) val videoTrack = muxer.addTrack(videoFormat) val videoBuf = ByteBuffer.allocate(sampleSize) val videoBufferInfo = MediaCodec.BufferInfo() // 初始化音频Extractor audioExtractor = MediaExtractor().apply { setDataSource(audioFile.absolutePath) selectTrack(0) seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) } val audioFormat = audioExtractor.getTrackFormat(0) val audioTrack = muxer.addTrack(audioFormat) val audioBuf = ByteBuffer.allocate(sampleSize) val audioBufferInfo = MediaCodec.BufferInfo() muxer.start() // 维护音频累计时间偏移量,处理循环 var audioTimeOffsetUs = 0L while (true) { // 读取视频样本 videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, 0) if (videoBufferInfo.size < 0) { // 视频读取完毕,退出循环 break } // 处理音频样本,支持循环和截断 audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, 0) if (audioBufferInfo.size < 0) { // 音频读取到末尾,重置到开头并累加时间偏移 audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, 0) audioTimeOffsetUs += audioDurationUs } // 计算当前音频样本的实际时间戳 val currentAudioSampleTimeUs = audioExtractor.sampleTime + audioTimeOffsetUs // 检查是否超过视频时长,需要截断 if (currentAudioSampleTimeUs + audioBufferInfo.size * 1000L > videoDurationUs) { // 计算可写入的音频数据长度 val maxAllowedTimeUs = videoDurationUs - currentAudioSampleTimeUs val bytesPerUs = audioBufferInfo.size.toFloat() / (audioExtractor.sampleTime - (audioExtractor.sampleTime - audioBufferInfo.size * 1000L)) audioBufferInfo.size = (maxAllowedTimeUs * bytesPerUs).toInt() audioBufferInfo.flags = audioBufferInfo.flags or MediaCodec.BUFFER_FLAG_END_OF_STREAM } // 写入视频样本 videoBufferInfo.apply { presentationTimeUs = videoExtractor.sampleTime flags = videoExtractor.sampleFlags } muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo) videoExtractor.advance() // 写入音频样本 audioBufferInfo.apply { presentationTimeUs = currentAudioSampleTimeUs flags = audioExtractor.sampleFlags or audioBufferInfo.flags } muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo) audioExtractor.advance() // 如果音频已经标记为结束,后续不再处理音频,但继续写完视频 if (audioBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { audioBufferInfo.size = 0 } } muxer.stop() muxer.release() return true } catch (e: Exception) { e.printStackTrace() } finally { videoExtractor?.release() audioExtractor?.release() muxer?.release() } return false } /** * 检测文件是否被设备支持(通过MediaExtractor尝试打开) */ private fun isFileSupported(file: File): Boolean { return try { MediaExtractor().apply { setDataSource(file.absolutePath) selectTrack(0) release() } true } catch (e: Exception) { false } } /** * 获取媒体文件的总时长(微秒) */ private fun getMediaDurationUs(file: File): Long { return try { MediaMetadataRetriever().apply { setDataSource(file.absolutePath) }.use { retriever -> retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L * 1000 } } catch (e: Exception) { 0L } } /** * 动态获取设备支持的所有音视频MIME类型 */ fun getSupportedMediaTypes(): Pair<List<String>, List<String>> { val videoTypes = mutableListOf<String>() val audioTypes = mutableListOf<String>() val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) codecList.codecInfos.forEach { codecInfo -> if (!codecInfo.isEncoder) { // 解码器对应可处理的输入格式 codecInfo.supportedTypes.forEach { mimeType -> when { mimeType.startsWith("video/") -> videoTypes.add(mimeType) mimeType.startsWith("audio/") -> audioTypes.add(mimeType) } } } } return videoTypes.distinct() to audioTypes.distinct() } }
代码关键说明
- 音频循环:通过
audioTimeOffsetUs累加每次循环的音频时长,确保时间戳连续,避免Muxer报错; - 精准截断:计算音频样本的结束时间,若超过视频时长则调整样本大小,同时标记
BUFFER_FLAG_END_OF_STREAM; - 格式检测:
isFileSupported方法通过尝试初始化MediaExtractor判断文件是否可处理; - 动态支持类型:
getSupportedMediaTypes遍历解码器,收集所有支持的音视频MIME类型。
内容的提问来源于stack exchange,提问作者android developer
相关产品推荐
相关产品推荐

