Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/dom/media/   (Firefox Browser Version 153.0.1©)  Datei vom 27.6.2026 mit Größe 57 kB image not shown  

Quelle  ExternalEngineStateMachine.cpp

  Sprache: C
 

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


#include "ExternalEngineStateMachine.h"

#include "PerformanceRecorder.h"
#ifdef MOZ_WMF_MEDIA_ENGINE
#  include "MFMediaEngineDecoderModule.h"
#  include "mozilla/MFMediaEngineChild.h"
#  include "mozilla/StaticPrefs_media.h"
#endif
#ifdef MOZ_WMF_CDM
#  include "mozilla/EMEUtils.h"
#endif
#include "VideoUtils.h"
#include "mozilla/AppShutdown.h"
#include "mozilla/Atomics.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/glean/DomMediaPlatformsWmfMetrics.h"
#include "nsPrintfCString.h"
#include "nsThreadUtils.h"

namespace mozilla {

extern LazyLogModule gMediaDecoderLog;

#define FMT(x, ...) \
  "Decoder=%p, State=%s, " x, mDecoderID, GetStateStr(), ##__VA_ARGS__
#define LOG(x, ...)                                                        \
  DDMOZ_LOG(gMediaDecoderLog, LogLevel::Debug, "Decoder=%p, State=%s, " x, \
            mDecoderID, GetStateStr(), ##__VA_ARGS__)
#define LOGV(x, ...)                                                         \
  DDMOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, "Decoder=%p, State=%s, " x, \
            mDecoderID, GetStateStr(), ##__VA_ARGS__)
#define LOGW(x, ...) NS_WARNING(nsPrintfCString(FMT(x, ##__VA_ARGS__)).get())
#define LOGE(x, ...)                                                   \
  NS_DebugBreak(NS_DEBUG_WARNING,                                      \
                nsPrintfCString(FMT(x, ##__VA_ARGS__)).get(), nullptr, \
                __FILE__, __LINE__)

const char* ExternalEngineEventToStr(ExternalEngineEvent aEvent) {
#define EVENT_TO_STR(event)        \
  case ExternalEngineEvent::event: \
    return #event
  switch (aEvent) {
    EVENT_TO_STR(LoadedMetaData);
    EVENT_TO_STR(LoadedFirstFrame);
    EVENT_TO_STR(LoadedData);
    EVENT_TO_STR(Waiting);
    EVENT_TO_STR(Playing);
    EVENT_TO_STR(Seeked);
    EVENT_TO_STR(BufferingStarted);
    EVENT_TO_STR(BufferingEnded);
    EVENT_TO_STR(Timeupdate);
    EVENT_TO_STR(Ended);
    EVENT_TO_STR(RequestForAudio);
    EVENT_TO_STR(RequestForVideo);
    EVENT_TO_STR(AudioEnough);
    EVENT_TO_STR(VideoEnough);
    default:
      MOZ_ASSERT_UNREACHABLE("Undefined event!");
      return "Undefined";
  }
#undef EVENT_TO_STR
}

/**
 * This class monitors the amount of crash happened for a remote engine
 * process. It the amount of crash of the remote process exceeds the defined
 * threshold, then `ShouldRecoverProcess()` will return false to indicate that
 * we should not keep spawning that remote process because it's too easy to
 * crash.
 *
 * In addition, we also have another mechanism in the media format reader
 * (MFR) to detect crash amount of remote processes, but that would only
 * happen during the decoding process. The main reason to choose using this
 * simple monitor, instead of the mechanism in the MFR is because that
 * mechanism can't detect every crash happening in the remote process, such as
 * crash happening during initializing the remote engine, or setting the CDM
 * pipepline, which can happen prior to decoding.
 */

class ProcessCrashMonitor final {
 public:
  static void NotifyCrash() {
    StaticMutexAutoLock lock(sMutex);
    auto* monitor = ProcessCrashMonitor::EnsureInstance();
    if (!monitor) {
      return;
    }
    monitor->mCrashNums++;
  }
  static bool ShouldRecoverProcess() {
    StaticMutexAutoLock lock(sMutex);
    auto* monitor = ProcessCrashMonitor::EnsureInstance();
    if (!monitor) {
      return false;
    }
    return monitor->mCrashNums <= monitor->mMaxCrashes;
  }
  ProcessCrashMonitor(const ProcessCrashMonitor&) = delete;
  ProcessCrashMonitor& operator=(const ProcessCrashMonitor&) = delete;

 private:
  ProcessCrashMonitor() : mCrashNums(0) {
#ifdef MOZ_WMF_MEDIA_ENGINE
    mMaxCrashes = StaticPrefs::media_wmf_media_engine_max_crashes();
#else
    mMaxCrashes = 0;
#endif
  };

  static ProcessCrashMonitor* EnsureInstance() {
    if (sIsShutdown) {
      return nullptr;
    }
    if (!sCrashMonitor) {
      sCrashMonitor.reset(new ProcessCrashMonitor());
      GetMainThreadSerialEventTarget()->Dispatch(
          NS_NewRunnableFunction("ProcessCrashMonitor::EnsureInstance", [&] {
            RunOnShutdown(
                [&] {
                  StaticMutexAutoLock lock(sMutex);
                  sCrashMonitor.reset();
                  sIsShutdown = true;
                },
                ShutdownPhase::XPCOMShutdown);
          }));
    }
    return sCrashMonitor.get();
  }

  static StaticMutex sMutex;
  static UniquePtr<ProcessCrashMonitor> sCrashMonitor;
  static Atomic<bool> sIsShutdown;

  uint32_t mCrashNums;
  uint32_t mMaxCrashes;
};

StaticMutex ProcessCrashMonitor::sMutex;
constinit UniquePtr<ProcessCrashMonitor> ProcessCrashMonitor::sCrashMonitor;
Atomic<bool> ProcessCrashMonitor::sIsShutdown{false};

/* static */
const char* ExternalEngineStateMachine::StateToStr(State aNextState) {
#define STATE_TO_STR(state) \
  case State::state:        \
    return #state
  switch (aNextState) {
    STATE_TO_STR(InitEngine);
    STATE_TO_STR(ReadingMetadata);
    STATE_TO_STR(RunningEngine);
    STATE_TO_STR(SeekingData);
    STATE_TO_STR(ShutdownEngine);
    STATE_TO_STR(RecoverEngine);
    default:
      MOZ_ASSERT_UNREACHABLE("Undefined state!");
      return "Undefined";
  }
#undef STATE_TO_STR
}

const char* ExternalEngineStateMachine::GetStateStr() const {
  return StateToStr(mState.mName);
}

static bool IsBeingProfiledOrLogEnabled() {
  return MOZ_LOG_TEST(gMediaDecoderLog, LogLevel::Info) ||
         profiler_thread_is_being_profiled_for_markers();
}

void ExternalEngineStateMachine::ChangeStateTo(State aNextState) {
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("Change state : '%s' -> '%s' (play-state=%d)",
                        StateToStr(mState.mName), StateToStr(aNextState),
                        mPlayState.Ref());
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::ChangeStateTo", MEDIA_PLAYBACK, {}, msg);
  }
  // Assert the possible state transitions.
  MOZ_ASSERT_IF(
      mState.IsReadingMetadata(),
      aNextState == State::InitEngine || aNextState == State::ShutdownEngine);
  MOZ_ASSERT_IF(mState.IsInitEngine(), aNextState == State::RunningEngine ||
                                           aNextState == State::ShutdownEngine);
  MOZ_ASSERT_IF(mState.IsRunningEngine(),
                aNextState == State::SeekingData ||
                    aNextState == State::ShutdownEngine ||
                    aNextState == State::RecoverEngine);
  MOZ_ASSERT_IF(mState.IsSeekingData(),
                aNextState == State::RunningEngine ||
                    aNextState == State::ShutdownEngine ||
                    aNextState == State::RecoverEngine);
  MOZ_ASSERT_IF(mState.IsShutdownEngine(), aNextState == State::ShutdownEngine);
  MOZ_ASSERT_IF(
      mState.IsRecoverEngine(),
      aNextState == State::SeekingData || aNextState == State::ShutdownEngine);
  if (aNextState == State::SeekingData) {
    mState = StateObject({StateObject::SeekingData()});
  } else if (aNextState == State::InitEngine) {
    mState = StateObject({StateObject::InitEngine()});
  } else if (aNextState == State::RunningEngine) {
    mState = StateObject({StateObject::RunningEngine()});
  } else if (aNextState == State::ShutdownEngine) {
    mState = StateObject({StateObject::ShutdownEngine()});
  } else if (aNextState == State::RecoverEngine) {
    mState = StateObject({StateObject::RecoverEngine()});
  } else {
    MOZ_ASSERT_UNREACHABLE("Wrong state!");
  }
  NotifyAudibleStateChangeIfNeeded();
}

ExternalEngineStateMachine::ExternalEngineStateMachine(
    MediaDecoder* aDecoder, MediaFormatReader* aReader)
    : MediaDecoderStateMachineBase(aDecoder, aReader) {
  LOG("Created ExternalEngineStateMachine");
  MOZ_ASSERT(mState.IsReadingMetadata());
  ReadMetadata();
}

ExternalEngineStateMachine::~ExternalEngineStateMachine() {
  LOG("ExternalEngineStateMachine is destroyed");
}

void ExternalEngineStateMachine::InitEngine() {
  MOZ_ASSERT(mState.IsInitEngine() || mState.IsRecoverEngine());
#ifdef MOZ_WMF_MEDIA_ENGINE
  mEngine.reset(new MFMediaEngineWrapper(this, mFrameStats));
#endif
  if (mEngine) {
    MOZ_ASSERT(mInfo);
    if (IsBeingProfiledOrLogEnabled()) {
      nsPrintfCString msg{"mMinimizePreroll %d IsEncryptedCustomIdent %d",
                          mMinimizePreroll, mReader->IsEncryptedCustomIdent()};
      LOG("Init engine, %s", msg.get());
      PROFILER_MARKER_TEXT("EESM::InitEngine", MEDIA_PLAYBACK, {}, msg);
    }
    auto* state = mState.AsInitEngine();
    ExternalPlaybackEngine::InitFlagSet flags;
    if (mMinimizePreroll) {
      flags += ExternalPlaybackEngine::InitFlag::ShouldPreload;
    }
    if (mReader->IsEncryptedCustomIdent()) {
      flags += ExternalPlaybackEngine::InitFlag::EncryptedCustomIdent;
    }
    state->mInitPromise = mEngine->Init(*mInfo, flags);
    state->mInitPromise
        ->Then(OwnerThread(), __func__, this,
               &ExternalEngineStateMachine::OnEngineInitSuccess,
               &ExternalEngineStateMachine::OnEngineInitFailure)
        ->Track(state->mEngineInitRequest);
  }
}

void ExternalEngineStateMachine::OnEngineInitSuccess() {
  AssertOnTaskQueue();
  AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnEngineInitSuccess",
                      MEDIA_PLAYBACK);
  MOZ_ASSERT(mState.IsInitEngine() || mState.IsRecoverEngine());
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("Initialized the external playback engine %" PRIu64,
                        mEngine->Id());
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::OnEngineInitSuccess", MEDIA_PLAYBACK, {}, msg);
  }
  auto* state = mState.AsInitEngine();
  state->mEngineInitRequest.Complete();
  mReader->UpdateMediaEngineId(mEngine->Id());
  state->mInitPromise = nullptr;
  mIsEngineReady = true;
  if (mState.IsInitEngine()) {
    StartRunningEngine();
    return;
  }
  // We just recovered from CDM process crash, seek to previous position.
  SeekTarget target(mCurrentPosition.Ref(), SeekTarget::Type::Accurate);
  Seek(target);
}

void ExternalEngineStateMachine::OnEngineInitFailure() {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsInitEngine() || mState.IsRecoverEngine());
  LOGE("Failed to initialize the external playback engine");
  PROFILER_MARKER_UNTYPED("EESM::OnEngineInitFailure", MEDIA_PLAYBACK);
  mIsEngineReady = false;
  auto* state = mState.AsInitEngine();
  state->mEngineInitRequest.Complete();
  state->mInitPromise = nullptr;
  // Even if we failed to initialize the media engine, we still want to try
  // again with the normal state machine, so don't return a fatal error, return
  // NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR instead.
  ReportTelemetry(NS_ERROR_DOM_MEDIA_MEDIA_ENGINE_INITIALIZATION_ERR);
  DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR,
                          __func__));
}

void ExternalEngineStateMachine::ReadMetadata() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mState.IsReadingMetadata());
  PROFILER_MARKER_UNTYPED("EESM::ReadMetadata", MEDIA_PLAYBACK);
  (void)OwnerThread()->Dispatch(NS_NewRunnableFunction(
      "ExternalEngineStateMachine::ReadMetadata",
      [self = RefPtr<ExternalEngineStateMachine>{this}, this] {
        mReader->ReadMetadata()
            ->Then(OwnerThread(), __func__, this,
                   &ExternalEngineStateMachine::OnMetadataRead,
                   &ExternalEngineStateMachine::OnMetadataNotRead)
            ->Track(mState.AsReadingMetadata()->mMetadataRequest);
      }));
}

void ExternalEngineStateMachine::OnMetadataRead(MetadataHolder&& aMetadata) {
  AssertOnTaskQueue();
  AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnMetadataRead",
                      MEDIA_PLAYBACK);
  MOZ_ASSERT(mState.IsReadingMetadata());
  LOG("OnMetadataRead");

  mState.AsReadingMetadata()->mMetadataRequest.Complete();
  mInfo.emplace(*aMetadata.mInfo);
  mMediaSeekable = Info().mMediaSeekable;
  mMediaSeekableOnlyInBufferedRanges =
      Info().mMediaSeekableOnlyInBufferedRanges;

  if (!IsFormatSupportedByExternalEngine(*mInfo)) {
    // The external engine doesn't support the type, try to notify the decoder
    // to use our own state machine again. Not a real "error", because it would
    // fallback to another state machine.
    DecodeError(
        MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
    return;
  }

#ifdef MOZ_WMF_MEDIA_ENGINE
  // Only support encrypted playback. Not a real "error", because it would
  // fallback to another state machine.
  if ((!mInfo->IsEncrypted() && !mReader->IsEncryptedCustomIdent()) &&
      StaticPrefs::media_wmf_media_engine_enabled() == 2) {
    LOG("External engine only supports encrypted playback by the pref");
    DecodeError(
        MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
    return;
  }
#endif

  if (Info().mMetadataDuration.isSome()) {
    mDuration = Info().mMetadataDuration;
  } else if (Info().mUnadjustedMetadataEndTime.isSome()) {
    const media::TimeUnit unadjusted = Info().mUnadjustedMetadataEndTime.ref();
    const media::TimeUnit adjustment = Info().mStartTime;
    mInfo->mMetadataDuration.emplace(unadjusted - adjustment);
    mDuration = Info().mMetadataDuration;
  }

  // If we don't know the duration by this point, we assume infinity, per spec.
  if (mDuration.Ref().isNothing()) {
    mDuration = Some(media::TimeUnit::FromInfinity());
  }
  MOZ_ASSERT(mDuration.Ref().isSome());

  if (mInfo->HasVideo()) {
    mVideoDisplay = mInfo->mVideo.mDisplay;
  }

  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg(
        "a=%s, v=%s, size=[%dx%d], duration=%s, encrypted=%d, "
        "IsEncryptedCustomIdent=%d",
        mInfo->HasAudio() ? mInfo->mAudio.mMimeType.get() : "none",
        mInfo->HasVideo() ? mInfo->mVideo.mMimeType.get() : "none",
        mVideoDisplay.width, mVideoDisplay.height,
        mDuration.Ref()->ToString().get(), mInfo->IsEncrypted(),
        mReader->IsEncryptedCustomIdent());
    LOG("Metadata loaded : %s", msg.get());
    PROFILER_MARKER_TEXT("EESM::OnMetadataRead", MEDIA_PLAYBACK, {}, msg);
  }

  mMetadataLoadedEvent.Notify(std::move(aMetadata.mInfo),
                              std::move(aMetadata.mTags),
                              MediaDecoderEventVisibility::Observable);
  ChangeStateTo(State::InitEngine);
  InitEngine();
}

void ExternalEngineStateMachine::OnMetadataNotRead(const MediaResult& aError) {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsReadingMetadata());
  LOGE("Decode metadata failed, shutting down decoder");
  PROFILER_MARKER_UNTYPED("EESM::OnMetadataNotRead", MEDIA_PLAYBACK);
  mState.AsReadingMetadata()->mMetadataRequest.Complete();
  ReportTelemetry(aError);
  DecodeError(aError);
}

bool ExternalEngineStateMachine::IsFormatSupportedByExternalEngine(
    const MediaInfo& aInfo) {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsReadingMetadata());
#ifdef MOZ_WMF_MEDIA_ENGINE
  const bool audioSupported =
      !aInfo.HasAudio() ||
      MFMediaEngineDecoderModule::SupportsConfig(aInfo.mAudio);
  const bool videoSupported =
      !aInfo.HasVideo() ||
      MFMediaEngineDecoderModule::SupportsConfig(aInfo.mVideo);
  LOG("audio=%s (supported=%d), video=%s(supported=%d)",
      aInfo.HasAudio() ? aInfo.mAudio.mMimeType.get() : "none", audioSupported,
      aInfo.HasVideo() ? aInfo.mVideo.mMimeType.get() : "none", videoSupported);
  return audioSupported && videoSupported;
#else
  return false;
#endif
}

RefPtr<MediaDecoder::SeekPromise> ExternalEngineStateMachine::InvokeSeek(
    const SeekTarget& aTarget) {
  return InvokeAsync(
      OwnerThread(), __func__,
      [self = RefPtr<ExternalEngineStateMachine>(this), this,
       target = aTarget]() -> RefPtr<MediaDecoder::SeekPromise> {
        AssertOnTaskQueue();
        if (mState.IsShutdownEngine()) {
          return MediaDecoder::SeekPromise::CreateAndReject(true, __func__);
        }
        if (!mIsEngineReady) {
          LOG("Can't perform seek (%" PRId64 ") now, add a pending seek task",
              target.GetTime().ToMicroseconds());
          // We haven't added any pending seek before
          if (mPendingSeek.mPromise.IsEmpty()) {
            mPendingTasks.AppendElement(NS_NewRunnableFunction(
                "ExternalEngineStateMachine::InvokeSeek",
                [self = RefPtr{this}, this] {
                  if (!mPendingSeek.Exists()) {
                    return;
                  }
                  Seek(*mPendingSeek.mTarget)
                      ->Then(OwnerThread(), __func__,
                             [self = RefPtr{this},
                              this](const MediaDecoder::SeekPromise::
                                        ResolveOrRejectValue& aVal) {
                               mPendingSeekRequest.Complete();
                               if (aVal.IsResolve()) {
                                 mPendingSeek.Resolve(__func__);
                               } else {
                                 mPendingSeek.RejectIfExists(__func__);
                               }
                               mPendingSeek = SeekJob();
                             })
                      ->Track(mPendingSeekRequest);
                }));
          } else {
            // Reject previous pending promise, as we will create a new one
            LOG("Replace previous pending seek with a new one");
            mPendingSeek.RejectIfExists(__func__);
            mPendingSeekRequest.DisconnectIfExists();
          }
          mPendingSeek.mTarget = Some(target);
          return mPendingSeek.mPromise.Ensure(__func__);
        }
        if (mPendingSeek.Exists()) {
          LOG("Discard pending seek because another new seek happens");
          mPendingSeek.RejectIfExists(__func__);
          mPendingSeek = SeekJob();
          mPendingSeekRequest.DisconnectIfExists();
        }
        return self->Seek(target);
      });
}

RefPtr<MediaDecoder::SeekPromise> ExternalEngineStateMachine::Seek(
    const SeekTarget& aTarget) {
  AssertOnTaskQueue();
  if (!mState.IsRunningEngine() && !mState.IsSeekingData() &&
      !mState.IsRecoverEngine()) {
    MOZ_ASSERT(false"Can't seek due to unsupported state.");
    return MediaDecoder::SeekPromise::CreateAndReject(true, __func__);
  }
  // We don't support these type of seek, because they're depending on the
  // implementation of the external engine, which might not be supported.
  if (aTarget.IsNextFrame() || aTarget.IsVideoOnly()) {
    return MediaDecoder::SeekPromise::CreateAndReject(true, __func__);
  }
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("Start seeking to %" PRId64,
                        aTarget.GetTime().ToMicroseconds());
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::Seek", MEDIA_PLAYBACK, {}, msg);
  }
  auto* state = mState.AsSeekingData();
  if (!state) {
    // We're in other states, so change the state to seeking.
    ChangeStateTo(State::SeekingData);
    state = mState.AsSeekingData();
  }
  state->SetTarget(aTarget);

  // Update related status.
  mSentPlaybackEndedEvent = false;
  mOnPlaybackEvent.Notify(MediaPlaybackEvent::SeekStarted);
  mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING);

  // Notify the external playback engine about seeking. After the engine changes
  // its current time, it would send `seeked` event.
  mEngine->Seek(aTarget.GetTime());
  state->mWaitingEngineSeeked = true;
  SeekReader();
  return state->mSeekJob.mPromise.Ensure(__func__);
}

void ExternalEngineStateMachine::SeekReader() {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsSeekingData());
  auto* state = mState.AsSeekingData();

  // Reset the reader first and ask it to perform a demuxer seek.
  ResetDecode();
  state->mWaitingReaderSeeked = true;
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("Seek reader to %" PRId64,
                        state->GetTargetTime().ToMicroseconds());
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::SeekReader", MEDIA_PLAYBACK, {}, msg);
  }
  mReader->Seek(state->mSeekJob.mTarget.ref())
      ->Then(OwnerThread(), __func__, this,
             &ExternalEngineStateMachine::OnSeekResolved,
             &ExternalEngineStateMachine::OnSeekRejected)
      ->Track(state->mSeekRequest);
}

void ExternalEngineStateMachine::OnSeekResolved(const media::TimeUnit& aUnit) {
  AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnSeekResolved",
                      MEDIA_PLAYBACK);
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsSeekingData());
  auto* state = mState.AsSeekingData();

  LOG("OnReaderSeekResolved");
  PROFILER_MARKER_UNTYPED("EESM::OnReaderSeekResolved", MEDIA_PLAYBACK);
  state->mSeekRequest.Complete();
  state->mWaitingReaderSeeked = false;

  // Start sending new data to the external playback engine.
  if (HasAudio()) {
    mHasEnoughAudio = false;
    OnRequestAudio();
  }
  if (HasVideo()) {
    mHasEnoughVideo = false;
#ifdef MOZ_WMF_CDM
    mVideoEOSSentToEngine = false;
#endif
    OnRequestVideo();
  }
  CheckIfSeekCompleted();
}

void ExternalEngineStateMachine::OnSeekRejected(
    const SeekRejectValue& aReject) {
  AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnSeekRejected",
                      MEDIA_PLAYBACK);
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsSeekingData());
  auto* state = mState.AsSeekingData();

  LOG("OnReaderSeekRejected");
  PROFILER_MARKER_UNTYPED("EESM::OnReaderSeekRejected", MEDIA_PLAYBACK);
  state->mSeekRequest.Complete();
  if (aReject.mError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
    LOG("OnSeekRejected reason=WAITING_FOR_DATA type=%s",
        MediaData::EnumValueToString(aReject.mType));
    MOZ_ASSERT_IF(aReject.mType == MediaData::Type::AUDIO_DATA,
                  !IsRequestingAudioData());
    MOZ_ASSERT_IF(aReject.mType == MediaData::Type::VIDEO_DATA,
                  !IsRequestingVideoData());
    MOZ_ASSERT_IF(aReject.mType == MediaData::Type::AUDIO_DATA,
                  !IsWaitingAudioData());
    MOZ_ASSERT_IF(aReject.mType == MediaData::Type::VIDEO_DATA,
                  !IsWaitingVideoData());

    // Fire 'waiting' to notify the player that we are waiting for data.
    mOnNextFrameStatus.Notify(
        MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING);
    WaitForData(aReject.mType);
    return;
  }

  if (aReject.mError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
    EndOfStream(aReject.mType);
    return;
  }

  MOZ_ASSERT(NS_FAILED(aReject.mError),
             "Cancels should also disconnect mSeekRequest");
  state->RejectIfExists(__func__);
  ReportTelemetry(aReject.mError);
  DecodeError(aReject.mError);
}

bool ExternalEngineStateMachine::IsSeeking() {
  AssertOnTaskQueue();
  const auto* state = mState.AsSeekingData();
  return state && state->IsSeeking();
}

void ExternalEngineStateMachine::CheckIfSeekCompleted() {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsSeekingData());
  auto* state = mState.AsSeekingData();
  if (state->mWaitingEngineSeeked || state->mWaitingReaderSeeked) {
    LOG("Seek hasn't been completed yet, waitEngineSeeked=%d, "
        "waitReaderSeeked=%d",
        state->mWaitingEngineSeeked, state->mWaitingReaderSeeked);
    return;
  }

  // As seeking should be accurate and we can't control the exact timing inside
  // the external media engine. We always set the newCurrentTime = seekTime
  // so that the updated HTMLMediaElement.currentTime will always be the seek
  // target.
  if (state->GetTargetTime() != mCurrentPosition) {
    LOG("Force adjusting current time (%" PRId64
        ") to match to target (%" PRId64 ")",
        mCurrentPosition.Ref().ToMicroseconds(),
        state->GetTargetTime().ToMicroseconds());
    mCurrentPosition = state->GetTargetTime();
  }

  LOG("Seek completed");
  PROFILER_MARKER_TEXT(
      "EESM::SeekCompleted", MEDIA_PLAYBACK, {},
      nsPrintfCString("currentTime %" PRId64,
                      mCurrentPosition.Ref().ToMicroseconds()));
  state->Resolve(__func__);
  mOnPlaybackEvent.Notify(MediaPlaybackEvent::Invalidate);
  mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
  StartRunningEngine();
}

void ExternalEngineStateMachine::ResetDecode() {
  AssertOnTaskQueue();
  if (!mInfo) {
    return;
  }

  LOG("ResetDecode");
  MediaFormatReader::TrackSet tracks;
  if (HasVideo()) {
    mVideoDataRequest.DisconnectIfExists();
    mVideoWaitRequest.DisconnectIfExists();
    tracks += TrackInfo::kVideoTrack;
  }
  if (HasAudio()) {
    mAudioDataRequest.DisconnectIfExists();
    mAudioWaitRequest.DisconnectIfExists();
    tracks += TrackInfo::kAudioTrack;
  }
  mReader->ResetDecode(tracks);
}

RefPtr<GenericPromise> ExternalEngineStateMachine::InvokeSetSink(
    const RefPtr<AudioDeviceInfo>& aSink) {
  MOZ_ASSERT(NS_IsMainThread());
  // TODO : can media engine support this?
  return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
}

RefPtr<ShutdownPromise> ExternalEngineStateMachine::Shutdown() {
  AssertOnTaskQueue();
  if (mState.IsShutdownEngine()) {
    LOG("Already shutdown");
    return mState.AsShutdownEngine()->mShutdown;
  }

  LOG("Shutdown");
  mIsEngineReady = false;
  ChangeStateTo(State::ShutdownEngine);
  ResetDecode();

  mAudioDataRequest.DisconnectIfExists();
  mVideoDataRequest.DisconnectIfExists();
  mAudioWaitRequest.DisconnectIfExists();
  mVideoWaitRequest.DisconnectIfExists();

  mDuration.DisconnectAll();
  mCurrentPosition.DisconnectAll();
  mIsAudioDataAudible.DisconnectAll();

  mMetadataManager.Disconnect();

  mSetCDMProxyPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_ABORT_ERR, __func__);
  mSetCDMProxyRequest.DisconnectIfExists();

  mPendingSeek.RejectIfExists(__func__);
  mPendingSeekRequest.DisconnectIfExists();

  mPendingTasks.Clear();

  if (mEngine) {
    mEngine->Shutdown();
  }

  auto* state = mState.AsShutdownEngine();
  state->mShutdown = mReader->Shutdown()->Then(
      OwnerThread(), __func__, [self = RefPtr{this}, this]() {
        LOG("Shutting down state machine task queue");
        return OwnerThread()->BeginShutdown();
      });
  return state->mShutdown;
}

void ExternalEngineStateMachine::BufferedRangeUpdated() {
  AssertOnTaskQueue();
  AUTO_PROFILER_LABEL("ExternalEngineStateMachine::BufferedRangeUpdated",
                      MEDIA_PLAYBACK);

  // While playing an unseekable stream of unknown duration, mDuration
  // is updated as we play. But if data is being downloaded
  // faster than played, mDuration won't reflect the end of playable data
  // since we haven't played the frame at the end of buffered data. So update
  // mDuration here as new data is downloaded to prevent such a lag.
  if (mBuffered.Ref().IsInvalid()) {
    return;
  }

  bool exists;
  media::TimeUnit end{mBuffered.Ref().GetEnd(&exists)};
  if (!exists) {
    return;
  }

  // Use estimated duration from buffer ranges when mDuration is unknown or
  // the estimated duration is larger.
  if (mDuration.Ref().isNothing() || mDuration.Ref()->IsInfinite() ||
      end > mDuration.Ref().ref()) {
    mDuration = Some(end);
    DDLOG(DDLogCategory::Property, "duration_us",
          mDuration.Ref()->ToMicroseconds());
  }
}

#define PERFORM_WHEN_ALLOW(Func)                                          \
  do {                                                                    \
    if (mState.IsShutdownEngine() || mHasFatalError ||                    \
        AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { \
      return;                                                             \
    }                                                                     \
    if (!mIsEngineReady) {                                                \
      LOG("%s is called before init", __func__);                          \
      mPendingTasks.AppendElement(NewRunnableMethod(                      \
          __func__, this, &ExternalEngineStateMachine::Func));            \
      return;                                                             \
    }                                                                     \
  } while (false)

void ExternalEngineStateMachine::SetPlaybackRate(double aPlaybackRate) {
  AssertOnTaskQueue();
  // TODO : consider to make `mPlaybackRate` a mirror to fit other usages like
  // `mVolume` and `mPreservesPitch`.
  mPlaybackRate = aPlaybackRate;
  PlaybackRateChanged();
}

void ExternalEngineStateMachine::PlaybackRateChanged() {
  AssertOnTaskQueue();
  PERFORM_WHEN_ALLOW(PlaybackRateChanged);
  MOZ_ASSERT(mState.IsReadingMetadata() || mState.IsRunningEngine() ||
             mState.IsSeekingData());
  mEngine->SetPlaybackRate(mPlaybackRate);
}

void ExternalEngineStateMachine::VolumeChanged() {
  AssertOnTaskQueue();
  PERFORM_WHEN_ALLOW(VolumeChanged);
  MOZ_ASSERT(mState.IsReadingMetadata() || mState.IsRunningEngine() ||
             mState.IsSeekingData());
  mEngine->SetVolume(mVolume);
}

void ExternalEngineStateMachine::PreservesPitchChanged() {
  AssertOnTaskQueue();
  PERFORM_WHEN_ALLOW(PreservesPitchChanged);
  MOZ_ASSERT(mState.IsReadingMetadata() || mState.IsRunningEngine() ||
             mState.IsSeekingData());
  mEngine->SetPreservesPitch(mPreservesPitch);
}

void ExternalEngineStateMachine::PlayStateChanged() {
  AssertOnTaskQueue();
  PERFORM_WHEN_ALLOW(PlayStateChanged);
  MOZ_ASSERT(mState.IsReadingMetadata() || mState.IsRunningEngine() ||
             mState.IsSeekingData());
  if (mPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
    mEngine->Play();
  } else if (mPlayState == MediaDecoder::PLAY_STATE_PAUSED) {
    mEngine->Pause();
  }
  NotifyAudibleStateChangeIfNeeded();
}

void ExternalEngineStateMachine::LoopingChanged() {
  AssertOnTaskQueue();
  PERFORM_WHEN_ALLOW(LoopingChanged);
  MOZ_ASSERT(mState.IsReadingMetadata() || mState.IsRunningEngine() ||
             mState.IsSeekingData());
  mEngine->SetLooping(mLooping);
}

#undef PERFORM_WHEN_ALLOW

void ExternalEngineStateMachine::EndOfStream(MediaData::Type aType) {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
  static auto DataTypeToTrackType = [](const MediaData::Type& aType) {
    if (aType == MediaData::Type::VIDEO_DATA) {
      return TrackInfo::TrackType::kVideoTrack;
    }
    if (aType == MediaData::Type::AUDIO_DATA) {
      return TrackInfo::TrackType::kAudioTrack;
    }
    return TrackInfo::TrackType::kUndefinedTrack;
  };
#ifdef MOZ_WMF_CDM
  if (aType == MediaData::Type::VIDEO_DATA) {
    mVideoEOSSentToEngine = true;
  }
#endif
  mEngine->NotifyEndOfStream(DataTypeToTrackType(aType));
}

void ExternalEngineStateMachine::WaitForData(MediaData::Type aType) {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
  AUTO_PROFILER_LABEL("ExternalEngineStateMachine::WaitForData",
                      MEDIA_PLAYBACK);
  MOZ_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
             aType == MediaData::Type::VIDEO_DATA);

  LOG("WaitForData");
  RefPtr<ExternalEngineStateMachine> self = this;
  if (aType == MediaData::Type::AUDIO_DATA) {
    MOZ_ASSERT(HasAudio());
    mReader->WaitForData(MediaData::Type::AUDIO_DATA)
        ->Then(
            OwnerThread(), __func__,
            [self, this](MediaData::Type aType) {
              AUTO_PROFILER_LABEL(
                  "ExternalEngineStateMachine::WaitForData:AudioResolved",
                  MEDIA_PLAYBACK);
              MOZ_ASSERT(aType == MediaData::Type::AUDIO_DATA);
              LOG("Done waiting for audio data");
              mAudioWaitRequest.Complete();
              MaybeFinishWaitForData();
            },
            [self, this](const WaitForDataRejectValue& aRejection) {
              AUTO_PROFILER_LABEL(
                  "ExternalEngineStateMachine::WaitForData:AudioRejected",
                  MEDIA_PLAYBACK);
              mAudioWaitRequest.Complete();
              DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
            })
        ->Track(mAudioWaitRequest);
  } else {
    MOZ_ASSERT(HasVideo());
    mReader->WaitForData(MediaData::Type::VIDEO_DATA)
        ->Then(
            OwnerThread(), __func__,
            [self, this](MediaData::Type aType) {
              AUTO_PROFILER_LABEL(
                  "ExternalEngineStateMachine::WaitForData:VideoResolved",
                  MEDIA_PLAYBACK);
              MOZ_ASSERT(aType == MediaData::Type::VIDEO_DATA);
              LOG("Done waiting for video data");
              mVideoWaitRequest.Complete();
              MaybeFinishWaitForData();
            },
            [self, this](const WaitForDataRejectValue& aRejection) {
              AUTO_PROFILER_LABEL(
                  "ExternalEngineStateMachine::WaitForData:VideoRejected",
                  MEDIA_PLAYBACK);
              mVideoWaitRequest.Complete();
              DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
            })
        ->Track(mVideoWaitRequest);
  }
}

void ExternalEngineStateMachine::MaybeFinishWaitForData() {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());

  bool isWaitingForAudio = HasAudio() && mAudioWaitRequest.Exists();
  bool isWaitingForVideo = HasVideo() && mVideoWaitRequest.Exists();
  if (isWaitingForAudio || isWaitingForVideo) {
    LOG("Still waiting for data (waitAudio=%d, waitVideo=%d)",
        isWaitingForAudio, isWaitingForVideo);
    return;
  }

  LOG("Finished waiting for data");
  if (mState.IsSeekingData()) {
    SeekReader();
    return;
  }
  if (HasAudio()) {
    RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
  }
  if (HasVideo()) {
    RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
  }
}

void ExternalEngineStateMachine::StartRunningEngine() {
  ChangeStateTo(State::RunningEngine);
  // Manually check the play state because the engine might be recovered from
  // crash or just get recreated, so PlayStateChanged() won't be triggered.
  if (mPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
    mEngine->Play();
  }
  if (HasAudio()) {
    RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
  }
  if (HasVideo()) {
    RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
  }
  // Run tasks which was called before the engine is ready.
  if (!mPendingTasks.IsEmpty()) {
    for (auto& task : mPendingTasks) {
      (void)OwnerThread()->Dispatch(task.forget());
    }
    mPendingTasks.Clear();
  }
}

void ExternalEngineStateMachine::RunningEngineUpdate(MediaData::Type aType) {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
  if (aType == MediaData::Type::AUDIO_DATA && !mHasEnoughAudio) {
    OnRequestAudio();
  }
  if (aType == MediaData::Type::VIDEO_DATA && !mHasEnoughVideo
#ifdef MOZ_WMF_CDM
      && !mVideoEOSSentToEngine
#endif
  ) {
    OnRequestVideo();
  }
}

void ExternalEngineStateMachine::OnRequestAudio() {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());

  if (!HasAudio()) {
    return;
  }

  if (IsRequestingAudioData() || mAudioWaitRequest.Exists() || IsSeeking()) {
    LOGV(
        "No need to request audio, isRequesting=%d, waitingAudio=%d, "
        "isSeeking=%d",
        IsRequestingAudioData(), mAudioWaitRequest.Exists(), IsSeeking());
    return;
  }

  PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestData);
  RefPtr<ExternalEngineStateMachine> self = this;
  mReader->RequestAudioData()
      ->Then(
          OwnerThread(), __func__,
          [this, self, perfRecorder(std::move(perfRecorder))](
              const RefPtr<AudioData>& aAudio) mutable {
            perfRecorder.Record();
            mAudioDataRequest.Complete();
            AUTO_PROFILER_LABEL(
                "ExternalEngineStateMachine::OnRequestAudio:Resolved",
                MEDIA_PLAYBACK);
            MOZ_ASSERT(aAudio);
            RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
          },
          [this, self](const MediaResult& aError) {
            mAudioDataRequest.Complete();
            AUTO_PROFILER_LABEL(
                "ExternalEngineStateMachine::OnRequestAudio:Rejected",
                MEDIA_PLAYBACK);
            LOG("OnRequestAudio ErrorName=%s Message=%s",
                aError.ErrorName().get(), aError.Message().get());
            switch (aError.Code()) {
              case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
                WaitForData(MediaData::Type::AUDIO_DATA);
                break;
              case NS_ERROR_DOM_MEDIA_CANCELED:
                OnRequestAudio();
                break;
              case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
                LOG("Reach to the end, no more audio data");
                EndOfStream(MediaData::Type::AUDIO_DATA);
                break;
              case NS_ERROR_DOM_MEDIA_REMOTE_CRASHED_MF_CDM_ERR:
                // We will handle the process crash in `NotifyErrorInternal()`
                // so here just silently ignore this.
                break;
              default:
                ReportTelemetry(aError);
                DecodeError(aError);
            }
          })
      ->Track(mAudioDataRequest);
}

void ExternalEngineStateMachine::OnRequestVideo() {
  AssertOnTaskQueue();
  MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());

  if (!HasVideo()) {
    return;
  }

  if (IsTrackingVideoData() || IsSeeking()) {
    LOGV(
        "No need to request video, isRequesting=%d, waitingVideo=%d, "
        "isSeeking=%d",
        IsRequestingVideoData(), mVideoWaitRequest.Exists(), IsSeeking());
    return;
  }

  PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestData,
                                                  Info().mVideo.mImage.height);
  RefPtr<ExternalEngineStateMachine> self = this;
  mReader->RequestVideoData(GetVideoThreshold(), false)
      ->Then(
          OwnerThread(), __func__,
          [this, self, perfRecorder(std::move(perfRecorder))](
              const RefPtr<VideoData>& aVideo) mutable {
            perfRecorder.Record();
            mVideoDataRequest.Complete();
            AUTO_PROFILER_LABEL(
                "ExternalEngineStateMachine::OnRequestVideo:Resolved",
                MEDIA_PLAYBACK);
            MOZ_ASSERT(aVideo);
            if (!mHasReceivedFirstDecodedVideoFrame) {
              mHasReceivedFirstDecodedVideoFrame = true;
              OnLoadedFirstFrame();
            }
            RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
            // Send image to PIP window.
            if (mSecondaryVideoContainer.Ref()) {
              mSecondaryVideoContainer.Ref()->SetCurrentFrame(
                  mVideoDisplay, aVideo->mImage, TimeStamp::Now(),
                  media::TimeUnit::Invalid(), aVideo->mTime);
            } else {
              mVideoFrameContainer->SetCurrentFrame(
                  mVideoDisplay, aVideo->mImage, TimeStamp::Now(),
                  media::TimeUnit::Invalid(), aVideo->mTime);
            }
          },
          [this, self](const MediaResult& aError) {
            mVideoDataRequest.Complete();
            AUTO_PROFILER_LABEL(
                "ExternalEngineStateMachine::OnRequestVideo:Rejected",
                MEDIA_PLAYBACK);
            LOG("OnRequestVideo ErrorName=%s Message=%s",
                aError.ErrorName().get(), aError.Message().get());
            switch (aError.Code()) {
              case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
                WaitForData(MediaData::Type::VIDEO_DATA);
                break;
              case NS_ERROR_DOM_MEDIA_CANCELED:
                OnRequestVideo();
                break;
              case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
                LOG("Reach to the end, no more video data");
                EndOfStream(MediaData::Type::VIDEO_DATA);
                break;
              case NS_ERROR_DOM_MEDIA_REMOTE_CRASHED_MF_CDM_ERR:
                // We will handle the process crash in `NotifyErrorInternal()`
                // so here just silently ignore this.
                break;
              default:
                ReportTelemetry(aError);
                DecodeError(aError);
            }
          })
      ->Track(mVideoDataRequest);
}

void ExternalEngineStateMachine::OnLoadedFirstFrame() {
  AssertOnTaskQueue();
  // We will wait until receive the first video frame.
  if (mInfo->HasVideo() &&
      !mHasReceivedFirstDecodedVideoFrame
#ifdef MOZ_WMF_CDM
      // In frame server mode no decoded frame is ever delivered to us, so we
      // must not gate the first-frame-loaded event on receiving one.
      && !mIsFrameServerMode
#endif
  ) {
    LOG("Hasn't received first decoded video frame");
    return;
  }
  LOG("OnLoadedFirstFrame");
  MediaDecoderEventVisibility visibility =
      mSentFirstFrameLoadedEvent ? MediaDecoderEventVisibility::Suppressed
                                 : MediaDecoderEventVisibility::Observable;
  mSentFirstFrameLoadedEvent = true;
  mFirstFrameLoadedEvent.Notify(UniquePtr<MediaInfo>(new MediaInfo(Info())),
                                visibility);
  mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
}

void ExternalEngineStateMachine::OnLoadedData() {
  AssertOnTaskQueue();
  // In case the external engine doesn't send the first frame loaded event
  // correctly.
  LOG("OnLoadedData");
  if (!mSentFirstFrameLoadedEvent) {
    OnLoadedFirstFrame();
  }
  mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
}

void ExternalEngineStateMachine::OnWaiting() {
  AssertOnTaskQueue();
  LOG("OnWaiting");
  mOnNextFrameStatus.Notify(
      MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING);
}

void ExternalEngineStateMachine::OnPlaying() {
  AssertOnTaskQueue();
  LOG("OnPlaying");
  mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
}

void ExternalEngineStateMachine::OnSeeked() {
  AssertOnTaskQueue();
  if (!mState.IsSeekingData()) {
    LOG("Engine Seeking has been completed, ignore the event");
    return;
  }
  MOZ_ASSERT(mState.IsSeekingData());

  const auto currentTime = mEngine->GetCurrentPosition();
  auto* state = mState.AsSeekingData();
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("target=%" PRId64 ", currentTime=%" PRId64,
                        state->GetTargetTime().ToMicroseconds(),
                        currentTime.ToMicroseconds());
    LOG("OnEngineSeeked : %s", msg.get());
    PROFILER_MARKER_TEXT("EESM::OnEngineSeeked", MEDIA_PLAYBACK, {}, msg);
  }
  // It's possible to receive multiple seeked event if we seek the engine
  // before the previous seeking finishes, so we would wait until the last
  // seeking is finished.
  if (currentTime >= state->GetTargetTime()) {
    state->mWaitingEngineSeeked = false;
    CheckIfSeekCompleted();
  }
}

void ExternalEngineStateMachine::OnBufferingStarted() {
  AssertOnTaskQueue();
  mOnNextFrameStatus.Notify(
      MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING);
  if (HasAudio()) {
    WaitForData(MediaData::Type::AUDIO_DATA);
  }
  if (HasVideo()) {
    WaitForData(MediaData::Type::VIDEO_DATA);
  }
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("hasAudio=%d, hasVideo=%d", HasAudio(), HasVideo());
    LOG("OnBufferingStarted : %s", msg.get());
    PROFILER_MARKER_TEXT("EESM::OnBufferingStarted", MEDIA_PLAYBACK, {}, msg);
  }
}

void ExternalEngineStateMachine::OnBufferingEnded() {
  AssertOnTaskQueue();
  LOG("OnBufferingEnded");
  PROFILER_MARKER_UNTYPED("EESM::OnBufferingEnded", MEDIA_PLAYBACK);
  mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
}

void ExternalEngineStateMachine::OnEnded() {
  AssertOnTaskQueue();
  if (mSentPlaybackEndedEvent) {
    return;
  }
  LOG("Playback is ended");
  PROFILER_MARKER_UNTYPED("EESM::OnEnded", MEDIA_PLAYBACK);
  mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE);
  mOnPlaybackEvent.Notify(MediaPlaybackEvent::PlaybackEnded);
  mSentPlaybackEndedEvent = true;
}

void ExternalEngineStateMachine::OnTimeupdate() {
  AssertOnTaskQueue();
  if (IsSeeking()) {
    return;
  }
  mCurrentPosition = mEngine->GetCurrentPosition();
  if (mDuration.Ref().ref() < mCurrentPosition.Ref()) {
    mDuration = Some(mCurrentPosition.Ref());
  }
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("current time=%" PRId64 ", duration=%" PRId64,
                        mCurrentPosition.Ref().ToMicroseconds(),
                        mDuration.Ref()->ToMicroseconds());
    LOG("OnTimeupdate, %s", msg.get());
    PROFILER_MARKER_TEXT("EESM::OnTimeupdate", MEDIA_PLAYBACK, {}, msg);
  }
}

void ExternalEngineStateMachine::NotifyEventInternal(
    ExternalEngineEvent aEvent) {
  AssertOnTaskQueue();
  AUTO_PROFILER_LABEL("ExternalEngineStateMachine::NotifyEventInternal",
                      MEDIA_PLAYBACK);
  if (mState.IsShutdownEngine()) {
    return;
  }
  PROFILER_MARKER_TEXT("EESM::NotifyEventInternal", MEDIA_PLAYBACK, {},
                       nsPrintfCString("%s", ExternalEngineEventToStr(aEvent)));
  switch (aEvent) {
    case ExternalEngineEvent::LoadedMetaData:
      // We read metadata by ourselves, ignore this if there is any.
      MOZ_ASSERT(mInfo);
      break;
    case ExternalEngineEvent::LoadedFirstFrame:
      OnLoadedFirstFrame();
      break;
    case ExternalEngineEvent::LoadedData:
      OnLoadedData();
      break;
    case ExternalEngineEvent::Waiting:
      OnWaiting();
      break;
    case ExternalEngineEvent::Playing:
      OnPlaying();
      break;
    case ExternalEngineEvent::Seeked:
      OnSeeked();
      break;
    case ExternalEngineEvent::BufferingStarted:
      OnBufferingStarted();
      break;
    case ExternalEngineEvent::BufferingEnded:
      OnBufferingEnded();
      break;
    case ExternalEngineEvent::Timeupdate:
      OnTimeupdate();
      break;
    case ExternalEngineEvent::Ended:
      OnEnded();
      break;
    case ExternalEngineEvent::RequestForAudio:
      mHasEnoughAudio = false;
      if (ShouldRunEngineUpdateForRequest()) {
        RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
      }
      break;
    case ExternalEngineEvent::RequestForVideo:
      mHasEnoughVideo = false;
      if (ShouldRunEngineUpdateForRequest()) {
        RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
      }
      break;
    case ExternalEngineEvent::AudioEnough:
      mHasEnoughAudio = true;
      break;
    case ExternalEngineEvent::VideoEnough:
      mHasEnoughVideo = true;
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("Undefined event!");
      break;
  }
}

bool ExternalEngineStateMachine::ShouldRunEngineUpdateForRequest() {
  // Running engine update will request new data, which could be run on
  // `RunningEngine` or `SeekingData` state. However, in `SeekingData` we should
  // only request new data after finishing reader seek, otherwise the reader
  // would start requesting data from a wrong position.
  return mState.IsRunningEngine() ||
         (mState.AsSeekingData() &&
          !mState.AsSeekingData()->mWaitingReaderSeeked);
}

void ExternalEngineStateMachine::NotifyErrorInternal(
    const MediaResult& aError) {
  AssertOnTaskQueue();
  LOG("Engine error: %s", aError.Description().get());
  PROFILER_MARKER_TEXT("EESM::NotifyErrorInternal", MEDIA_PLAYBACK, {},
                       aError.Description());
  if (mState.IsRecoverEngine()) {
    // Suppress errors that arrive during engine recovery to avoid
    // interrupting the in-progress reinit sequence.
    LOG("Ignoring error during hardware reset recovery: %s",
        aError.Description().get());
    return;
  }
  if (aError == NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR) {
    // The external engine doesn't support the type, try to notify the decoder
    // to use our own state machine again.
    ReportTelemetry(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR);
    DecodeError(
        MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
  } else if (aError == NS_ERROR_DOM_MEDIA_REMOTE_CRASHED_MF_CDM_ERR) {
    ReportTelemetry(NS_ERROR_DOM_MEDIA_REMOTE_CRASHED_MF_CDM_ERR);
    RecoverFromCDMProcessCrashIfNeeded();
  } else if (mState.IsInitEngine() && mKeySystem.IsEmpty()) {
    // If any error occurs during media engine initialization, we should attempt
    // to use another state machine for playback. Unless the key system is
    // already set, it indicates that playback can only be initiated via the
    // media engine. In this case, we will propagate the error and refrain
    // from trying another state machine.
    LOG("Error happened on the engine initialization, the media engine "
        "playback might not be supported");
    ReportTelemetry(NS_ERROR_DOM_MEDIA_MEDIA_ENGINE_INITIALIZATION_ERR);
    DecodeError(
        MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
  } else {
    ReportTelemetry(aError);
    DecodeError(aError);
  }
}

void ExternalEngineStateMachine::NotifyResizingInternal(uint32_t aWidth,
                                                        uint32_t aHeight) {
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("video resize from [%d,%d] to [%d,%d]",
                        mVideoDisplay.width, mVideoDisplay.height, aWidth,
                        aHeight);
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::NotifyResizingInternal", MEDIA_PLAYBACK, {},
                         msg);
  }
  mVideoDisplay = gfx::IntSize{aWidth, aHeight};
}

#ifdef MOZ_WMF_CDM
void ExternalEngineStateMachine::NotifyFrameServerModeInternal() {
  AssertOnTaskQueue();
  LOG("NotifyFrameServerModeInternal: engine is in frame server mode");
  MOZ_ASSERT(IsWMFClearKeySystemAndSupported(NS_ConvertUTF8toUTF16(mKeySystem)),
             "Frame server mode is only for WMFClearKey");
  mIsFrameServerMode = true;
  mOnPlaybackEvent.Notify(MediaPlaybackEvent::FrameServerMode);
  mHasReceivedFirstDecodedVideoFrame = true;
  if (!mSentFirstFrameLoadedEvent) {
    OnLoadedFirstFrame();
  }
}
#endif

void ExternalEngineStateMachine::RecoverFromCDMProcessCrashIfNeeded() {
  AssertOnTaskQueue();
  if (mState.IsRecoverEngine()) {
    LOG("In the recover state already");
    return;
  }
  ProcessCrashMonitor::NotifyCrash();
  if (!ProcessCrashMonitor::ShouldRecoverProcess()) {
    LOG("CDM process has crashed too many times, abort recovery");
    DecodeError(
        MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
    return;
  }

  if (mState.IsInitEngine()) {
    LOG("Failed on the engine initialization, the media engine playback might "
        "not be supported");
    DecodeError(
        MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
    return;
  }

  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg(
        "CDM process crashed, recover the engine again (last time=%" PRId64 ")",
        mCurrentPosition.Ref().ToMicroseconds());
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::RecoverFromCDMProcessCrashIfNeeded",
                         MEDIA_PLAYBACK, {}, msg);
  }
  mIsEngineReady = false;
  ChangeStateTo(State::RecoverEngine);
  if (HasVideo()) {
    mVideoDataRequest.DisconnectIfExists();
    mVideoWaitRequest.DisconnectIfExists();
  }
  if (HasAudio()) {
    mAudioDataRequest.DisconnectIfExists();
    mAudioWaitRequest.DisconnectIfExists();
  }
  // Ask the reader to shutdown current decoders which are no longer available
  // due to the remote process crash.
  mReader->ReleaseResources();
  InitEngine();
}

void ExternalEngineStateMachine::RecoverFromHardwareReset() {
  AssertOnTaskQueue();
  if (mState.IsRecoverEngine()) {
    LOG("In the recover state already");
    return;
  }
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg(
        "Hardware context reset, recovering engine (pos=%" PRId64 ")",
        mCurrentPosition.Ref().ToMicroseconds());
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::RecoverFromHardwareReset", MEDIA_PLAYBACK, {},
                         msg);
  }
  mIsEngineReady = false;
  ChangeStateTo(State::RecoverEngine);
  if (HasVideo()) {
    mVideoDataRequest.DisconnectIfExists();
    mVideoWaitRequest.DisconnectIfExists();
  }
  if (HasAudio()) {
    mAudioDataRequest.DisconnectIfExists();
    mAudioWaitRequest.DisconnectIfExists();
  }
  MOZ_ASSERT(mEngine);
  mEngine->Shutdown();
  mReader->ReleaseResources();
  InitEngine();
}

#ifdef MOZ_WMF_CDM
void ExternalEngineStateMachine::NotifyWaitingForKeyInternal() {
  AssertOnTaskQueue();
  mReader->NotifyWaitingForKey();
}
#endif

media::TimeUnit ExternalEngineStateMachine::GetVideoThreshold() {
  AssertOnTaskQueue();
  if (auto* state = mState.AsSeekingData()) {
    return state->GetTargetTime();
  }
  return mCurrentPosition.Ref();
}

void ExternalEngineStateMachine::UpdateSecondaryVideoContainer() {
  AssertOnTaskQueue();
  LOG("UpdateSecondaryVideoContainer=%p", mSecondaryVideoContainer.Ref().get());
  mOnSecondaryVideoContainerInstalled.Notify(mSecondaryVideoContainer.Ref());
}

RefPtr<SetCDMPromise> ExternalEngineStateMachine::SetCDMProxy(
    CDMProxy* aProxy) {
  if (!OnTaskQueue()) {
    return InvokeAsync(OwnerThread(), __func__,
                       [self = RefPtr{this}, proxy = RefPtr{aProxy}, this]() {
                         return SetCDMProxy(proxy);
                       });
  }
  AssertOnTaskQueue();
  if (mState.IsShutdownEngine()) {
    return SetCDMPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
  }

  if (!aProxy) {
    // A null proxy means the CDM is being cleared. The EESM-specific path
    // below requires a valid WMFCDMProxy to obtain its ID and register with
    // the engine, so it cannot handle null. Delegate to the base class, which
    // clears the CDM state on the reader via mReader->SetCDMProxy(null).
    // For WMFClearKey, failing to clear the CDM state would leave the reader
    // holding a stale reference to the old proxy, causing use-after-free or
    // silent decryption failures on subsequent playback.
    return MediaDecoderStateMachineBase::SetCDMProxy(aProxy);
  }

  if (!mIsEngineReady) {
    LOG("SetCDMProxy is called before init");
    mReader->SetEncryptedCustomIdent();
    mPendingTasks.AppendElement(NS_NewRunnableFunction(
        "ExternalEngineStateMachine::SetCDMProxy",
        [self = RefPtr{this}, proxy = RefPtr{aProxy}, this] {
          SetCDMProxy(proxy)
              ->Then(OwnerThread(), __func__,
                     [self = RefPtr{this},
                      this](const SetCDMPromise::ResolveOrRejectValue& aVal) {
                       mSetCDMProxyRequest.Complete();
                       if (aVal.IsResolve()) {
                         mSetCDMProxyPromise.Resolve(true, __func__);
                       } else {
                         mSetCDMProxyPromise.Reject(NS_ERROR_DOM_MEDIA_CDM_ERR,
                                                    __func__);
                       }
                     })
              ->Track(mSetCDMProxyRequest);
        }));
    return mSetCDMProxyPromise.Ensure(__func__);
  }

  // TODO : set CDM proxy again if we recreate the media engine after crash.
  mKeySystem = NS_ConvertUTF16toUTF8(aProxy->KeySystem());
  if (IsBeingProfiledOrLogEnabled()) {
    nsPrintfCString msg("SetCDMProxy=%p (key-system=%s)", aProxy,
                        mKeySystem.get());
    LOG("%s", msg.get());
    PROFILER_MARKER_TEXT("EESM::SetCDMProxy", MEDIA_PLAYBACK, {}, msg);
  }
  MOZ_DIAGNOSTIC_ASSERT(mEngine);
  // TODO : we should check the result of setting CDM proxy in the MFCDM process
  if (!mEngine->SetCDMProxy(aProxy)) {
    LOG("Failed to set CDM proxy on the engine");
    return SetCDMPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CDM_ERR, __func__);
  }
  return MediaDecoderStateMachineBase::SetCDMProxy(aProxy);
}

nsresult ExternalEngineStateMachine::IsCDMProxySupported(CDMProxy* aProxy) {
#ifdef MOZ_WMF_CDM
  MOZ_ASSERT(aProxy);

  // The CDM needs to be hosted in the same process of the external engine, and
  // only WMFCDM meets this requirement.
  if (!aProxy->AsWMFCDMProxy()) {
    return NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR;
  }

  // 1=enabled encrypted and clear, 2=enabled encrypted
  if (StaticPrefs::media_wmf_media_engine_enabled() != 1 &&
      StaticPrefs::media_wmf_media_engine_enabled() != 2) {
    return NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR;
  }

  return NS_OK;
#else
  return NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR;
#endif
}

void ExternalEngineStateMachine::ReportTelemetry(const MediaResult& aError) {
  glean::mfcdm::ErrorExtra extraData;
  extraData.errorName = Some(aError.ErrorName());
  extraData.currentState = Some(nsAutoCString{StateToStr(mState.mName)});
  nsAutoCString resolution;
  if (mInfo) {
    if (mInfo->HasAudio()) {
      extraData.audioCodec = Some(mInfo->mAudio.mMimeType);
    }
    if (mInfo->HasVideo()) {
      extraData.videoCodec = Some(mInfo->mVideo.mMimeType);
      DetermineResolutionForTelemetry(*mInfo, resolution);
      extraData.resolution = Some(resolution);
    }
  }
  if (!mKeySystem.IsEmpty()) {
    extraData.keySystem = Some(mKeySystem);
  }
  if (auto platformErrorCode = aError.GetPlatformErrorCode()) {
    extraData.platformError = platformErrorCode;
  }
  glean::mfcdm::error.Record(Some(extraData));
  if (MOZ_LOG_TEST(gMediaDecoderLog, LogLevel::Debug)) {
    nsPrintfCString logMessage{"MFCDM Error event, error=%s",
                               aError.ErrorName().get()};
    if (auto platformErrorCode = aError.GetPlatformErrorCode()) {
      logMessage.Append(nsPrintfCString{", hr=%x", *platformErrorCode});
    }
    if (mInfo) {
      if (mInfo->HasAudio()) {
        logMessage.Append(
            nsPrintfCString{", audio=%s", mInfo->mAudio.mMimeType.get()});
      }
      if (mInfo->HasVideo()) {
        logMessage.Append(nsPrintfCString{", video=%s, resolution=%s",
                                          mInfo->mVideo.mMimeType.get(),
                                          resolution.get()});
      }
    }
    if (!mKeySystem.IsEmpty()) {
      logMessage.Append(nsPrintfCString{", keySystem=%s", mKeySystem.get()});
    }
    LOG("%s", logMessage.get());
  }
}

void ExternalEngineStateMachine::DecodeError(const MediaResult& aError) {
  if (aError != NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA ||
      aError != NS_ERROR_DOM_MEDIA_CANCELED) {
    mHasFatalError = true;
  }
  MediaDecoderStateMachineBase ::DecodeError(aError);
}

void ExternalEngineStateMachine::NotifyAudibleStateChangeIfNeeded() {
  // Only perform a simple check because we can't access audio data from the
  // external engine.
  mIsAudioDataAudible = mInfo && HasAudio() &&
                        mPlayState == MediaDecoder::PLAY_STATE_PLAYING &&
                        mState.IsRunningEngine();
}

#undef FMT
#undef LOG
#undef LOGV
#undef LOGW
#undef LOGE

}  // namespace mozilla

Messung V0.5 in Prozent
C=95 H=99 G=96

¤ Dauer der Verarbeitung: 0.24 Sekunden  (vorverarbeitet am  2026-08-23) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.