概要
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処理では MediaExtractor
の readSampleData()
でデータを読み込み 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し元動画と同じ動画を出力することができた
今回は入力動画と出力動画が同じだったが、今度はデコード結果の生の動画データや音声データを加工し動画の一部を切り出したり音のサンプリングレートを変換したりなどの面白いことに挑戦してみたい 💪
参考
Android Developer (MediaCodec)
上の和訳
Googleのサンプルコード (ExtractDecodeEditEncodeMuxTest.java)