背景
我需要将视频文件和音频文件合并为一个视频文件,以便:
- 输出视频文件的持续时间与输入视频文件的持续时间相同
- 输出文件中的音频将仅是输入音频文件的音频。如果太短,则会循环到最后(如果需要可以在最后停止)。这意味着一旦音频播放完毕而视频尚未播放,我应该一次又一次地播放它,直到视频结束(音频的串联)。
正如我所读到的,这种合并操作的技术术语称为“混合”。
举个例子,假设我们有一个 10 秒的输入视频和一个 4 秒的音频文件,输出视频将是 10 秒(始终与输入视频相同),音频将播放 2.5 次(前 2 次)覆盖前 8 秒,然后是其余 4 秒中的 2 秒)。
存在的问题
虽然我找到了如何混合视频和音频的解决方案(here https://stackoverflow.com/a/31591485/878126),我遇到了多个问题:
我不知道如何在需要时循环写入音频内容。无论我如何尝试,它总是给我一个错误
输入文件必须是特定的文件格式。否则,它可能会引发异常,或者(在极少数情况下)更糟糕:创建包含黑色内容的视频文件。更重要的是:有时“.mkv”文件(例如)可能没问题,有时则不会被接受(并且两者都可以在视频播放器应用程序上播放)。
当前代码处理缓冲区而不是实际持续时间。这意味着在许多情况下,我可能会停止混合音频,即使我不应该这样做,并且输出视频文件的音频内容将比原始文件更短,即使视频足够长。
我尝试过的
-
我尝试使音频的 MediaExtractor 在每次到达结尾时都转到开头,方法是:
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)
}
为了检查文件的类型,我尝试使用MediaMetadataRetriever
然后检查 mime 类型。我认为受支持的可以在文档中找到(here https://developer.android.com/guide/topics/media/media-formats)如那些标有“编码器”的。对此不太确定。我也不知道其中提到的哑剧类型属于哪种类型。
我还尝试重新初始化与音频相关的所有内容,但也不起作用。
这是我当前的多路复用本身代码(完整的示例项目可用here https://github.com/AndroidDeveloperLB/VideoAndAudioMux) :
object VideoAndAudioMuxer {
// based on: https://stackoverflow.com/a/31591485/878126
@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 库(here https://superuser.com/a/1319950/152400 and here https://github.com/umair13adil/KotlinFFMpeg/issues/11) ,看看如何做。它工作得很好,但有一些可能的问题:该库似乎占用了大量空间,烦人的许可条款,并且由于某种原因,示例无法播放我必须创建的输出文件,除非我删除了文件中的某些内容。命令将使转换速度慢得多。我真的更喜欢使用内置的 API,而不是使用这个库,即使它是一个非常强大的库...另外,似乎对于某些输入文件,它没有循环...
问题
如何混合视频和音频文件,以便在音频比视频短(持续时间)的情况下音频会循环播放?
我怎样才能做到这一点,以便在视频结束时精确地剪切音频(视频和音频上都没有剩余)?
在调用此函数之前如何检查当前设备是否可以处理给定的输入文件并实际复用它们?有没有一种方法可以在运行时检查此类操作支持哪些内容,而不是依赖将来可能会更改的文档列表?