You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 03:54:06