Android オーディオおよびビデオ コーデック (2) -- MediaCodec デコード (同期および非同期)

前回の記事Android のオーディオとビデオのコーデック (1) - MediaCodec 入門 で、MediaCodec の知覚的な理解がすでにあるこの章では、MediaCodec のデコード機能を学習します。

この章の効果は次のとおりです。

ここに画像の説明を挿入

1.同期デコード

MediaCodec の動作原理と動作手順をよりよく理解するために、まず同期デコード方式を使用してローカル ビデオをデコードします。

1.1 ビデオ パラメータを取得する

まず、MP4 形式などのエンコード済みのビデオを準備する必要があります.MediaCodec を介してビデオの MediaFormat 情報を取得できます.MediaExtractor に慣れていない場合は、Android のオーディオおよびビデオ開発を使用できます(5) – MediaExtractor を使用してオーディオとビデオを分離し、MediaMuxer を使用して新しいビデオを合成 (オーディオとビデオの同期) して学習します。

MediaExtractor を実装して具体的にビデオを分析し、ビデオ データを取得する MyExtractor を定義します。

  public MyExtractor(String path) {
        try {
            mediaExtractor = new MediaExtractor();
            // 设置数据源
            mediaExtractor.setDataSource(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //拿到所有的轨道
        int count = mediaExtractor.getTrackCount();
        for (int i = 0; i < count; i++) {
            //根据下标拿到 MediaFormat
            MediaFormat format = mediaExtractor.getTrackFormat(i);
            //拿到 mime 类型
            String mime = format.getString(MediaFormat.KEY_MIME);
            //拿到视频轨
            if (mime.startsWith("video")) {
                videoTrackId = i;
                videoFormat = format;
            } else if (mime.startsWith("audio")) {
                //拿到音频轨
                audioTrackId = i;
                audioFormat = format;
            }
​
        }
    }
​
​
    public void selectTrack(int trackId){
        mediaExtractor.selectTrack(trackId);
    }
​
    /**
     * 读取一帧的数据
     *
     * @param buffer
     * @return
     */
    public int readBuffer(ByteBuffer buffer) {
        //先清空数据
        buffer.clear();
        //选择要解析的轨道
      //  mediaExtractor.selectTrack(video ? videoTrackId : audioTrackId);
        //读取当前帧的数据
        int buffercount = mediaExtractor.readSampleData(buffer, 0);
        if (buffercount < 0) {
            return -1;
        }
        //记录当前时间戳
        curSampleTime = mediaExtractor.getSampleTime();
        //记录当前帧的标志位
        curSampleFlags = mediaExtractor.getSampleFlags();
        //进入下一帧
        mediaExtractor.advance();
        return buffercount;
    }

最初に、selectTrack を使用してビデオまたはオーディオを分析するかどうかを指定し、次に、mediaExtractor.readSampleData(buffer, 0); を使用する readBuffer メソッドを使用して現在のビデオのバッファを取得し、mediaExtractor.advance( を介して次のものを取得します。 ) フレーム データ。

1.2 デコード処理

前の章で、MediaCodec のデコードは次の 2 つの図に基づいていると述べました。

MediaCodec の動作図MediaCodec の動作原理図

MediaCodec 状態図

MediaCodec 状態図

この 2 つの画像の処理に慣れていない場合は、Android のオーディオとビデオのコーデック (1) - MediaCodec 予備調査 をお読みください。

後続のオーディオ デコーディングを容易にするために、ここではビデオとオーディオを解析するための基本クラスを定義します。ビデオは同期的に解析されるため、スレッドで解析する必要があるため、Runnable を継承します。

   /**
     * 解码基类,用于解码音视频
     */
    abstract class BaseDecode implements Runnable {
        final static int VIDEO = 1;
        final static int AUDIO = 2;
        //等待时间
        final static int TIME_US = 1000;
        MediaFormat mediaFormat;
        MediaCodec mediaCodec;
        MyExtractor extractor;
​
        private boolean isDone;
        public BaseDecode() {
            try {
                //获取 MediaExtractor
                extractor = new MyExtractor(Constants.VIDEO_PATH);
                //判断是音频还是视频
                int type = decodeType();
                //拿到音频或视频的 MediaFormat
                mediaFormat = (type == VIDEO ? extractor.getVideoFormat() : extractor.getAudioFormat());
                String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
                //选择要解析的轨道
                extractor.selectTrack(type == VIDEO ? extractor.getVideoTrackId() : extractor.getAudioTrackId());
                //创建 MediaCodec
                mediaCodec = MediaCodec.createDecoderByType(mime);
                //由子类去配置
                configure();
                //开始工作,进入编解码状态
                mediaCodec.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
 }

ビデオまたはビデオに応じて異なる MediaFormats が取得され、MIME タイプに応じて MediaCodec が作成されることがわかります。

MediaCodec 状態図

このようにして、最初のステップが作成されます. 次に、上記の状態から未初期化状態になります. 次に、configure メソッドを呼び出して構成済み状態に入る必要があります. このステップは、ビデオなどのサブクラスによって完了されます:

 @Override
 void configure() {
     mediaCodec.configure(mediaFormat, new Surface(mTextureView.getSurfaceTexture()), null, 0);
 }

ビデオを再生するための現在の MediaFormat と Surface が mediaCodec.configure() によって構成されており、TextureView がここで使用されていることがわかります。次に、MediaCodec の start() を呼び出して Executing 状態に入り、エンコードとデコードを開始します。

1.3 ビデオのデコード

デコードプロセスはこの画像に基づいていますMediaCodec の動作原理図

1.3.1 入力

BaseDecode は Runnable を継承していると前述したので、デコード処理は run メソッド内にあります。

    @Override
        public void run() {
​
​
            try {
                
                MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                //编码
                while (!isDone) {
                    /**
                     * 延迟 TIME_US 等待拿到空的 input buffer下标,单位为 us
                     * -1 表示一直等待,知道拿到数据,0 表示立即返回
                     */
                    int inputBufferId = mediaCodec.dequeueInputBuffer(TIME_US);
​
                    if (inputBufferId > 0) {
                        //拿到 可用的,空的 input buffer
                        ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
                        if (inputBuffer != null) {
                            /**
                             * 通过 mediaExtractor.readSampleData(buffer, 0) 拿到视频的当前帧的buffer
                             * 通过 mediaExtractor.advance() 拿到下一帧
                             */
                            int size = extractor.readBuffer(inputBuffer);
                            //解析数据
                            if (size >= 0) {
                                mediaCodec.queueInputBuffer(
                                        inputBufferId,
                                        0,
                                        size,
                                        extractor.getSampleTime(),
                                        extractor.getSampleFlags()
                                );
                            } else {
                                //结束,传递 end-of-stream 标志
                                mediaCodec.queueInputBuffer(
                                        inputBufferId,
                                        0,
                                        0,
                                        0,
                                        MediaCodec.BUFFER_FLAG_END_OF_STREAM
                                );
                                isDone = true;
​
                            }
                        }
                    }
                    //解码输出交给子类
                    boolean isFinish =  handleOutputData(info);
                    if (isFinish){
                        break;
                    }
​
                }
​
                done();
​
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
​
        protected void done(){
            try {
                isDone = true;
                //释放 mediacodec
                mediaCodec.stop();
                mediaCodec.release();
​
                //释放 MediaExtractor
                extractor.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
​
        }
        abstract boolean handleOutputData(MediaCodec.BufferInfo info);

while ループの連続デコードでは、上記のコードは次のプロセスを実行します。

MediaCodec からアイドル バッファを取得します ビデオからビデオの現在のフレームのデータを取得し、MediaCodec のバッファに入力します mediaCodec.queueInputBuffer() を使用して、バッファのデータを MediaCodec に渡してデコードします

1.3.2 出力

上記の出力処理は handleOutputData() で実装され、VideoDecodeSync に渡されます. コードは次のように実装されます:

@Override
boolean handleOutputData(MediaCodec.BufferInfo info) {
    //等到拿到输出的buffer下标
    int outputId = mediaCodec.dequeueOutputBuffer(info, TIME_US);
​
    if (outputId >= 0){
        //释放buffer,并渲染到 Surface 中
        mediaCodec.releaseOutputBuffer(outputId, true);
    }
​
    // 在所有解码后的帧都被渲染后,就可以停止播放了
    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        Log.e(TAG, "zsr OutputBuffer BUFFER_FLAG_END_OF_STREAM");
​
        return true;
    }
​
    return false;
}

上記のコードは、次の 2 つのことも行います。

  1. 出力バッファを取得する

  2. releaseOutputBuffer() の 2 番目のパラメーターによって制御される、バッファーを解放し、ビデオを Surface にレンダリングします。

このようにして、ビデオのデコード部分を書きました。効果は次のとおりです。

ここに画像の説明を挿入

ただし、ビデオが 2 倍の速度で再生されているように見えることがわかります。

1.3.3 正しい表示タイムスタンプ

上記の状況が発生するのはなぜですか?通常、当社のビデオ再生のフレーム レートは約 30、つまり 30fps であり、33.33ms ごとに 1 フレームが再生されますが、フレームをデコードする時間は約数ミリ秒で、デコードされた場合はそのまま表示されます。そうしないと、ビデオが 2 倍速で再生されているように見えます。だったら30msの遅延で遊べばいい、というのが標準ではないでしょうか。はい、30fps が標準ですが、すべてのビデオが 30 であるという意味ではありません。ここでは、オーディオとビデオ、DTS と PTS の基本を学ぶ必要があります。

【学習アドレス】:FFmpeg/WebRTC/RTMP/NDK/Androidの音声・動画ストリーミングメディアの高度な開発

[記事の特典]: より多くのオーディオおよびビデオ学習パッケージ、Dachang インタビューの質問、テクニカル ビデオ、学習ロードマップを無料で受け取ることができます (C/C++、Linux、FFmpeg webRTC rtmp hls rtsp ffplay srs など) 1079654574 をクリックして参加ます受け取るグループ〜

DTS (Decoding Time Stamp): デコード タイム スタンプです. このタイム スタンプの意味は、このフレームのデータをいつデコードするかをプレーヤーに伝えることです. PTS (Presentation Time Stamp): タイム スタンプを表示します. このタイム スタンプは、 The player what このフレームを再生するとき、DTS と PTS はプレーヤーの動作をガイドするために使用されますが、これらはエンコード中にエンコーダーによって生成されることに注意してください。B フレームがない場合、DTS と PTS の出力順序は同じですが、B フレームがある場合は順序が異なります。ここでは、タイムスタンプを表示する PTS だけを気にする必要があります。現在の pts タイムスタンプは、MediaCodec.BufferInfo の presentationTimeUs を介して取得できます. 単位は微妙です. 0 に対する再生開始時間です. したがって、システムの時間差を使用して、2 つのフレーム間の時間差を模倣することができます。デコードされた pts を比較すると、時間差が速い場合は遅れて Surface に出力され、そうでない場合は直接 Surface に表示されます。

これはスレッド内にあるため、Surface にレンダリングする前に Thread.sleep() を使用して実現できます。

 // 用于对准视频的时间戳
 private long startMs = -1;
if (outputId >= 0){
     if (mStartMs == -1) {
            mStartMs = System.currentTimeMillis();
      }
    //矫正pts
     sleepRender(info, startMs);
     //释放buffer,并渲染到 Surface 中
     mediaCodec.releaseOutputBuffer(outputId, true);
 }
#sleepRender
    /**
     * 数据的时间戳对齐
     **/
    private void sleepRender(MediaCodec.BufferInfo info, long startMs) {
        /**
         * 注意这里是以 0 为出事目标的,info.presenttationTimes 的单位为微秒
         * 这里用系统时间来模拟两帧的时间差
         */
        long ptsTimes = info.presentationTimeUs / 1000;
        long systemTimes = System.currentTimeMillis() - startMs;
        long timeDifference = ptsTimes - systemTimes;
        // 如果当前帧比系统时间差快了,则延时以下
        if (timeDifference > 0) {
            try {
                Thread.sleep(timeDifference);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
        }
    }

時間再生は正常になりました。

ここに画像の説明を挿入

1.4 オーディオのデコード

映像のデコードは以上で理解できたので、音声のデコードは比較的簡単です. 新しい AudioDecodeSync クラスを作成し、BaseDecode を継承し、configure メソッドで MediaCodec を構成します. Surface は必要ないので、null を渡すだけです.

 @Override
 void configure() {
     mediaCodec.configure(mediaFormat, null, null, 0);
 }

Surface を使用する必要はありませんが、ビデオを再生する必要がある場合は、AudioTrack を使用する必要があります. 必要がない場合は、Android のオーディオとビデオの開発 (1) - AudioRecord を使用して PCM を記録するを参照してください。 (録音); オーディオを再生するための AudioTrack。

したがって、AudioDecodeSync の構築メソッドでは、次の AudioTrack を構成する必要があります。

    class AudioDecodeSync extends BaseDecode {
​
        private int mPcmEncode;
        //一帧的最小buffer大小
        private final int minBufferSize;
        private AudioTrack audioTrack;
​
​
        public AudioDecodeSync() {
            //拿到采样率
            if (mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
                mPcmEncode = mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING);
            } else {
                //默认采样率为 16bit
                mPcmEncode = AudioFormat.ENCODING_PCM_16BIT;
            }
​
            //音频采样率
            int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
            //获取视频通道数
            int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
​
            //拿到声道
            int channelConfig = channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO;
            minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, mPcmEncode);
​
​
            /**
             * 设置音频信息属性
             * 1.设置支持多媒体属性,比如audio,video
             * 2.设置音频格式,比如 music
             */
            AudioAttributes attributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build();
            /**
             * 设置音频数据
             * 1. 设置采样率
             * 2. 设置采样位数
             * 3. 设置声道
             */
            AudioFormat format = new AudioFormat.Builder()
                    .setSampleRate(sampleRate)
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setChannelMask(channelConfig)
                    .build();
​
​
            //配置 audioTrack
            audioTrack = new AudioTrack(
                    attributes,
                    format,
                    minBufferSize,
                    AudioTrack.MODE_STREAM, //采用流模式
                    AudioManager.AUDIO_SESSION_ID_GENERATE
            );
            //监听播放
            audioTrack.play();
        }
        }

AudioTrack を取得したら、play() メソッドを使用してデータが書き込まれているかどうかを監視し、オーディオの再生を開始できます。

ハンドル出力データ:

    @Override
        boolean handleOutputData(MediaCodec.BufferInfo info) {
            //拿到output buffer
            int outputIndex = mediaCodec.dequeueOutputBuffer(info, TIME_US);
            ByteBuffer outputBuffer;
            if (outputIndex >= 0) {
                outputBuffer = mediaCodec.getOutputBuffer(outputIndex);
                //写数据到 AudioTrack 只,实现音频播放
                audioTrack.write(outputBuffer, info.size, AudioTrack.WRITE_BLOCKING);
                mediaCodec.releaseOutputBuffer(outputIndex, false);
            }
            // 在所有解码后的帧都被渲染后,就可以停止播放了
            if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                Log.e(TAG, "zsr OutputBuffer BUFFER_FLAG_END_OF_STREAM");
​
                return true;
            }
            return false;
        }

音声は正常に再生されており、音声のタイム スタンプは比較的連続しているため、早送りの意味がないことがわかります。したがって、それを修正する必要はありません。

1.5 オーディオとビデオの同期

では、オーディオとビデオを同期するにはどうすればよいでしょうか。実際、2 つのスレッドを開いて同時に再生させるだけなので、難しくはありません。

if (mExecutorService.isShutdown()) {
            mExecutorService = Executors.newFixedThreadPool(2);
        }
        mVideoSync = new VideoDecodeSync();
        mAudioDecodeSync = new AudioDecodeSync();
        mExecutorService.execute(mVideoSync);
        mExecutorService.execute(mAudioDecodeSync);
  }

2.非同期デコード

5.0 以降、google は非同期デコードによる MediaCodec の使用を推奨しています。これも非常に簡単で、setCallback メソッドを使用するだけです。たとえば、上記のビデオを分析するには、次の手順を実行できます。

MediaExtractor を使用してビデオを解析し、MediaFormat を取得し、MediaCodec.setCallback() メソッドを使用して mediaCodec.configure() および mediaCodec.start() を呼び出してデコードを開始します。したがって、コードは次のようになります。

    class AsyncDecode {
        MediaFormat mediaFormat;
        MediaCodec mediaCodec;
        MyExtractor extractor;
​
        public AsyncDecode() {
            try {
                //解析视频,拿到 mediaformat
                extractor = new MyExtractor(Constants.VIDEO_PATH);
                mediaFormat = (extractor.getVideoFormat());
                String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
                extractor.selectTrack(extractor.getVideoTrackId());
                mediaCodec = MediaCodec.createDecoderByType(mime);
​
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
​
        private void start() {
            //异步解码
            mediaCodec.setCallback(new MediaCodec.Callback() {
                @Override
                public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
                        ByteBuffer inputBuffer = codec.getInputBuffer(index);
                    int size = extractor.readBuffer(inputBuffer);
                    if (size >= 0) {
                        codec.queueInputBuffer(
                                index,
                                0,
                                size,
                                extractor.getSampleTime(),
                                extractor.getSampleFlags()
                        );
                        handler.sendEmptyMessage(1);
                    } else {
                        //结束
                        codec.queueInputBuffer(
                                index,
                                0,
                                0,
                                0,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM
                        );
                    }
                }
​
                @Override
                public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
                        mediaCodec.releaseOutputBuffer(index, true);
                }
​
                @Override
                public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
                    codec.reset();
                }
​
                @Override
                public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
​
                }
            });
            //需要在 setCallback 之后,配置 configure
            mediaCodec.configure(mediaFormat, new Surface(mTextureView.getSurfaceTexture()), null, 0);
            //开始解码
            mediaCodec.start();
        }
​
    }

非同期デコードを使用するプロセスは、基本的に同期デコードのプロセスと同じですが、同期コードでは、

int inputBufferId = mediaCodec.dequeueInputBuffer(TIME_US);

未使用の入力バッファーの添え字を取得するのを待機するには、非同期で、コールバックを使用します。

void onInputBufferAvailable(@NonNull MediaCodec codec, int index) 

入力バッファの添え字を取得します。事故がなければ、ビデオはすでに再生を開始していますが、上記の問題、つまり、倍速で再生されるという問題が発生する場合もあります.これは、PTSの問題が修正されていないためです.

私たちのコードはメイン スレッドで実行されるため、Thread.sleep() は確実にフリーズしますが、HandlerThread または他のスレッドを使用して解析できるので、ここでは投稿しません。詳しくはソース コードを見てみましょう。

3、参考:

[Android のオーディオとビデオの開発モンスター アップグレード: オーディオとビデオのハード デコードの記事] 3. オーディオとビデオの再生: オーディオとビデオの同期 - ショート ブック Android ビデオ処理 MediaCodec-3 - ビデオの再生 MediaCodec | Android 開発者

元のリンク: Android オーディオおよびビデオ コーデック (2) -- MediaCodec デコード (同期および非同期)_mediacodec 非同期デコード_夏至の稲穂のブログ - CSDN ブログ

おすすめ

転載: blog.csdn.net/irainsa/article/details/130020115