Android 13 - メディア フレームワーク (9) - NuPlayer::Decoder

このセクションでは、NuPlayer::Decoder について学び、MediaCodec を強力な Decoder にラップする方法を学びます。このセクションでは、MediaCodec 関連のコンテンツについて事前に説明します。理解できない場合は、この記事をスキップしてください。最初は Decoder の部分は簡単だと思っていましたが、読めば読むほど自分の無知に気づき、Android のソースコードは本当に大きな宝の山です。
ps: この記事の大文字はDecoderNuPlayer::Decoder を指し、小文字はdecoderメディアコーデックとその基礎となる実際のデコーダーを指します。

1、デコーダーベース

まず NuPlayer::Decoder の基本クラスを見てみましょうDecoderBase

struct NuPlayer::DecoderBase : public AHandler {
    
    
    explicit DecoderBase(const sp<AMessage> &notify);
    void configure(const sp<AMessage> &format);
    void init();
    void setParameters(const sp<AMessage> &params);
    // Synchronous call to ensure decoder will not request or send out data.
    void pause();
    void setRenderer(const sp<Renderer> &renderer);
    virtual status_t setVideoSurface(const sp<Surface> &) {
    
     return INVALID_OPERATION; }
    void signalFlush();
    void signalResume(bool notifyComplete);
    void initiateShutdown();
    virtual sp<AMessage> getStats() {
    
    
        return mStats;
    }
protected:
	virtual void onMessageReceived(const sp<AMessage> &msg);

    virtual void onConfigure(const sp<AMessage> &format) = 0;
    virtual void onSetParameters(const sp<AMessage> &params) = 0;
    virtual void onSetRenderer(const sp<Renderer> &renderer) = 0;
    virtual void onResume(bool notifyComplete) = 0;
    virtual void onFlush() = 0;
    virtual void onShutdown(bool notifyComplete) = 0;
    void onRequestInputBuffers();
    virtual bool doRequestBuffers() = 0;
}

DecoderBase は、NuPlayer が Decoder を呼び出すことができるすべてのインターフェイスを定義します。インターフェイスの数が非常にまれであることがわかります。開始、停止、リセット、シークなどのメソッドはありません。現時点では、疑問を持つ人もいるかもしれません。なぜこれらのインターフェイスは上位層の下位層によって呼び出されますか? しかし、それはなくなってしまったのですか? 実際、インターフェイスの一部については以前の記事ですでに説明しているため、ここでは詳しく説明しません。

これらのインターフェイスの用途と使用方法を理解しましょう。

  • 构造函数: イベントをステータスにスローするための AMessage オブジェクトを渡します。
  • configure: 準備プロセス中に Source によって解析された形式情報を渡します。形式情報には、MIME タイプ、サーフェス、セキュア、幅、高さ、暗号化、csd などが含まれます。MediaCodec インスタンスを作成し、構成して開始します。
  • init: ALooper に登録します。
  • setParameters: 上位層からデコーダに渡されるパラメータを設定します。
  • pause: この方法は実際には役に立ちません。なぜ役に立たないのかについては後ほど詳しく説明します。
  • setRenderer: レンダーを設定します。デコーダーによってデコードされたデータは、avsync のレンダリングに送信されます。オーディオ データの場合は、AudioTrack に直接書き込まれます。
  • setVideoSurface: サーフェスをリセットします。オーディオ デコーダはこのメソッドを必要としません。
  • signalFlush:flush、デコーダの入出力バッファをリフレッシュします。
  • signalResume:デコーダのデコードプロセスを復元します。
  • initiateShutdown: デコードプロセスを停止し、関連リソースを解放します。
  • getStats: フォーマット情報、現在解決されているフレーム数、破棄されたフレーム数など、現在の Decoder のステータスを取得します。
  • 其他: onConfigure およびその他のメソッドは特定の Decoder によって実装されます。オーディオがオフロードを経由しない場合、オーディオ / ビデオ デコーダーは同じプロセスを経由します。

2. デコーダの作成と起動

NuPlayer のソース コードから、start メソッドを呼び出した後、Decoder が作成されることがわかります。ここでは、DecoderDecoderBase から継承されています。その後、Decoder.init と Decoder.configure が呼び出されます。ここで、Decoder が開始されます。

status_t NuPlayer::instantiateDecoder(
        bool audio, sp<DecoderBase> *decoder, bool checkAudioModeChange) {
    
    
	sp<AMessage> format = mSource->getFormat(audio);
    *decoder = new Decoder(
          notify, mSource, mPID, mUID, mRenderer, mSurface, mCCDecoder);
    (*decoder)->init();
    (*decoder)->configure(format);
}

init メソッドは非常に単純で、AHandler を ALooper に登録するだけです。ここで 2 つの質問がありますが、thisregisterHandler とは誰のことを指しますか?

void NuPlayer::DecoderBase::init() {
    
    
    mDecoderLooper->registerHandler(this);
}

onRequestInputBuffers 内のメッセージは、最初に Decoder::onMessageReceived または DecoderBase::onMessageReceived によって処理されますか? 答えがわからない場合は、ポリモーフィズムを検索してください。

void NuPlayer::DecoderBase::onRequestInputBuffers() {
    
    
	....
    sp<AMessage> msg = new AMessage(kWhatRequestInputBuffers, this);
    msg->post(10 * 1000LL);
}

引き続き下に見ていき、最終的には onConfigure メソッドでconfigureが呼び出されます。

void NuPlayer::Decoder::onConfigure(const sp<AMessage> &format) {
    
    
    ++mBufferGeneration;

    AString mime;
    CHECK(format->findString("mime", &mime));

    mIsAudio = !strncasecmp("audio/", mime.c_str(), 6);
    mComponentName = mime;
    mComponentName.append(" decoder");

    mCodec = MediaCodec::CreateByType(
            mCodecLooper, mime.c_str(), false /* encoder */, NULL /* err */, mPid, mUid, format);
    int32_t secure = 0;
    if (format->findInt32("secure", &secure) && secure != 0) {
    
    
        if (mCodec != NULL) {
    
    
            mCodec->getName(&mComponentName);
            mComponentName.append(".secure");
            mCodec->release();
            mCodec = MediaCodec::CreateByComponentName(
                    mCodecLooper, mComponentName.c_str(), NULL /* err */, mPid, mUid);
        }
    }
    err = mCodec->configure(
            format, mSurface, crypto, 0 /* flags */);
   	rememberCodecSpecificData(format);
    sp<AMessage> reply = new AMessage(kWhatCodecNotify, this);
    mCodec->setCallback(reply);

    err = mCodec->start();
}
  1. 形式で MIME を検索するには、これが必要です。
  2. CreateByType を呼び出して MediaCodec インスタンスを作成します。形式にセキュリティで保護されたフィールドがある場合は、CreateByComponentName を呼び出してセキュリティで保護されたコンポーネントを作成します。
  3. MediaCodec を設定するためにconfigureを呼び出すとき、コードストリームのフォーマットを渡す必要があります。フォーマットで何が必要かについては後ほど見ていきます。ここで渡されるサーフェスはNuPlayerでの判定があるためNULLではありません。サーフェスの場合NULL の場合、ビデオ用のコーデックは作成されません。
  4. コーデック固有のデータ (csd バッファ) を形式で保存します。これらのバッファには、sps pps や h264 および h265 コード ストリームのその他の情報などのコード ストリーム情報が記録されます。一部のデコーダでは、この情報を渡す必要がありますが、一部のデコーダでは、この情報を渡すことができます。コード ストリームで解析される情報は、各企業のデコーダの実装によって異なります。
  5. MediaCodec のコールバックを登録し、异步次の方法で動作させます。
  6. start メソッドを呼び出してデコーダを開始し、データの読み取り、データのデコード、およびデータのレンダリングのプロセス全体を開始します。

以下の内容は私の個人的な意見であり、少し長めですので、苦手な方は読み飛ばしてください。

これらに加えて、 member も調べる必要がありますmBufferGeneration。これは何をするのでしょうか? 実際、メディアでは複数の世代、つまりトリックという概念が使用されていると以前に述べましたが、では、ここでの世代は何のために使用されるのでしょうか?

コード内で mBufferGeneration を検索したところ、その値がonConfiguredoFlushonShutdownおよびhandleErrorで変更されることがわかりました。これら 4 つのメソッドには共通点が 1 つあります。これらは MediaCodec を操作し、MediaCodec の状態を変更するため、MediaCodec バッファの状態に影響を与えます。

mBufferGeneration がどこで使用されるかを見てみましょう。

bool NuPlayer::Decoder::isStaleReply(const sp<AMessage> &msg) {
    
    
    int32_t generation;
    CHECK(msg->findInt32("generation", &generation));
    return generation != mBufferGeneration;
}

void NuPlayer::Decoder::onMessageReceived(const sp<AMessage> &msg) {
    
    
	switch (msg->what()) {
    
    
        case kWhatRenderBuffer:
        {
    
    
            if (!isStaleReply(msg)) {
    
    
                onRenderBuffer(msg);
            }
            break;
        }
    }
}

ビデオ出力バッファが avsync のためにレンダラーに送信され、その後レンダリングのために送り返されると、現在の mBufferGeneration が変更されたかどうかが判断されます。ここでこれを行う目的は何ですか?

私の理解は次のとおりです。出力バッファが処理のためにレンダラーに送信された後、レンダラーはレンダリング関連のメソッドを呼び出しますが、この時点でバッファのステータスがフラッシュやシャットダウンなど変更されている可能性があり、バッファはもう機能しません。 Processing である必要がある場合は、mBufferGeneration を使用して判断し、処理ステップをスキップできます。

問題を引き渡すその他のコンポーネント処理中に、イベント処理が終了した時点で現在の世代を記録します。現在のコンポーネントに戻るコンテンツを破棄する必要があるかどうかは、現在の世代に基づいて決定されます。Android は、ACodec、NuPlayer::Source、Renderer、およびその他の実装の生成手法を使用して、状態遷移中のトランザクションを処理します。

上記は、初めてコードを読んだときの世代についての理解ですが、もう一度読んだ後、いくつかの新しい洞察が得られました。

ALooper および AHandler の非同期メッセージング メカニズムは、NuPlayer で広く使用されています。ここでの非同期は呼び出し元に関連しています。たとえば、NuPlayer が Decoder のconfigure メソッドを呼び出すと、NuPlayer は呼び出し後に終了します。このとき、Decoder の MediaCodec オブジェクトは、はまだ作成されていません。これは非同期です。しかし、Decoder の場合、すべての呼び出し (送信されたメッセージ) は Looper 内のスレッドによって 1 つずつ処理されるため、Decoder の内部は次のようになります。同期の。

なんでそんなこというの?Decoder にメッセージを送信する人を見てみましょう: NuPlayer、Renderer、MediaCodec です。これらはすべて同時に、または順番にメッセージを Decoder に送信する可能性があります。これによりどのような問題が発生するでしょうか? レンダラを例に挙げます。

    sp<AMessage> reply = new AMessage(kWhatRenderBuffer, this);
    reply->setSize("buffer-ix", index);
    reply->setInt32("generation", mBufferGeneration);
    reply->setSize("size", size);

Decoder は、MediaCodec によって送信された CB_OUTPUT_AVAILABLE イベントを受信した後、mBufferGeneration を msg に保存し、Renderer に渡します。Renderer が同期を完了した後、メッセージを Decoder に再送信します。ただし、同期プロセス中に上位層がリセットを呼び出し、デコーダーもイベントを処理する場合、デコーダーはレンダラによって送信されたレンダリング メッセージを処理できなくなります (プロセス全体が停止し、コンポーネントが解放されます)。 。

画像の説明を追加してください

生成は状態レコードの役割を果たし、状態が変化すると、状態に依存するメッセージは処理されなくなります。これは、MediaPlayer.cpp での状態の使用など、いくつかの特定の状態に似ていますが、生成の使用はより単純です。特定の状態には注意を払わず、Decoder の状態を変更するメソッドが呼び出されるかどうかにのみ焦点を当てます。 。

3、スタート

前のセクションの start の説明を引用します。开启整个数据读取、数据解码 以及 数据渲染流程start は MediaCodec を開始するだけでなく、すべてのコンポーネントの動作も制御します。見てみましょう。

まず、主要な MediaCodec コールバックを見てみましょう。

void NuPlayer::Decoder::onMessageReceived(const sp<AMessage> &msg) {
    
    
    switch (msg->what()) {
    
    
        case kWhatCodecNotify:
        {
    
    
            int32_t cbID;
            CHECK(msg->findInt32("callbackID", &cbID));
            switch (cbID) {
    
    
                case MediaCodec::CB_INPUT_AVAILABLE:
                {
    
    
                    int32_t index;
                    CHECK(msg->findInt32("index", &index));

                    handleAnInputBuffer(index);
                    break;
                }

                case MediaCodec::CB_OUTPUT_AVAILABLE:
                {
    
    
                    int32_t index;
                    size_t offset;
                    size_t size;
                    int64_t timeUs;
                    int32_t flags;

                    CHECK(msg->findInt32("index", &index));
                    CHECK(msg->findSize("offset", &offset));
                    CHECK(msg->findSize("size", &size));
                    CHECK(msg->findInt64("timeUs", &timeUs));
                    CHECK(msg->findInt32("flags", &flags));

                    handleAnOutputBuffer(index, offset, size, timeUs, flags);
                    break;
                }
            }
        }
    }
}

MediaCodec は、kWhatCodecNotifyメッセージを Decoder に送信することでメッセージを処理し、送信されたコンテンツを区別するために callbackID を使用します。一般的に使用されるのはCB_INPUT_AVAILABLECB_OUTPUT_AVAILABLE、の4 つでCB_ERROR、ここでは入力と出力についてのみ説明します。CB_OUTPUT_FORMAT_CHANGED

3.1、CB_INPUT_AVAILABLE

MediaCodec によってスローされた入力イベントを受信した後、handleAnInputBuffer メソッドが呼び出されます。入力パラメーターは入力バッファー ID です。

bool NuPlayer::Decoder::handleAnInputBuffer(size_t index) {
    
    
	// 判断是否处在 处理不连续码流 的状态
    if (isDiscontinuityPending()) {
    
    
        return false;
    }

    sp<MediaCodecBuffer> buffer;
    mCodec->getInputBuffer(index, &buffer);

    if (index >= mInputBuffers.size()) {
    
    
        for (size_t i = mInputBuffers.size(); i <= index; ++i) {
    
    
            mInputBuffers.add();
            mInputBufferIsDequeued.add();
            mMediaBuffers.editItemAt(i) = NULL;
            mInputBufferIsDequeued.editItemAt(i) = false;
        }
    }
    mInputBuffers.editItemAt(index) = buffer;
    mInputBufferIsDequeued.editItemAt(index) = true;

	// 如果有码流不连续的情况,恢复播放后重新发送csd buffer
    if (!mCSDsToSubmit.isEmpty()) {
    
    
        sp<AMessage> msg = new AMessage();
        msg->setSize("buffer-ix", index);

        sp<ABuffer> buffer = mCSDsToSubmit.itemAt(0);
        msg->setBuffer("buffer", buffer);
        mCSDsToSubmit.removeAt(0);
        if (!onInputBufferFetched(msg)) {
    
    
            handleError(UNKNOWN_ERROR);
            return false;
        }
        return true;
    }
	// 如果有 buffer 没有成功写入 mediacodec 的情况,尝试重新写入
    while (!mPendingInputMessages.empty()) {
    
    
        sp<AMessage> msg = *mPendingInputMessages.begin();
        if (!onInputBufferFetched(msg)) {
    
    
            break;
        }
        mPendingInputMessages.erase(mPendingInputMessages.begin());
    }
	// 如果在 尝试重新写入的过程中,把当前 buffer 也顺带处理了,那么就直接返回
    if (!mInputBufferIsDequeued.editItemAt(index)) {
    
    
        return true;
    }
	// 将 buffer 记录到 mDequeuedInputBuffers 中
    mDequeuedInputBuffers.push_back(index);
	// 尝试从 source 获取数据,填充数据,并送回 decoder
    onRequestInputBuffers();
    return true;
}

Sourceデータの取得と入力バッファの書き込みがあり、さらにデータ取得失敗やデータ書き込み失敗の問題も考慮するため、入力バッファの処理フローはより複雑に見えますが、気にせずに一歩踏み出しましょう。分析。

まず、handleAnInputBuffer の動作を見てみましょう (一部のコメントは省略しています)。

  1. コード ストリームが現在処理中かどうかを確認します。
  2. MediaCodec から対応するインデックスを取得しますMediaCodecBuffer
  3. 取得した MediaCodecBuffer をインデックスに従って mInputBuffers リストに記録します。
  4. mInputBufferIsDequeuedインデックスに対応する入力バッファがデキューされたかどうかを記録するリストを作成します。
  5. フラッシュを実行するたびに、csd バッファをデコーダに送信する必要があります。これは毎回であることを忘れないでください。実際、すべてのデコーダ フラッシュに csd バッファが必要なわけではありません。
  6. 最初にキルトを処理します遅れ入力バッファ。遅延の理由については後で説明します。
  7. 遅延バッファを処理するとき、現在の入力バッファが処理される可能性があります。キューからのレコードのリスト内の対応する位置が false の場合、それは処理されたことを意味します。
  8. 入力バッファが処理されていない場合は、それを未処理リストに追加しますmDequeuedInputBuffers
  9. onRequestInputBuffers を呼び出して、Source からデータを読み取ります。

ここには 4 つのメソッドが含まれており、それらの名前は非常に似ています。まず、それらが何に使用されるかを紹介しましょう。

  • onRequestInputBuffers: ソースからの入力データをリクエストします。
  • doRequestBuffers: onRequestInputBuffers の内部実装。
  • onInputBufferFetched: Source からデータを正常に取得し、それを入力バッファに格納して、MediaCodec に返します。
  • fetchInputData: onInputBufferFetched の内部実装。

onRequestInputBuffers から始めましょう。このメソッドは DecoderBase に実装されており、権限は保護されています。

void NuPlayer::DecoderBase::onRequestInputBuffers() {
    
    
	// 判断是否处在 处理不连续码流 的状态
    if (mRequestInputBuffersPending) {
    
    
        return;
    }

    // doRequestBuffers() return true if we should request more data
    // 从 Source 请求数据,如果失败返回 true,发送一条延时消息,retry
    if (doRequestBuffers()) {
    
    
    	// retry 时不会继续处理 获取数据的调用
        mRequestInputBuffersPending = true;

        sp<AMessage> msg = new AMessage(kWhatRequestInputBuffers, this);
        msg->post(10 * 1000LL);
    }
}

onRequestInputBuffers は doRequestBuffers をカプセル化しているので、それらの機能は同じですが、1 つは基本クラスのメソッドで、もう 1 つはサブクラスのメソッドであることがわかります。このようなデザインは何の役に立つのでしょうか? 私の理解では、各DecoderはSourceからデータを取得する必要があるため、データを取得するためのメソッドonRequestInputBuffersが基本クラスに定義されていますが、Decoderごとにデータを取得する方法またはプロセスが異なるため、doRequestBuffersがサブクラス。で実現されます。サブクラスは親クラスのonRequestInputBuffersメソッドを呼び出して親クラスで定義したデータ読み込み処理を利用し、その途中でサブクラスのデータ読み込み実装を呼び出すことで、読み込み処理の統一と差別化の一石二鳥です。読書方法。

mRequestInputBuffersPendingSource からのデータの取得に失敗した場合は、遅延を待って再度取得を試行する必要があります。待機プロセス中に、外部から doRequestBuffers を呼び出してデータを取得する必要はありませんなので、mRequestInputBuffersPending を設定します。これは true で、待機状態を示します。この状態は、再試行メッセージが処理された場合にのみ解放されます。

bool NuPlayer::Decoder::doRequestBuffers() {
    
    
    if (isDiscontinuityPending()) {
    
    
        return false;
    }
    status_t err = OK;
    while (err == OK && !mDequeuedInputBuffers.empty()) {
    
    
        size_t bufferIx = *mDequeuedInputBuffers.begin();
        sp<AMessage> msg = new AMessage();
        msg->setSize("buffer-ix", bufferIx);
        err = fetchInputData(msg);
        if (err != OK && err != ERROR_END_OF_STREAM) {
    
    
            // if EOS, need to queue EOS buffer
            break;
        }
        mDequeuedInputBuffers.erase(mDequeuedInputBuffers.begin());

        if (!mPendingInputMessages.empty()
                || !onInputBufferFetched(msg)) {
    
    
            mPendingInputMessages.push_back(msg);
        }
    }

    return err == -EWOULDBLOCK
            && mSource->feedMoreTSData() == OK;
}

mDequeuedInputBuffersdoRequestBuffers には、現在その中にあるすべての入力バッファを処理するループがあります。ここで質問があります。mDequeuedInputBuffers のバッファーの数が 1 より大きくなるのはいつですか? ソースからのデータの読み込みに失敗した場合は直接リターンし、erase メソッドを呼び出すことができませんが、このとき mDequeuedInputBuffers の数は 1 より大きくなります。

status_t NuPlayer::Decoder::fetchInputData(sp<AMessage> &reply) {
    
    
    sp<ABuffer> accessUnit;
    bool dropAccessUnit = true;
    do {
    
    
    	// 从 Source 获取数据
        status_t err = mSource->dequeueAccessUnit(mIsAudio, &accessUnit);
		// 判断返回值,如果是 EWOULDBLOCK 那么说明没读到数据,如果是其他的返回值则说名读到数据
        if (err == -EWOULDBLOCK) {
    
    
            return err;
        } else if (err != OK) {
    
    
        	// 如果 error 不等于 OK,说明码流出现了一些情况
            if (err == INFO_DISCONTINUITY) {
    
    
                int32_t type;
                // 获取码流不连续的原因
                CHECK(accessUnit->meta()->findInt32("discontinuity", &type));

                bool formatChange =
                    (mIsAudio &&
                     (type & ATSParser::DISCONTINUITY_AUDIO_FORMAT))
                    || (!mIsAudio &&
                            (type & ATSParser::DISCONTINUITY_VIDEO_FORMAT));

                bool timeChange = (type & ATSParser::DISCONTINUITY_TIME) != 0;

                ALOGI("%s discontinuity (format=%d, time=%d)",
                        mIsAudio ? "audio" : "video", formatChange, timeChange);

                bool seamlessFormatChange = false;
                sp<AMessage> newFormat = mSource->getFormat(mIsAudio);
                // 如果是格式变化
                if (formatChange) {
    
    
                	// 判断当前播放的码流格式是否支持无缝切换
                    seamlessFormatChange =
                        supportsSeamlessFormatChange(newFormat);
                    // treat seamless format change separately
                    formatChange = !seamlessFormatChange;
                }

                // For format or time change, return EOS to queue EOS input,
                // then wait for EOS on output.
                // 如果不支持无缝切换,那么就要向 decoder 填充 eos
                if (formatChange /* not seamless */) {
    
    
                    mFormatChangePending = true;
                    err = ERROR_END_OF_STREAM;
                } else if (timeChange) {
    
    
                	// 如果pts不连续,那么就要向 decoder 填充 eos,恢复播放后要 发送 csd buffer
                    rememberCodecSpecificData(newFormat);
                    mTimeChangePending = true;
                    err = ERROR_END_OF_STREAM;
                } else if (seamlessFormatChange) {
    
    
                    // reuse existing decoder and don't flush
                    // 如果是无缝切换,那么仍要发送 csd buffer
                    rememberCodecSpecificData(newFormat);
                    continue;
                } else {
    
    
                    // This stream is unaffected by the discontinuity
                    return -EWOULDBLOCK;
                }
            }
            // reply should only be returned without a buffer set
            // when there is an error (including EOS)
            CHECK(err != OK);

            reply->setInt32("err", err);
            return ERROR_END_OF_STREAM;
        }
		// 以下是 drop 机制
        dropAccessUnit = false;
        if (!mIsAudio && !mIsEncrypted) {
    
    
			// 如果视频流慢了 100ms,视频为avc,并且不是参考帧,那么就drop掉当前读取的内容
            if (mRenderer->getVideoLateByUs() > 100000LL
                    && mIsVideoAVC
                    && !IsAVCReferenceFrame(accessUnit)) {
    
    
                dropAccessUnit = true;
            } 
            if (dropAccessUnit) {
    
    
                ++mNumInputFramesDropped;
            }
        }
    } while (dropAccessUnit);

    reply->setBuffer("buffer", accessUnit);

    return OK;
}

fetchInputData はデータを取得するだけでなく、コード ストリーム内の例外も処理します。dequeueAccessUnit には 4 つの戻り値があります。

  • OK: 有効なデータが取得されました。
  • -EWOULDBLOCK: データの読み取りに失敗しました。
  • INFO_DISCONTINUITY: コード ストリームが不連続です。
  • ERROR_END_OF_STREAM: ファイルの最後まで読み取ります。

OK および ERROR_END_OF_STREAM の戻り値は正常です。-EWOULDBLOCK は直接戻り、再試行を試みます。INFO_DISCONTINUITY は、selectTrack または Seek が呼び出されたためにコード ストリームが不連続であることを示します。

コード ストリームの不連続には 2 つのケースがあります。

  • コードストリームフォーマットの変更、エラーはDISCONTINUITY_VIDEO_FORMAT、形式の変更は幅と高さの変更、および MIME タイプの変更であり、selectTrack が呼び出された後に表示される可能性があります。
  • コードストリームポイントが連続していないDISCONTINUITY_TIME、シーク呼び出しの後、またはストリーム再生の最後にポイントがジャンプして戻るときに、エラーが表示されることがあります。

1 つはコード ストリームですフォーマットの変更 DISCONTINUITY_VIDEO_FORMAT原因となるフラッシュは、Track を選択するときに発生する可能性があります。もう 1 つは、コードストリームポイントラップアラウンド DISCONTINUITY_TIME、この状況はライブリプレイでより頻繁に発生します。

フォーマットが変更された場合、現在再生中のコードストリームがサポートされているかどうかを判定し、无缝切换(adaptive-playback)サポートされている場合はイベントを処理せず、サポートされていない場合は戻り値に ERROR_END_OF_STREAM が設定されます。

pts が不連続な場合、戻り値は ERROR_END_OF_STREAM に直接設定されます。ERROR_END_OF_STREAM が設定されているため、再生を再開した後、最初に csd バッファーを埋める必要があります。

fetchInputData は、AVC 形式のコード ストリームのドロップ メカニズムも設計しており、ビデオ ストリームがオーディオより 100 ミリ秒遅く、現在のフレームが参照フレームではない場合、フレームはドロップされます。

fetchInputData が正常に呼び出された後、onInputBufferFetched を呼び出して、取得したデータを入力バッファに入力し、MediaCodec に送り返す必要があります。これは比較的単純で、データをコピーするだけであり、注目するのは EOS だけです。EOS には 2 つの状況があり、1 つはバッファーが空であり、ERROR_END_OF_STREAM が受信されたことを示し、もう 1 つはバッファーが空ではなく戻り値は OK であるが、bufferMeta に eos 情報がある場合です。

コード ストリームが終了した場合、fetchInputData は eos 情報の送信後にデータを読み取りません。

コード ストリームが不連続であるために eos が送信された場合、入力バッファの処理フローは isDiscontinuityPending によって中断されます。前のデータがデコードされてレンダリングされるまで待ってから、Discontinuity イベントを処理します。, 処理が完了すると次のシーケンスのデータが書き込まれますが、この部分については次のセクションで見ていきます。

bool NuPlayer::Decoder::isDiscontinuityPending() const {
    
    
    return mFormatChangePending || mTimeChangePending;
}

3.2、CB_OUTPUT_AVAILABLE

出力バッファの処理フローは入力の処理フローよりもはるかに単純で、主に handleAnOutputBuffer メソッドを呼び出します。

bool NuPlayer::Decoder::handleAnOutputBuffer(
        size_t index,
        size_t offset,
        size_t size,
        int64_t timeUs,
        int32_t flags) {
    
    
    sp<MediaCodecBuffer> buffer;
    // 获取 output buffer
    mCodec->getOutputBuffer(index, &buffer);

    int64_t frameIndex;
    bool frameIndexFound = buffer->meta()->findInt64("frameIndex", &frameIndex);

    buffer->setRange(offset, size);
    // 设置 pts
    buffer->meta()->clear();
    buffer->meta()->setInt64("timeUs", timeUs);
    if (frameIndexFound) {
    
    
        buffer->meta()->setInt64("frameIndex", frameIndex);
    }
	// 判断 output buffer 是否到达 eos
    bool eos = flags & MediaCodec::BUFFER_FLAG_EOS;
    // we do not expect CODECCONFIG or SYNCFRAME for decoder

	// 创建 reply,设置 generation,avsync完成后 renderer 通过该消息 callback 回来
    sp<AMessage> reply = new AMessage(kWhatRenderBuffer, this);
    reply->setSize("buffer-ix", index);
    reply->setInt32("generation", mBufferGeneration);
    reply->setSize("size", size);
	// 如果出现 eos 则在 reply 中也进行标记
    if (eos) {
    
    
        ALOGV("[%s] saw output EOS", mIsAudio ? "audio" : "video");

        buffer->meta()->setInt32("eos", true);
        reply->setInt32("eos", true);
    }

    mNumFramesTotal += !mIsAudio;
	// 判断 input buffer 有没有设定起播时间
    if (mSkipRenderingUntilMediaTimeUs >= 0) {
    
    
        if (timeUs < mSkipRenderingUntilMediaTimeUs) {
    
    
            ALOGV("[%s] dropping buffer at time %lld as requested.",
                     mComponentName.c_str(), (long long)timeUs);

            reply->post();
            if (eos) {
    
    
                notifyResumeCompleteIfNecessary();
                if (mRenderer != NULL && !isDiscontinuityPending()) {
    
    
                    mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);
                }
            }
            return true;
        }
        mSkipRenderingUntilMediaTimeUs = -1;
    }

    // wait until 1st frame comes out to signal resume complete
    // 播放停止后重新恢复播放,等待第一帧到达后上抛消息,在seek时用到
    notifyResumeCompleteIfNecessary();

    if (mRenderer != NULL) {
    
    
        // send the buffer to renderer.
        // 将 ouput buffer 送到 renderer 做 avsync
        mRenderer->queueBuffer(mIsAudio, buffer, reply);
        // 如果到达 eos,并且不是因为码流中断,调用queueEOS
        if (eos && !isDiscontinuityPending()) {
    
    
            mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);
        }
    }

    return true;
}
  1. 出力バッファを取得します。
  2. 応答を作成し、世代を設定し、avsync が完了すると、レンダラーはこのメッセージを通じてデコーダーにコールバックします。
  3. 出力バッファ フラグが eos であるかどうかを確認し、そうである場合は応答でそれをマークします。
  4. キューの入力バッファーにはレンダリングを開始するための pts がある場合があります。出力バッファー pts がその pts よりも小さい場合、出力バッファー pts は直接削除されます。
  5. 出力バッファと応答メッセージを一緒にレンダラーに送信します。コード ストリームが不連続であるためではなく、eos に達した場合は、EOS がレンダラーに送信されます。

3.3、CB_OUTPUT_FORMAT_CHANGED

デコーダを使用する際には入力フォーマットを渡しますが、データ受信後にデコーダが自らフォーマットを解析し、出力フォーマット変更イベントをスローするため、上位層はイベント受信後に対応する処理を行う必要があります。

void NuPlayer::Decoder::handleOutputFormatChange(const sp<AMessage> &format) {
    
    
    if (!mIsAudio) {
    
    
        int32_t width, height;
        if (format->findInt32("width", &width)
                && format->findInt32("height", &height)) {
    
    
            Mutex::Autolock autolock(mStatsLock);
            mStats->setInt32("width", width);
            mStats->setInt32("height", height);
        }
        sp<AMessage> notify = mNotify->dup();
        notify->setInt32("what", kWhatVideoSizeChanged);
        notify->setMessage("format", format);
        notify->post();
    } else if (mRenderer != NULL) {
    
    
        uint32_t flags;
        int64_t durationUs;
        bool hasVideo = (mSource->getFormat(false /* audio */) != NULL);
        if (getAudioDeepBufferSetting() // override regardless of source duration
                || (mSource->getDuration(&durationUs) == OK
                        && durationUs > AUDIO_SINK_MIN_DEEP_BUFFER_DURATION_US)) {
    
    
            flags = AUDIO_OUTPUT_FLAG_DEEP_BUFFER;
        } else {
    
    
            flags = AUDIO_OUTPUT_FLAG_NONE;
        }

        sp<AMessage> reply = new AMessage(kWhatAudioOutputFormatChanged, this);
        reply->setInt32("generation", mBufferGeneration);
        mRenderer->changeAudioFormat(
                format, false /* offloadOnly */, hasVideo,
                flags, mSource->isStreaming(), reply);
    }
}

ビデオ形式が変更された場合は、イベントをスローし続けるだけです。オーディオ フォーマットが変更された場合、デコーダーは Renderer.changeAudioFormat を呼び出して AudioTrack を再度開く必要があります。これを処理する方法については、レンダラーの記事で簡単に紹介します。

3.4、kWhatRenderBuffer

前のセクションで、Renderer が avsync を完了した後、メッセージの形式で Decoder にコールバックすることを述べました。

        case kWhatRenderBuffer:
        {
    
    
            if (!isStaleReply(msg)) {
    
    
                onRenderBuffer(msg);
            }
            break;
        }

isStaleReply についてはすでに上で説明したので、ここでは詳しく説明しません。主に onRenderBuffer を見てみましょう。

void NuPlayer::Decoder::onRenderBuffer(const sp<AMessage> &msg) {
    
    
    status_t err;
    int32_t render;
    size_t bufferIx;
    int32_t eos;
    size_t size;
    // 查找要渲染的output buffer index
    CHECK(msg->findSize("buffer-ix", &bufferIx));

    if (mCodec == NULL) {
    
    
        err = NO_INIT;
    } else if (msg->findInt32("render", &render) && render) {
    
     // 判断是否render
        int64_t timestampNs;
        CHECK(msg->findInt64("timestampNs", &timestampNs));	// 获取render时间
        err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
    } else {
    
    
    	// 如果是 eos 或者 不render 则直接 drop
        if (!msg->findInt32("eos", &eos) || !eos ||
                !msg->findSize("size", &size) || size) {
    
    
            mNumOutputFramesDropped += !mIsAudio;
        }
        err = mCodec->releaseOutputBuffer(bufferIx);
    }
	// 如果是因为码流不连续造成的eos,则处理不连续事件
    if (msg->findInt32("eos", &eos) && eos
            && isDiscontinuityPending()) {
    
    
        finishHandleDiscontinuity(true /* flushOnTimeChange */);
    }
}

onRenderBuffer は主にビデオの処理に使用され、レンダラーはフレームをレンダリングする必要があると判断し、renderOutputBufferAndRelease を呼び出し、それ以外の場合は releaseOutputBuffer を呼び出します。

応答メッセージに eos が含まれている場合、eos がコード ストリームの不連続によって引き起こされているかどうかが判断されます。注意する必要があるのは、Renderer が実際に EOS を実行するとき、イベントは Decoder に送信されず、Decoder はバッファ イベントのみを処理することです。

ここで、finishHandleDiscontinuity がコード ストリーム例外をどのように処理するかを振り返ります。

void NuPlayer::Decoder::finishHandleDiscontinuity(bool flushOnTimeChange) {
    
    
    ALOGV("finishHandleDiscontinuity: format %d, time %d, flush %d",
            mFormatChangePending, mTimeChangePending, flushOnTimeChange);

    // If we have format change, pause and wait to be killed;
    // If we have time change only, flush and restart fetching.

    if (mFormatChangePending) {
    
    
        mPaused = true;
    } else if (mTimeChangePending) {
    
    
        if (flushOnTimeChange) {
    
    
            doFlush(false /* notifyComplete */);
            signalResume(false /* notifyComplete */);
        }
    }

    // Notify NuPlayer to either shutdown decoder, or rescan sources
    sp<AMessage> msg = mNotify->dup();
    msg->setInt32("what", kWhatInputDiscontinuity);
    msg->setInt32("formatChange", mFormatChangePending);
    msg->post();

    mFormatChangePending = false;
    mTimeChangePending = false;
}

コメントから、形式の変更と時間の変更が異なる方法で処理されることがわかります。

  • フォーマット変更: バッファ処理プロセスを一時停止し、デコーダが再起動されるのを待ちます。
  • 時間変更: フラッシュしてから、resume を呼び出して復元します。

形式の変更に一時停止が記載されています。mPaused が true に設定されている場合、onMessageReceived は送信されたバッファを処理しなくなります。この一時停止は再生の一時停止には使用されないフォーマット変更イベントは NuPlayer に送信される必要があります。

            if (what == DecoderBase::kWhatInputDiscontinuity) {
    
    
                int32_t formatChange;
                CHECK(msg->findInt32("formatChange", &formatChange));

                ALOGV("%s discontinuity: formatChange %d",
                        audio ? "audio" : "video", formatChange);

                if (formatChange) {
    
    
                    mDeferredActions.push_back(
                            new FlushDecoderAction(
                                audio ? FLUSH_CMD_SHUTDOWN : FLUSH_CMD_NONE,
                                audio ? FLUSH_CMD_NONE : FLUSH_CMD_SHUTDOWN));
                }

                mDeferredActions.push_back(
                        new SimpleAction(
                                &NuPlayer::performScanSources));

                processDeferredActions();
            } 

NuPlayer は FlushDecoderAction を実行し、シャットダウンを実行して現在のデコーダーを解放します。その後、performScanSources を再度呼び出して新しい形式のデコーダーを作成します。

4、シグナルフラッシュ

void NuPlayer::Decoder::doFlush(bool notifyComplete) {
    
    
    if (mCCDecoder != NULL) {
    
    
        mCCDecoder->flush();
    }

    if (mRenderer != NULL) {
    
    
        mRenderer->flush(mIsAudio, notifyComplete);
        mRenderer->signalTimeDiscontinuity();
    }

    status_t err = OK;
    if (mCodec != NULL) {
    
    
        err = mCodec->flush();
        mCSDsToSubmit = mCSDsForCurrentFormat; // copy operator
        ++mBufferGeneration;
    }

    if (err != OK) {
    
    
        ALOGE("failed to flush [%s] (err=%d)", mComponentName.c_str(), err);
        handleError(err);
        // finish with posting kWhatFlushCompleted.
        // we attempt to release the buffers even if flush fails.
    }
    releaseAndResetMediaBuffers();
    mPaused = true;
}

void NuPlayer::Decoder::onFlush() {
    
    
    doFlush(true);

    if (isDiscontinuityPending()) {
    
    
        // This could happen if the client starts seeking/shutdown
        // after we queued an EOS for discontinuities.
        // We can consider discontinuity handled.
        finishHandleDiscontinuity(false /* flushOnTimeChange */);
    }

    sp<AMessage> notify = mNotify->dup();
    notify->setInt32("what", kWhatFlushCompleted);
    notify->post();
}

フラッシュは比較的単純なので、くだらない話はしません。主なジョブは、Renderer のフラッシュを呼び出し、Renderer の状態をリセットし、MediaCodec のフラッシュを呼び出し、入力バッファと出力バッファを更新することです。このメソッド呼び出しは mBufferGeneration を変更し、最後に Decoder に格納されているバッファ リストをクリアすることに注意してください。

前のセクションで、doFlush がfinishHandleDiscontinuity で呼び出されたことを確認したため、NuPlayer に送信される kWhatFlushCompleted イベントはありません。

5、シャットダウンの開始

void NuPlayer::Decoder::onShutdown(bool notifyComplete) {
    
    
    status_t err = OK;

    // if there is a pending resume request, notify complete now
    notifyResumeCompleteIfNecessary();

    if (mCodec != NULL) {
    
    
    	// 释放decoder
        err = mCodec->release();
        // 释放 MediaCodec
        mCodec = NULL;
        // 修改 generation 阻止渲染
        ++mBufferGeneration;

        if (mSurface != NULL) {
    
    
            // reconnect to surface as MediaCodec disconnected from it
            status_t error = nativeWindowConnect(mSurface.get(), "onShutdown");
            ALOGW_IF(error != NO_ERROR,
                    "[%s] failed to connect to native window, error=%d",
                    mComponentName.c_str(), error);
        }
        mComponentName = "decoder";
    }
	// 释放 buffer list
    releaseAndResetMediaBuffers();

    if (err != OK) {
    
    
        ALOGE("failed to release [%s] (err=%d)", mComponentName.c_str(), err);
        handleError(err);
        // finish with posting kWhatShutdownCompleted.
    }

    if (notifyComplete) {
    
    
        sp<AMessage> notify = mNotify->dup();
        notify->setInt32("what", kWhatShutdownCompleted);
        notify->post();
        // 停止处理 buffer 事件
        mPaused = true;
    }
}

シャットダウンも非常に簡単です。

  1. MediaCodec の release メソッドを呼び出してデコーダを解放します。
  2. MediaCodec オブジェクトを解放します。
  3. 生成を変更し、レンダリング イベントの処理を停止します。
  4. バッファリストを解放します。
  5. バッファ イベントの処理を停止するには、mPaused を true に設定します。

6、シグナル再開

void NuPlayer::Decoder::onResume(bool notifyComplete) {
    
    
    mPaused = false;

    if (notifyComplete) {
    
    
        mResumePending = true;
    }

    if (mCodec == NULL) {
    
    
        ALOGE("[%s] onResume without a valid codec", mComponentName.c_str());
        handleError(NO_INIT);
        return;
    }
    mCodec->start();
}

MediaCodec リカバリ デコード プロセスを開始するには、フラッシュ後、signalResume を呼び出す必要があります。中心となるのは、MediaCodec の start メソッドを呼び出すことです。ここでの mResumePending は、デコーダーが最初のフレーム出力バッファーを送信するときに kWhatResumeCompleted を NuPlayer に送信する必要があるかどうかを決定するために使用されます。

7. まとめ

Android ALooper の仕組みもデザインパターンも深く理解していないということですので、今後もこの部分の勉強を強化していきたいと思います!

上記内容に誤りがある場合は、遠慮なくご指摘ください。

参考になったらぜひ「いいね」「まとめ」「フォロー」をお願いします。

他の Android Media フレームワークのコンテンツを読みたい場合は、https://blog.csdn.net/qq_41828351 ?spm=1000.2115.3001.5343 にもアクセスしてください。

おすすめ

転載: blog.csdn.net/qq_41828351/article/details/132589796