如何将视频和音频混合(合并),使得在输出视频中,如果音频时长太短,音频会循环播放?

6

背景

我需要将一个视频文件和一个音频文件合并成一个单独的视频文件,以便:

  1. 输出视频文件与输入视频文件具有相同的持续时间
  2. 输出文件中的音频仅为输入音频文件。如果太短,它将循环到结尾(如果需要可以停止在结尾)。这意味着一旦音频播放完,而视频没有完成,则应重复播放,直到视频结束(音频串联)。

这种合并操作的技术术语称为“muxing”,据我所知。

例如,假设我们有一个10秒的输入视频和一个4秒的音频文件,则输出视频的长度为10秒(始终与输入视频相同),音频将播放2.5次(前两次覆盖了前8秒,然后是剩余4秒中的2秒)。

问题

虽然我已经找到了如何混合视频和音频的解决方法(here),但我遇到了多个问题:

  1. 我无法弄清楚如何在需要时循环写入音频内容。无论我尝试什么,它都会给我一个错误。

  2. 输入文件必须是特定的文件格式。否则,它可能会抛出异常,或者(在非常罕见的情况下)更糟糕:创建一个具有黑色内容的视频文件。更多地是:有时'.mkv'文件(例如)可能很好,有时可能不被接受(而两个都可以在视频播放器应用中播放)。

  3. 当前代码处理缓冲区而不是实际持续时间。这意味着在许多情况下,我可能会停止混合音频,尽管我不应该停止,输出视频文件将比原始文件的音频内容更短,即使视频足够长。

我尝试过的事情

  • I tried to make the MediaExtractor of the audio to go to its beginning each time it reached the end, by using:

            if (audioBufferInfo.size < 0) {
                Log.d("AppLog", "reached end of audio, looping...")
                audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
                audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, 0)
            }
    
  • For checking the types of the files, I tried using MediaMetadataRetriever and then checking the mime-type. I think the supported ones are available on the docs (here) as those marked with "Encoder". Not sure about this. I also don't know which mime type is of which type that is mentioned there.

  • I also tried to re-initialize all that's related to the audio, but it didn't work either.

这是我目前用于复用的代码(完整示例项目可在此处获取):
object VideoAndAudioMuxer {
    //   based on:  https://dev59.com/nY3da4cB1Zd3GeqPx0ti#31591485
    @WorkerThread
    fun joinVideoAndAudio(videoFile: File, audioFile: File, outputFile: File): Boolean {
        try {
            //            val videoMediaMetadataRetriever = MediaMetadataRetriever()
            //            videoMediaMetadataRetriever.setDataSource(videoFile.absolutePath)
            //            val videoDurationInMs =
            //                videoMediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
            //            val videoMimeType =
            //                videoMediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
            //            val audioMediaMetadataRetriever = MediaMetadataRetriever()
            //            audioMediaMetadataRetriever.setDataSource(audioFile.absolutePath)
            //            val audioDurationInMs =
            //                audioMediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
            //            val audioMimeType =
            //                audioMediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
            //            Log.d(
            //                "AppLog",
            //                "videoDuration:$videoDurationInMs audioDuration:$audioDurationInMs videoMimeType:$videoMimeType audioMimeType:$audioMimeType"
            //            )
            //            videoMediaMetadataRetriever.release()
            //            audioMediaMetadataRetriever.release()
            outputFile.delete()
            outputFile.createNewFile()
            val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
            val sampleSize = 256 * 1024
            //video
            val videoExtractor = MediaExtractor()
            videoExtractor.setDataSource(videoFile.absolutePath)
            videoExtractor.selectTrack(0)
            videoExtractor.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()
//            Log.d("AppLog", "Video Format $videoFormat")
            //audio
            val audioExtractor = MediaExtractor()
            audioExtractor.setDataSource(audioFile.absolutePath)
            audioExtractor.selectTrack(0)
            audioExtractor.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()
//            Log.d("AppLog", "Audio Format $audioFormat")
            //
            muxer.start()
//            Log.d("AppLog", "muxing video&audio...")
            //            val minimalDurationInMs = Math.min(videoDurationInMs, audioDurationInMs)
            while (true) {
                videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, 0)
                audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, 0)
                if (audioBufferInfo.size < 0) {
                    //                    Log.d("AppLog", "reached end of audio, looping...")
                    //TODO somehow start from beginning of the audio again, for looping till the video ends
                    //                    audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
                    //                    audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, 0)
                }
                if (videoBufferInfo.size < 0 || audioBufferInfo.size < 0) {
//                    Log.d("AppLog", "reached end of video")
                    videoBufferInfo.size = 0
                    audioBufferInfo.size = 0
                    break
                } else {
                    //                    val donePercentage = videoExtractor.sampleTime / minimalDurationInMs / 10L
                    //                    Log.d("AppLog", "$donePercentage")
                    // video muxing
                    videoBufferInfo.presentationTimeUs = videoExtractor.sampleTime
                    videoBufferInfo.flags = videoExtractor.sampleFlags
                    muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo)
                    videoExtractor.advance()
                    // audio muxing
                    audioBufferInfo.presentationTimeUs = audioExtractor.sampleTime
                    audioBufferInfo.flags = audioExtractor.sampleFlags
                    muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo)
                    audioExtractor.advance()
                }
            }
            muxer.stop()
            muxer.release()
//            Log.d("AppLog", "success")
            return true
        } catch (e: Exception) {
            e.printStackTrace()
//            Log.d("AppLog", "Error " + e.message)
        }
        return false
    }
}
  • 我也尝试过使用FFMPEG库(这里这里),看看如何操作。它可以正常工作,但可能存在一些问题:该库似乎占用了大量空间,许可条款令人烦恼,并且由于某种原因,除非我删除命令中会使转换变慢的某些内容,否则样本无法播放我创建的输出文件。即使它是一个非常强大的库,我仍然更喜欢使用内置API而不是使用此库...另外,似乎对于某些输入文件,它没有循环...

问题

  1. 如何混合视频和音频文件,以便在音频持续时间较短(比视频)时音频将循环播放?

  2. 如何确保音频在视频结束时精确剪切(视频和音频都没有剩余)?

  3. 在调用此函数之前,如何检查当前设备是否能够处理给定的输入文件并实际混合它们?有没有一种方法可以在运行时检查支持此类操作的文件类型,而不是依赖于文档中可能会更改的列表?


@TDG 是的,我尝试了,但是因为某些原因失败了。这就是我应该做的事情的想法。我在“What I've tried”中写了一些关于它的内容。我尝试每次结束时重新创建它的实例。但没有成功。此外,我的代码中不存在时间处理,因为我不知道应该如何正确地处理它。它基于缓冲区而不是时间… :( - android developer
“每次结束时都试图重新创建其实例” - 您的意思是您尝试在实时中执行此操作吗?因为我所说的是在开始混合之前准备连接的音频。如果我误解了您,请原谅。 - TDG
@TDG 你是什么意思?我想多次混合音频,这样它就会一直添加到视频结束。无论如何,它都没有起作用,即使它起作用了,我也不知道如何在正确的时间与视频同步,以便它能够与视频同时结束(因为我所做的不是基于时间,而是基于缓冲区)。如果您有解决方案,请写下来。我已经发布了一个完整的项目,您可以尝试一下... - android developer
@TDG 这就是我想要实现的想法。但是,无论是循环还是时间控制,我都没有成功。如果您知道如何实现,请写下答案。您不需要解释我的问题... - android developer
@TDG,由于您没有理解问题,我已经更新了问题。希望您现在能够理解它。 - android developer
显示剩余7条评论
1个回答

2
我有相同的场景。
1. 当 audioBufferInfo.size < 0 时,要跳转到开头。但请记住,您需要累加 presentationTimeUs。 2. 获取视频持续时间,当音频循环到持续时间(也使用 presentationTimeUs),则剪切。 3. 音频文件需要是 MediaFormat.MIMETYPE_AUDIO_AMR_NBMediaFormat.MIMETYPE_AUDIO_AMR_WBMediaFormat.MIMETYPE_AUDIO_AAC。在我的测试机器上,它可以正常工作。
以下是代码:
private fun muxing(musicName: String) {
    val saveFile = File(DirUtils.getPublicMediaPath(), "$saveName.mp4")
    if (saveFile.exists()) {
        saveFile.delete()
        PhotoHelper.sendMediaScannerBroadcast(saveFile)
    }
    try {
        // get the video file duration in microseconds
        val duration = getVideoDuration(mSaveFile!!.absolutePath)

        saveFile.createNewFile()

        val videoExtractor = MediaExtractor()
        videoExtractor.setDataSource(mSaveFile!!.absolutePath)

        val audioExtractor = MediaExtractor()
        val afdd = MucangConfig.getContext().assets.openFd(musicName)
        audioExtractor.setDataSource(afdd.fileDescriptor, afdd.startOffset, afdd.length)

        val muxer = MediaMuxer(saveFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)

        videoExtractor.selectTrack(0)
        val videoFormat = videoExtractor.getTrackFormat(0)
        val videoTrack = muxer.addTrack(videoFormat)

        audioExtractor.selectTrack(0)
        val audioFormat = audioExtractor.getTrackFormat(0)
        val audioTrack = muxer.addTrack(audioFormat)

        var sawEOS = false
        val offset = 100
        val sampleSize = 1000 * 1024
        val videoBuf = ByteBuffer.allocate(sampleSize)
        val audioBuf = ByteBuffer.allocate(sampleSize)
        val videoBufferInfo = MediaCodec.BufferInfo()
        val audioBufferInfo = MediaCodec.BufferInfo()

        videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
        audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)

        muxer.start()

        val frameRate = videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE)
        val videoSampleTime = 1000 * 1000 / frameRate

        while (!sawEOS) {
            videoBufferInfo.offset = offset
            videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset)

            if (videoBufferInfo.size < 0) {
                sawEOS = true
                videoBufferInfo.size = 0

            } else {
                videoBufferInfo.presentationTimeUs += videoSampleTime
                videoBufferInfo.flags = videoExtractor.sampleFlags
                muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo)
                videoExtractor.advance()
            }
        }

        var sawEOS2 = false
        var sampleTime = 0L
        while (!sawEOS2) {

            audioBufferInfo.offset = offset
            audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset)

            if (audioBufferInfo.presentationTimeUs >= duration) {
                sawEOS2 = true
                audioBufferInfo.size = 0
            } else {
                if (audioBufferInfo.size < 0) {
                    sampleTime = audioBufferInfo.presentationTimeUs
                    audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
                    continue
                }
            }
            audioBufferInfo.presentationTimeUs = audioExtractor.sampleTime + sampleTime
            audioBufferInfo.flags = audioExtractor.sampleFlags
            muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo)
            audioExtractor.advance()
        }

        muxer.stop()
        muxer.release()
        videoExtractor.release()
        audioExtractor.release()
        afdd.close()
    } catch (e: Exception) {
        LogUtils.e(TAG, "Mixer Error:" + e.message)
    }
}

我认为你缺少一些函数,比如“getVideoDuration”。另外,我建议避免使用文件路径或File,因为在Android Q上它有很多限制... - android developer
getVideoDuration 是一个获取视频文件时长的函数,它将用于写入音频样本数据。 - lijia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接