Как мультиплексировать (объединять) видео и аудио, чтобы звук зацикливался в выходном видео, если оно слишком короткое по продолжительности?

Задний план

Мне нужно объединить видеофайл и аудиофайл в один видеофайл, чтобы:

  1. Выходной видеофайл будет иметь ту же продолжительность, что и входной видеофайл.
  2. Звук в выходном файле будет только из входного аудиофайла. Если он слишком короткий, он будет зацикливаться до конца (может остановиться в конце, если это необходимо). Это означает, что после того, как звук закончил воспроизводиться, а видео - нет, я должен воспроизводить его снова и снова, пока видео не закончится (объединение звука).

Насколько я читал, технический термин этой операции слияния называется «мультиплексирование».

В качестве примера предположим, что у нас есть входное видео продолжительностью 10 секунд и аудиофайл продолжительностью 4 секунды, выходное видео будет длиться 10 секунд (всегда такое же, как входное видео), а звук будет воспроизводиться 2,5 раза (первые 2 раза). охватывают первые 8 секунд, а затем 2 секунды из 4 для остальных).

Проблемы

Хотя я нашел решение, как мультиплексировать видео и аудио (здесь), Я столкнулся с несколькими проблемами:

  1. Я не могу понять, как зациклить запись аудиоконтента, когда это необходимо. У меня постоянно выдает ошибку, что бы я ни пытался

  2. Входные файлы должны иметь определенные форматы файлов. В противном случае может возникнуть исключение или (в очень редких случаях) хуже: создать видеофайл с черным содержимым. Более того: иногда файл «.mkv» (например) может подойти, а иногда он не будет принят (и оба они могут воспроизводиться в приложении видеоплеера).

  3. Текущий код обрабатывает буферы, а не реальную длительность. Это означает, что во многих случаях я могу прекратить мультиплексирование звука, даже если этого делать не следует, и выходной видеофайл будет иметь более короткий аудиоконтент по сравнению с исходным, даже если видео достаточно длинное.

Что я пробовал

  • Я попытался заставить 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. Я думаю, что поддерживаемые доступны в документации (здесь ) как те, которые помечены как "Кодировщик". Не уверен в этом. Я также не знаю, какой тип пантомимы относится к тому типу, который там упоминается.

  • Я также пытался переинициализировать все, что связано со звуком, но это тоже не сработало.

Вот мой текущий код для самого мультиплексирования (полный пример проекта доступен здесь):

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 (здесь и здесь), чтобы узнать, как это сделать. Это работало нормально, но у него есть некоторые возможные проблемы: библиотека, кажется, занимает много места, раздражает условия лицензирования, и по какой-то причине образец не может воспроизвести выходной файл, который я должен создать, если я не удалю что-то в команда, которая сделает преобразование намного медленнее. Я действительно предпочел бы использовать встроенный API, чем использовать эту библиотеку, хотя это очень мощная библиотека... Кроме того, кажется, что для некоторых входных файлов она не зацикливалась...

Вопросы

  1. Как я могу мультиплексировать видео и аудио файлы, чтобы звук зацикливался, если звук короче (по продолжительности) по сравнению с видео?

  2. Как я могу сделать так, чтобы звук обрезался именно тогда, когда видео заканчивается (без остатка ни на видео, ни на аудио)?

  3. Как я могу проверить перед вызовом этой функции, может ли текущее устройство обрабатывать данные входные файлы и фактически мультиплексировать их? Есть ли способ проверить во время выполнения, какие из них поддерживаются для такого рода операций, вместо того, чтобы полагаться на список документов, которые могут измениться в будущем?


person android developer    schedule 19.02.2019    source источник
comment
Рассматривали ли вы возможность объединения аудиофайла с самим собой, чтобы общая продолжительность была равна продолжительности видео? Если продолжительность видео в 3 раза больше аудио, объедините его в 2 раза, а затем объедините вместе. Если продолжительность 3,5 раза - соедините ее 3 раза и обрежьте разницу.   -  person TDG    schedule 23.02.2019
comment
@TDG Да, я пытался, но по какой-то причине мне это не удалось. Это идея того, что я должен делать. Я писал об этом в разделе Что я пробовал. Я пытался воссоздавать его экземпляр каждый раз, когда он заканчивается. Не работает. Кроме того, в моем коде не существует обработки времени, так как я не понимаю, как мне правильно это сделать. Он основан на буфере, а не на времени... :(   -  person android developer    schedule 23.02.2019
comment
пытался воссоздавать свой экземпляр каждый раз, когда он заканчивается - вы имеете в виду, что вы пытались сделать это в реальном времени? Потому что я говорю о подготовке объединенного аудио перед началом мультиплексирования. Извините, если я вас неправильно понял.   -  person TDG    schedule 23.02.2019
comment
@TDG Что ты имеешь в виду? Я хотел мультиплексировать звук несколько раз, чтобы он добавлялся в конец, пока не закончится видео. В любом случае, это не сработало, а если бы и сработало, я не мог знать, как сделать так, чтобы оно синхронизировалось с видео в нужное время, чтобы оно заканчивалось вместе с видео (потому что то, что я сделал, не t на основе времени, а вместо этого на основе буфера). Пожалуйста, если есть решение, напишите. Я опубликовал полный проект, который вы можете попробовать...   -  person android developer    schedule 23.02.2019
comment
Моя основная идея такова: предположим, что продолжительность видео составляет 60 секунд, а продолжительность звука — 11 секунд. Сначала соедините аудио с самим собой еще 5 раз (теперь у вас будет 66 секунд звука), а затем обрежьте дополнительные 6 секунд, чтобы продолжительность аудио и видео была одинаковой. Теперь вы можете мультиплексировать оба потока, и они имеют одинаковую длину. Я никогда раньше не пытался мультиплексировать аудио и видео, но это первое, чему я научился, когда прочитал ваш вопрос.   -  person TDG    schedule 23.02.2019
comment
@TDG Это идея, которую я пытаюсь реализовать. Опять же, мне это не удалось, поскольку важны как зацикливание, так и время. Если вы знаете, как это сделать, пожалуйста, напишите ответ. Нет нужды объяснять мою проблему...   -  person android developer    schedule 23.02.2019
comment
@TDG Поскольку вы не поняли проблему, я обновил вопрос. Надеюсь, теперь вы это поймете.   -  person android developer    schedule 24.02.2019
comment
@androiddeveloper у вас есть какое-нибудь решение?   -  person Nikunj Paradva    schedule 11.07.2019
comment
Я получил сообщение об ошибке E/MPEG4Writer: Неподдерживаемый mime 'audio/mpeg' с вашим кодом, какое-либо решение?   -  person Nikunj Paradva    schedule 11.07.2019
comment
@NikunjParadiva В моем случае мы выбираем форматы файлов, поэтому выбрали те, которые работают.   -  person android developer    schedule 11.07.2019
comment
Я использовал mp4 для видео и mp3 для аудио, но все же я получил этот. у вас есть решение для этого? как это мультить?   -  person Nikunj Paradva    schedule 11.07.2019
comment
Используйте только поддерживаемые файлы. Я думаю, что aac вместо mp3.   -  person android developer    schedule 11.07.2019


Ответы (1)


У меня такая же сцена.

  • 1: Когда audioBufferInfo.size ‹ 0, искать пуск. Но помните, вам нужно накопить presentationTimeUs.

  • 2: Получить продолжительность видео, когда звук зациклится на продолжительности (также используйте presentationTimeUs), обрезать.

  • 3: Аудиофайл должен быть MediaFormat.MIMETYPE_AUDIO_AMR_NB или MediaFormat.MIMETYPE_AUDIO_AMR_WB или MediaFormat.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)
    }
}
person lijia    schedule 19.09.2019
comment
Я думаю, вам не хватает некоторых функций, таких как getVideoDuration. Кроме того, я предлагаю избегать пути к файлу или файла, потому что на Android Q есть много ограничений... - person android developer; 19.09.2019
comment
getVideoDuration — это просто функция для получения продолжительности видеофайла, она будет использоваться при записи данных выборки аудио. - person lijia; 19.09.2019