Android オーディオとビデオの開発 (9) MediaCodec のデコードとビデオの再生

序章

MediaCodec は、オーディオとビデオのエンコードとデコード、つまりエンコーダー/デコーダー コンポーネントのために Android によって提供されるクラスです。基になるコーデックにアクセスすることでコーデック機能を実装します。これは Android メディア フレームワークの一部であり、通常 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack と一緒に使用され、ビデオ再生とビデオ圧縮エンコードで重要な役割を果たします。詳しいAPIの紹介は公式ドキュメントを参照してください

作業過程

全体的なプロセスに関しては、MediaCodec コーデックは入力データを処理してから出力データを生成します。このプロセスは非同期であり、一連の入力バッファーと出力バッファーを使用します。
プロセスは次のとおりです。
メディアコーデック

  1. クライアントは、MediaCodec から空の入力バッファ (ByteBuffer) を要求または受信し、それにデータを埋めて、処理のために MediaCodec に送信します。
  2. コーデックはデータを処理した後、空の出力バッファ (ByteBuffer) にデータを出力します。
  3. クライアントは、MediaCodec から満たされた出力バッファを取得し、そのコンテンツを取得して消費し、それをコーデックに解放して戻します。

よく使用される API の紹介

  • createDecoderByType は
    MimeType に基づいてデコーダを作成します


  • configureMediaCodecを構成する

  • getInputBuffer(index) は、
    データをエンコードする必要がある入力ストリーム キューを取得し、ByteBuffer を返します。

  • dequeueInputBuffer(long timeoutUs) は、
    有効なデータで満たされた入力バッファのインデックスを返します。現在使用可能なバッファがない場合は -1 を返します。timeoutUs == 0 の場合、このメソッドはすぐに戻ります。timeoutUs < 0 の場合、入力バッファーが使用可能になるまで無期限に待機し、timeoutUs > 0 の場合、「timeoutUs」マイクロ秒まで待機します。

  • queueInputBuffer は、
    指定されたインデックスの入力バッファに特定の範囲のデータを書き込みます。

  • dequeueOutputBuffer は、
    出力バッファからデータを取得し、データ インデックスまたは定義された状態定数を返します。

  • getOutputBuffer(index) は、
    出力バッファキュー内の指定されたインデックスの ByteBuffer を返します。

  • releaseOutputBuffer
    処理が完了し、ByteBuffer データを解放します

使用

以下は、ビデオ再生のための MediaCodec+MediaExtractor+SurfaceView の例です。
注: オーディオとビデオは別々に再生する必要があります。

最初のステップは、MediaExtractor を初期化してビデオ ファイルからビデオ トラック情報を取得することです。

private fun init() {
    
    
        try {
    
    
                //创建MediaExtractor对象
                videoExtractor = MediaExtractor()
                //设置视频数据源,可以是本地文件也可以是网络文件
                //注意,安卓9.0以上不允许http明文链接请求的地址,需要适配
                videoExtractor?.setDataSource(videoPath!!)
                val count = videoExtractor!!.trackCount //获取轨道数量
                //视频
                for (i in 0 until count) {
    
    
                    val mediaFormat = videoExtractor!!.getTrackFormat(i)
                    val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                    if (mimeType.startsWith("video/")) {
    
    //获取到视频轨道
                        videoExtractor?.selectTrack(i)
                        initVideo(mediaFormat)
                        break
                    }
                }
        } catch (e: Exception) {
    
    
            Log.e("Test", "出错了", e)
        }
    }

2 番目のステップは、MediaCodec ビデオ デコーダを初期化することです。

private fun initVideo(mediaFormat: MediaFormat) {
    
    
        val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
        val codecInfo = getCodecInfo(mimeType)//获取支持的解码格式

        if (codecInfo != null) {
    
    
            handler.sendMessage(Message.obtain(handler, 100, mediaFormat))
            //根据MimeType创建解码器
            videoCodec = MediaCodec.createDecoderByType(mimeType)
            //配置,在此关联SurfaceView
            //参数说明:mediaFormat:视频信息,surface:surface容器,crypto:数据加密 flags:解码器/编码器
            videoCodec?.configure(mediaFormat, sv_video.holder.surface, null, 0)
            videoCodec?.start()//开始解码

        } else {
    
    
            Log.e("Test", "格式错误")
        }
    }

3 番目のステップは、デコードおよび再生操作を実行するスレッドを作成することです。

  private val decodeVideoRunnable = Runnable {
    
    
        try {
    
    
            //存放目标文件的数据
            var byteBuffer: ByteBuffer? = null
            //解码后的数据,包含每一个buffer的元数据信息,例如偏差,在相关解码器中有效的数据大小
            val info = MediaCodec.BufferInfo()
//            videoCodec!!.getOutputBuffers()
            var first = false
            var startWhen: Long = 0
            var isInput = true
            while (isDecoding) {
    
    
//            Thread.sleep(20)//可以控制慢放
                if (isInput) {
    
    
                    //1.准备一个输入缓冲区
                    val inIndex = videoCodec!!.dequeueInputBuffer(TIMEOUT)

                    if (inIndex >= 0) {
    
    
                        //2.准备填充数据
                        byteBuffer = videoCodec!!.getInputBuffer(inIndex)
                        //使用MediaExtractor读取视频数据
                        val sampleSize = videoExtractor!!.readSampleData(byteBuffer!!, 0)

                        if (videoExtractor!!.advance() && sampleSize > 0) {
    
     //有数据,数据可用
                            //3.写入数据到输入缓冲区
                            videoCodec!!.queueInputBuffer(
                                inIndex,
                                0,
                                sampleSize,
                                videoExtractor!!.sampleTime,
                                0
                            )

                        } else {
    
     //没有数据了,停止输入处理
                            videoCodec!!.queueInputBuffer(
                                inIndex,
                                0,
                                0,
                                0,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM
                            )
                            isInput = false
                        }
                    } else {
    
    
                        continue
                    }
                }
                //4 获取一个输出缓冲区,开始解码
                val outIndex = videoCodec!!.dequeueOutputBuffer(info, TIMEOUT)

                if (outIndex >= 0) {
    
    
                    when (outIndex) {
    
    
                        MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
    
    
                            Log.d("Test", "INFO_OUTPUT_BUFFERS_CHANGED")
                            videoCodec!!.getOutputBuffer(outIndex)
                        }
                        MediaCodec.INFO_OUTPUT_FORMAT_CHANGED ->
                            Log.d(
                                "Test",
                                "INFO_OUTPUT_FORMAT_CHANGED format : " + videoCodec!!.getOutputFormat()
                            )
                        MediaCodec.INFO_TRY_AGAIN_LATER -> {
    
    
                        }
                        else -> {
    
    
                            if (!first) {
    
    
                                startWhen = System.currentTimeMillis()
                                first = true
                            }
                            try {
    
    
                                val sleepTime: Long =
                                    info.presentationTimeUs / 1000 - (System.currentTimeMillis() - startWhen)
                                if (sleepTime > 0) Thread.sleep(sleepTime)
                            } catch (e: InterruptedException) {
    
    
                                e.printStackTrace()
                            }
                            //对outputbuffer的处理完后,调用这个函数把buffer重新返回给codec类。
                            //调用这个api之后,SurfaceView才有图像
                            videoCodec!!.releaseOutputBuffer(outIndex, true)
                        }
                    }
                }
            }
            //解码完毕,释放资源
            videoCodec?.stop()
            videoCodec?.release()
            videoExtractor?.release()
        } catch (e: Exception) {
    
    
            Log.e("Test", "", e)
        }
    }

この時点で、ビデオの再生が完了しました。オーディオの再生について話しましょう。競合しないように、さまざまな MediaExtractor と MediaCodec の
構成と起動プロセスは、実際にはビデオのデコードに似ています。違いは、より多くの AudioTrack があることです。

//解码音频,进行播放
 private val decodeAudioRunnable = Runnable {
    
    
        try {
    
    
            val inputBuffers: Array<ByteBuffer> = audioCodec!!.getInputBuffers()
            var outputBuffers: Array<ByteBuffer> = audioCodec!!.getOutputBuffers()
            val info = BufferInfo()
            val buffsize = AudioTrack.getMinBufferSize(
                sampleRate,
                CHANNEL_OUT_STEREO,
                ENCODING_PCM_16BIT
            )

            // 创建AudioTrack对象
            var audioTrack: AudioTrack? = AudioTrack(
                AudioManager.STREAM_MUSIC, sampleRate,
                CHANNEL_OUT_STEREO,
                ENCODING_PCM_16BIT,
                buffsize,
                MODE_STREAM
            )
            //启动AudioTrack
            audioTrack!!.play()
            while (isDecoding) {
    
    
                val inIndex: Int = audioCodec!!.dequeueInputBuffer(TIMEOUT)
                if (inIndex >= 0) {
    
    
                    val buffer = inputBuffers[inIndex]
                    //从MediaExtractor中读取待解数据
                    val sampleSize = audioExtractor!!.readSampleData(buffer, 0)
                    if (sampleSize < 0) {
    
    
                        audioCodec!!.queueInputBuffer(
                            inIndex, 0, 0, 0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM
                        )
                    } else {
    
     //向MediaDecoder输入待解码数据
                        audioCodec!!.queueInputBuffer(
                            inIndex,
                            0,
                            sampleSize,
                            videoExtractor!!.sampleTime,
                            0
                        )
                        audioExtractor!!.advance()
                    }
                    //从输出缓冲区队列取出解码后的数据
                    val outIndex: Int = audioCodec!!.dequeueOutputBuffer(info, TIMEOUT)
                    when (outIndex) {
    
    
                        MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
    
    
                            outputBuffers = audioCodec!!.getOutputBuffers()
                        }
                        MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
    
    
                            val format: MediaFormat = audioCodec!!.getOutputFormat()
                            audioTrack.playbackRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
                        }
                        MediaCodec.INFO_TRY_AGAIN_LATER -> Log.d(
                            "Test",
                            "dequeueOutputBuffer timed out!"
                        )
                        else -> {
    
    
                            val outBuffer = outputBuffers[outIndex]
                            //Log.v(TAG, "outBuffer: " + outBuffer);
                            val chunk = ByteArray(info.size)
                            // Read the buffer all at once
                            outBuffer[chunk]
                            //清空buffer,否则下一次得到的还会得到同样的buffer
                            outBuffer.clear()
                            // AudioTrack write data
                            audioTrack.write(chunk, info.offset, info.offset + info.size)
                            audioCodec!!.releaseOutputBuffer(outIndex, false)
                        }
                    }
                    // 所有帧都解码、播放完之后退出循环
                    if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
    
    
                        break
                    }
                }
            }
            //释放MediaDecoder资源
            audioCodec?.stop()
            audioCodec?.release()
            audioCodec = null
            audioExtractor?.release()
            audioExtractor = null
            //释放AudioTrack资源
            audioTrack.stop()
            audioTrack.release()
            audioTrack = null
        } catch (e: Exception) {
    
    
            Log.e("Test", "", e)
        }
    }

最後に、重要なポイントであるビデオの幅と高さの決定について説明します。
ここでは MediaExtractor を使用して MediaFormat を取得し、ビデオの幅と高さを取得します。実際の開発では、通常、プレーヤーの幅と高さが固定され、SurfaceView が拡大縮小されて塗りつぶされます。

 /**
     * 根据视频大小改变SurfaceView大小
     */
    private fun changeVideoSize(mediaFormat: MediaFormat) {
    
    
        var videoWidth = mediaFormat.getInteger(MediaFormat.KEY_WIDTH) //获取高度
        var videoHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT) //获取高度
        val surfaceWidth = sv_video.measuredWidth
        val surfaceHeight = sv_video.measuredHeight
        //根据视频尺寸去计算->视频可以在sufaceView中放大的最大倍数。
        var maxSize: Double
        maxSize =
            if (resources.configuration.orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
    
    
                //竖屏模式下按视频宽度计算放大倍数值
                max(videoWidth / surfaceWidth.toDouble(), videoHeight / surfaceHeight.toDouble())
            } else {
    
    
                //横屏模式下按视频高度计算放大倍数值
                max(videoWidth / surfaceHeight.toDouble(), videoHeight / surfaceWidth.toDouble())
            }

        //视频宽高分别/最大倍数值 计算出放大后的视频尺寸
        videoWidth = Math.ceil(videoWidth / maxSize).toInt();
        videoHeight = Math.ceil(videoHeight / maxSize).toInt();

        //将计算出的视频尺寸设置到surfaceView 让视频自动填充。
        sv_video.layoutParams = ConstraintLayout.LayoutParams(videoWidth, videoHeight);
    }

完全なコードをご覧ください: VideoDemo

おすすめ

転載: blog.csdn.net/gs12software/article/details/106349092