LoginSignup
9
7

More than 3 years have passed since last update.

MediaCodecで動画をExtract, Decode, Encode, Muxしてみる

Last updated at Posted at 2020-02-22

概要

Android SDK に含まれる Media Codec を用いて動画の Extract(分離), Decode, Encode, Mux(結合) し再度動画ファイルとして出力してみる

処理の全体像

動画を入力したら Extract, Decode, Encode, Mux の処理を無限ループで回していく。各処理を無限ループで回しつつ一部のデータを切り出して少しずつ並列に処理することで効率を上げる作りになっている

    fun doExtractDecodeEncodeMux() {
        videoDecoder.start()
        videoEncoder.start()
        audioDecoder.start()
        audioEncoder.start()

        while (!isMuxEnd) {
            if (!isVideoExtractEnd) { isVideoExtractEnd = extract(videoExtractor, videoDecoder) }
            if (!isAudioExtractEnd) { isAudioExtractEnd = extract(audioExtractor, audioDecoder) }
            if (!isVideoDecodeEnd) { isVideoDecodeEnd = decode(videoDecoder, videoEncoder) }
            if (!isAudioDecodeEnd) { isAudioDecodeEnd = decode(audioDecoder, audioEncoder) }
            if (!isVideoEncodeEnd) {
                isVideoEncodeEnd = encode(videoEncoder, {
                    outputVideoFormat = it
                    outputVideoTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputVideoTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
            if (!isAudioEncodeEnd) {
                isAudioEncodeEnd = encode(audioEncoder, {
                    outputAudioFormat = it
                    outputAudioTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputAudioTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
        }
        ...
    }

Extract

Extract は MediaExtractorクラスで行う。入力元動画のファイルパスを設定しておく。 Video用とAudio用をそれぞれ用意する必要がある

    private val videoExtractor = MediaExtractor()
    private val audioExtractor = MediaExtractor()

    init {
        videoExtractor.setDataSource(inputFilePath)
        videoTrackIdx = getVideoTrackIdx(videoExtractor)
        if (videoTrackIdx == -1) {
            Logger.e("video not found")
            throw RuntimeException("video not found")
        }
        videoExtractor.selectTrack(videoTrackIdx)
        ...
        Logger.e("inputVideoFormat: $inputVideoFormat")

        audioExtractor.setDataSource(inputFilePath)
        audioTrackIdx = getAudioTrackIdx(audioExtractor)
        if (audioTrackIdx == -1) {
            Logger.e("audio not found")
            throw RuntimeException("audio not found")
        }
        audioExtractor.selectTrack(audioTrackIdx)
        ...
        Logger.e("inputAudioFormat: $inputAudioFormat")

        ...
    }

    private fun getAudioTrackIdx(extractor: MediaExtractor): Int {
        for (idx in 0 until extractor.trackCount) {
            val format = extractor.getTrackFormat(idx)
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime?.startsWith("audio") == true) {
                return idx
            }
        }
        return -1
    }

    private fun getVideoTrackIdx(extractor: MediaExtractor): Int {
        for (idx in 0 until extractor.trackCount) {
            val format = extractor.getTrackFormat(idx)
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime?.startsWith("video") == true) {
                return idx
            }
        }
        return -1
    }

Extract処理では MediaExtractorreadSampleData() でデータを読み込み Decoder の inputBuffer に送る。最後まで読み込んだら送信時の flag に最後のデータであることを示す MediaCodec.BUFFER_FLAG_END_OF_STREAM をセットして送る。同時に isExtractEnd フラグをセットし、無限ループでこの処理が呼ばれないようにする

    private fun extract(extractor: MediaExtractor, decoder: MediaCodec): Boolean {
        var isExtractEnd = false
        val inputBufferIdx = decoder.dequeueInputBuffer(CODEC_TIMEOUT_IN_US)
        if (inputBufferIdx >= 0) {
            val inputBuffer = decoder.getInputBuffer(inputBufferIdx) as ByteBuffer
            val sampleSize = extractor.readSampleData(inputBuffer, 0)
            if (sampleSize > 0) {
                decoder.queueInputBuffer(inputBufferIdx, 0, sampleSize, extractor.sampleTime, extractor.sampleFlags)
            } else {
                Logger.e("isExtractEnd = true")
                isExtractEnd = true
                decoder.queueInputBuffer(inputBufferIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
            }

            if (!isExtractEnd) {
                extractor.advance()
            }
        }
        return isExtractEnd
    }

Decode

Decodeは MediaCodec クラスで行う。 MediaCodec.createDecoderByType() で生成し、 configure() で各種パラメータをセットする。今回は入力と出力で同じ動画なので入力元動画のパラメータをそのままセットしている

    private val videoDecoder: MediaCodec
    private val audioDecoder: MediaCodec

    init {
        ...
        inputVideoFormat = videoExtractor.getTrackFormat(videoTrackIdx)
        ...
        videoDecoder = MediaCodec.createDecoderByType(inputVideoMime)
        videoDecoder.configure(inputVideoFormat, null, null, 0)

        ...
        inputAudioFormat = audioExtractor.getTrackFormat(audioTrackIdx)
        ...
        audioDecoder = MediaCodec.createDecoderByType(inputAudioMime)
        audioDecoder.configure(inputAudioFormat, null, null, 0)
        ...
    }

Decode処理では、Decoder の outputBuffer からデコードされたデータを取り出し今度は Encoder の inputBuffer へ送る

今回は入力動画と出力動画が同じものなのでそのまま decode 結果を encode して送ってしまうが、このタイミングで生のデータを加工し例えば動画にフィルターをかけたり、音声データのサンプリングレートを変換したりなどの面白いことが行える

最後まで処理したら Extract 時と同様に送信時の flag に最後のデータであることを示す MediaCodec.BUFFER_FLAG_END_OF_STREAM をセットして送る。同時に isDecodeEnd フラグをセットし、無限ループでこの処理が呼ばれないようにする

    private fun decode(decoder: MediaCodec, encoder: MediaCodec): Boolean {
        var isDecodeEnd = false
        val decoderOutputBufferInfo = MediaCodec.BufferInfo()
        val decoderOutputBufferIdx = decoder.dequeueOutputBuffer(decoderOutputBufferInfo, CODEC_TIMEOUT_IN_US)

        if (decoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
            Logger.e("isDecodeEnd = true")
            isDecodeEnd = true
        }
        if (decoderOutputBufferIdx >= 0) {
            val encoderInputBufferIdx = encoder.dequeueInputBuffer(CODEC_TIMEOUT_IN_US)
            if (encoderInputBufferIdx >= 0) {
                val decoderOutputBuffer = (decoder.getOutputBuffer(decoderOutputBufferIdx) as ByteBuffer).duplicate()
                decoderOutputBuffer.position(decoderOutputBufferInfo.offset)
                decoderOutputBuffer.limit(decoderOutputBufferInfo.offset + decoderOutputBufferInfo.size)

                val encoderInputBuffer = encoder.getInputBuffer(encoderInputBufferIdx)
                encoderInputBuffer?.position(0)
                encoderInputBuffer?.put(decoderOutputBuffer)

                val flags = if (isDecodeEnd) MediaCodec.BUFFER_FLAG_END_OF_STREAM else decoderOutputBufferInfo.flags

                encoder.queueInputBuffer(
                    encoderInputBufferIdx, 0,
                    decoderOutputBufferInfo.size,
                    decoderOutputBufferInfo.presentationTimeUs, flags
                )
                decoder.releaseOutputBuffer(decoderOutputBufferIdx, false)
            }
        }
        return isDecodeEnd
    }

Encode

Encode も MediaCodec クラスで行う。 configure() で各種パラメータをセットする。可能な限り入力元動画から取得したパラメータをセットしたが取得できないものがあるのでそこは別途指定していく。 configure() の第4引数は Decode の時とは違い MediaCodec.CONFIGURE_FLAG_ENCODE をセットする

    private val videoEncoder: MediaCodec
    private val audioEncoder: MediaCodec

    init {
        ...
        val inputVideoMime = inputVideoFormat.getString(MediaFormat.KEY_MIME)
        val width = inputVideoFormat.getInteger(MediaFormat.KEY_WIDTH)
        val height = inputVideoFormat.getInteger(MediaFormat.KEY_HEIGHT)
        encodeVideoFormat = MediaFormat.createVideoFormat(inputVideoMime, width, height).also {
            it.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
            it.setInteger(MediaFormat.KEY_BIT_RATE, 2000000)
            it.setInteger(MediaFormat.KEY_FRAME_RATE, inputVideoFormat.getInteger(MediaFormat.KEY_FRAME_RATE))
            it.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10)
        }
        videoEncoder = MediaCodec.createEncoderByType(inputVideoMime)
        videoEncoder.configure(encodeVideoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

        val inputAudioMime = inputAudioFormat.getString(MediaFormat.KEY_MIME)
        val sampleRate = inputAudioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channelCount = inputAudioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        encodeAudioFormat = MediaFormat.createAudioFormat(inputAudioMime, sampleRate, channelCount).also {
            it.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
            it.setInteger(MediaFormat.KEY_BIT_RATE, inputAudioFormat.getInteger(MediaFormat.KEY_BIT_RATE))
        }
        audioEncoder = MediaCodec.createEncoderByType(inputAudioMime)
        audioEncoder.configure(encodeAudioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        ...
    }

Encode処理では、Encoder の outputBuffer からエンコードされたデータを取り出し今度は Muxer へ送る

    private fun encode(
        encoder: MediaCodec,
        onOutputFormatChaned: (outputFormat: MediaFormat) -> Unit,
        writeEncodedData: (outputBuffer: ByteBuffer, outputBufferInfo: MediaCodec.BufferInfo) -> Unit
    ): Boolean {
        var isEncodeEnd = false
        val encoderOutputBufferInfo = MediaCodec.BufferInfo()
        val encoderOutputBufferIdx = encoder.dequeueOutputBuffer(encoderOutputBufferInfo, CODEC_TIMEOUT_IN_US)

        if (encoderOutputBufferIdx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            Logger.e("output format changed: ${encoder.outputFormat}")
            onOutputFormatChaned(encoder.outputFormat)
            if (outputVideoFormat != null && outputAudioFormat != null) {
                Logger.e("muxer start")
                muxer.start()
            }
            return isEncodeEnd
        }

        if (encoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
            encoder.releaseOutputBuffer(encoderOutputBufferIdx, false)
            return isEncodeEnd
        }

        outputVideoFormat?: return isEncodeEnd
        outputAudioFormat?: return isEncodeEnd

        if (encoderOutputBufferIdx >= 0) {
            val encoderOutputBuffer = encoder.getOutputBuffer(encoderOutputBufferIdx)
            if (encoderOutputBufferInfo.size >= 0) {
                encoderOutputBuffer?.let {
                    writeEncodedData(it, encoderOutputBufferInfo)
                    encoder.releaseOutputBuffer(encoderOutputBufferIdx, false)
                }
            }
            Logger.e("presentationTimeUs: ${encoderOutputBufferInfo.presentationTimeUs}, encoder: ${encoder.codecInfo.name}")
        }

        if (encoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
            Logger.e("isEncodeEnd = true")
            isEncodeEnd = true
        }
        return isEncodeEnd
    }

ここで注意しないといけないのが MediaCodec.INFO_OUTPUT_FORMAT_CHANGED を受け取り動画と音声両方の outputFormat が確定してからじゃないと Muxer にデータが書き込めないということである。 そのため両方が確定するまでは return している。outputFormat を受け取ったら Muxer の addTrack() を実行し、動画と音声両方を addTrack() 完了したら muxer.start() でMux処理を始めている

Mux

Muxは MediaMuxer クラスで行う。setOrientationHint() を設定しないと動画が反転することがあるようである。出力ファイルのパスを設定しておく

    private val muxer: MediaMuxer

    init {
        ...
        val videoMetaData = MediaMetadataRetriever()
        videoMetaData.setDataSource(inputFilePath)
        val degreeString = videoMetaData.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
        val videoDegree = degreeString?.toInt() ?: 0
        muxer = MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        muxer.setOrientationHint(videoDegree)
    }

writeSampleData() を実行することで最終的に動画ファイルとして出力される

        while (!isMuxEnd) {
            ...
            if (!isVideoEncodeEnd) {
                isVideoEncodeEnd = encode(videoEncoder, {
                    outputVideoFormat = it
                    outputVideoTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputVideoTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
            if (!isAudioEncodeEnd) {
                isAudioEncodeEnd = encode(audioEncoder, {
                    outputAudioFormat = it
                    outputAudioTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputAudioTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
        }

まとめ

MediaCodecを使って動画をExtract, Decode, Encode, Muxし元動画と同じ動画を出力することができた :ok_woman:
今回は入力動画と出力動画が同じだったが、今度はデコード結果の生の動画データや音声データを加工し動画の一部を切り出したり音のサンプリングレートを変換したりなどの面白いことに挑戦してみたい 💪

プロジェクトファイル一式

参考

Android Developer (MediaCodec)
上の和訳
Googleのサンプルコード (ExtractDecodeEditEncodeMuxTest.java)

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7