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

使用Android MediaMuxer截取带音频视频失败:音频无法嵌入

Android MediaMuxer导出带声音视频片段的问题修复

你的代码存在几个核心问题,导致音频无法正确写入导出的视频文件,下面是问题分析和修复方案:

问题根源

  1. 多轨道样本读取混乱:MediaExtractor同时选中音视频轨道时,readSampleData只能读取当前单个轨道的样本,但你把同一个buffer同时写给音视频轨道,相当于把视频数据重复写入音频轨道,音频数据根本没被正确读取。
  2. 硬编码样本Flags:音频样本没有关键帧概念,但你硬设置bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME,导致播放器无法解析音频流。
  3. 时间戳未适配:原视频的时间戳直接写入,播放器可能因时间戳起始值过大无法识别轨道。
  4. 资源未释放:MediaMetadataRetriever使用后未释放,存在内存泄漏风险。

修复后的完整代码

private fun splitVideo(inputPath: String, outputPath: String, startTimeMs: Long, endTimeMs: Long) {
    val outputFile = File(outputPath)
    if (outputFile.exists()) {
        outputFile.delete()
    }

    val mediaExtractor = MediaExtractor()
    val mediaMuxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
    try {
        mediaExtractor.setDataSource(inputPath)

        // 处理视频方向和尺寸
        var newVideoWidth = 0
        var newVideoHeight = 0
        val inputVideoRetriever = MediaMetadataRetriever()
        inputVideoRetriever.setDataSource(inputPath)
        val rotation = inputVideoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
        if (rotation == null || rotation == "0" || rotation == "180") {
            newVideoWidth = inputVideoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0
            newVideoHeight = inputVideoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 0
        } else {
            newVideoWidth = inputVideoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: 0
            newVideoHeight = inputVideoRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: 0
        }
        val orientationHint = when (rotation) {
            "90" -> 90
            "270" -> 270
            else -> 0
        }
        mediaMuxer.setOrientationHint(orientationHint)
        inputVideoRetriever.release() // 释放资源

        // 获取音视频轨道索引
        val videoTrackIndex = selectTrack(mediaExtractor, "video/")
        val audioTrackIndex = selectTrack(mediaExtractor, "audio/")
        println("#20230807X videoTrackIndex: $videoTrackIndex")
        println("#20230807X audioTrackIndex: $audioTrackIndex")

        val startTimeUs = startTimeMs * 1000
        val endTimeUs = endTimeMs * 1000

        // 原轨道索引 -> Muxer轨道索引的映射
        val trackMap = mutableMapOf<Int, Int>()
        val buffer = ByteBuffer.allocate(2 * 1024 * 1024)
        val bufferInfo = MediaCodec.BufferInfo()

        // 配置要处理的轨道
        val tracksToProcess = mutableListOf<Int>()
        if (videoTrackIndex >= 0) {
            tracksToProcess.add(videoTrackIndex)
            val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)
            if (newVideoWidth > 0 && newVideoHeight > 0) {
                videoFormat.setInteger(MediaFormat.KEY_WIDTH, newVideoWidth)
                videoFormat.setInteger(MediaFormat.KEY_HEIGHT, newVideoHeight)
            }
            trackMap[videoTrackIndex] = mediaMuxer.addTrack(videoFormat)
        }
        if (audioTrackIndex >= 0) {
            tracksToProcess.add(audioTrackIndex)
            val audioFormat = mediaExtractor.getTrackFormat(audioTrackIndex)
            trackMap[audioTrackIndex] = mediaMuxer.addTrack(audioFormat)
        }

        if (tracksToProcess.isEmpty()) {
            throw IllegalArgumentException("No valid tracks found in input video")
        }

        mediaMuxer.start()

        // 单独处理每个轨道的样本
        for (trackIndex in tracksToProcess) {
            mediaExtractor.selectTrack(trackIndex)
            mediaExtractor.seekTo(startTimeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
            var firstSampleTimeUs = -1L

            while (true) {
                val sampleSize = mediaExtractor.readSampleData(buffer, 0)
                if (sampleSize < 0) break

                val currentSampleTimeUs = mediaExtractor.sampleTime
                if (currentSampleTimeUs > endTimeUs) break

                // 计算相对时间戳,从0开始
                if (firstSampleTimeUs == -1L) {
                    firstSampleTimeUs = currentSampleTimeUs
                }
                val adjustedTimeUs = currentSampleTimeUs - firstSampleTimeUs

                bufferInfo.apply {
                    presentationTimeUs = adjustedTimeUs
                    offset = 0
                    size = sampleSize
                    flags = mediaExtractor.sampleFlags // 使用原样本的Flags
                }

                // 写入对应轨道
                mediaMuxer.writeSampleData(trackMap[trackIndex]!!, buffer, bufferInfo)
                mediaExtractor.advance()
            }
            mediaExtractor.unselectTrack(trackIndex)
        }

        mediaMuxer.stop()

    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        mediaExtractor.release()
        mediaMuxer.release()
    }
}

// 轨道选择辅助方法
private fun selectTrack(extractor: MediaExtractor, mimePrefix: String): Int {
    for (i in 0 until extractor.trackCount) {
        val format = extractor.getTrackFormat(i)
        val mime = format.getString(MediaFormat.KEY_MIME) ?: continue
        if (mime.startsWith(mimePrefix)) {
            return i
        }
    }
    return -1
}

关键修复说明

  • 分开处理音视频轨道:遍历每个轨道单独读取样本,避免多轨道选中导致的样本混淆。
  • 保留原样本Flags:使用mediaExtractor.sampleFlags代替硬编码,确保音视频样本的正确标识。
  • 重计算时间戳:将样本时间戳调整为相对于片段起始时间的偏移量,从0开始计数,兼容播放器要求。
  • 释放资源:添加inputVideoRetriever.release()清理未关闭的资源。
  • 移除硬编码Duration:让MediaMuxer自动计算导出片段的时长,避免与实际样本时长冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 07:52:29