Source code
Revision control
Copy as Markdown
Other Tools
/* 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
#include "ExternalEngineStateMachine.h"
#include "PerformanceRecorder.h"
#ifdef MOZ_WMF_MEDIA_ENGINE
# include "MFMediaEngineDecoderModule.h"
# include "mozilla/MFMediaEngineChild.h"
# include "mozilla/StaticPrefs_media.h"
#endif
#include "mozilla/AppShutdown.h"
#include "mozilla/Atomics.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/StaticMutex.h"
#include "mozilla/glean/GleanMetrics.h"
#include "nsThreadUtils.h"
#include "VideoUtils.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;
}
private:
ProcessCrashMonitor() : mCrashNums(0) {
#ifdef MOZ_WMF_MEDIA_ENGINE
mMaxCrashes = StaticPrefs::media_wmf_media_engine_max_crashes();
#else
mMaxCrashes = 0;
#endif
};
ProcessCrashMonitor(const ProcessCrashMonitor&) = delete;
ProcessCrashMonitor& operator=(const ProcessCrashMonitor&) = delete;
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 inline StaticMutex sMutex;
static inline MOZ_RUNINIT UniquePtr<ProcessCrashMonitor> sCrashMonitor;
static inline Atomic<bool> sIsShutdown{false};
uint32_t mCrashNums;
uint32_t mMaxCrashes;
};
/* 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);
}
void ExternalEngineStateMachine::ChangeStateTo(State aNextState) {
LOG("Change state : '%s' -> '%s' (play-state=%d)", StateToStr(mState.mName),
StateToStr(aNextState), mPlayState.Ref());
// 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);
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());
LOG("Initialized the external playback engine %" PRIu64, mEngine->Id());
auto* state = mState.AsInitEngine();
state->mEngineInitRequest.Complete();
mReader->UpdateMediaEngineId(mEngine->Id());
state->mInitPromise = nullptr;
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");
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());
Unused << 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;
}
LOG("Metadata loaded : a=%s, v=%s, size=[%dx%d], duration=%s",
mInfo->HasAudio() ? mInfo->mAudio.mMimeType.get() : "none",
mInfo->HasVideo() ? mInfo->mVideo.mMimeType.get() : "none",
mVideoDisplay.width, mVideoDisplay.height,
mDuration.Ref()->ToString().get());
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");
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 (!mEngine || !mEngine->IsInited()) {
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__);
}
LOG("Start seeking to %" PRId64, aTarget.GetTime().ToMicroseconds());
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;
LOG("Seek reader to %" PRId64, state->GetTargetTime().ToMicroseconds());
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");
state->mSeekRequest.Complete();
state->mWaitingReaderSeeked = false;
// Start sending new data to the external playback engine.
if (HasAudio()) {
mHasEnoughAudio = false;
OnRequestAudio();
}
if (HasVideo()) {
mHasEnoughVideo = false;
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");
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");
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");
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; \
} \
/* Initialzation is not done yet, postpone the operation */ \
if (!mEngine || !mEngine->IsInited()) { \
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;
};
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) {
Unused << 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) {
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_DECODER_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 (IsRequestingVideoData() || mVideoWaitRequest.Exists() || 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_DECODER_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) {
LOGV("Hasn't received first decoded video frame");
return;
}
LOGV("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.
if (!mSentFirstFrameLoadedEvent) {
OnLoadedFirstFrame();
}
mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
}
void ExternalEngineStateMachine::OnWaiting() {
AssertOnTaskQueue();
mOnNextFrameStatus.Notify(
MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING);
}
void ExternalEngineStateMachine::OnPlaying() {
AssertOnTaskQueue();
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();
LOG("OnEngineSeeked, target=%" PRId64 ", currentTime=%" PRId64,
state->GetTargetTime().ToMicroseconds(), currentTime.ToMicroseconds());
// 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);
}
}
void ExternalEngineStateMachine::OnBufferingEnded() {
AssertOnTaskQueue();
mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
}
void ExternalEngineStateMachine::OnEnded() {
AssertOnTaskQueue();
if (mSentPlaybackEndedEvent) {
return;
}
LOG("Playback is ended");
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());
}
}
void ExternalEngineStateMachine::NotifyEventInternal(
ExternalEngineEvent aEvent) {
AssertOnTaskQueue();
AUTO_PROFILER_LABEL("ExternalEngineStateMachine::NotifyEventInternal",
MEDIA_PLAYBACK);
LOG("Receive event %s", ExternalEngineEventToStr(aEvent));
if (mState.IsShutdownEngine()) {
return;
}
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());
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_DECODER_CRASHED_MF_CDM_ERR) {
ReportTelemetry(NS_ERROR_DOM_MEDIA_REMOTE_DECODER_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) {
LOG("video resize from [%d,%d] to [%d,%d]", mVideoDisplay.width,
mVideoDisplay.height, aWidth, aHeight);
mVideoDisplay = gfx::IntSize{aWidth, aHeight};
}
void ExternalEngineStateMachine::RecoverFromCDMProcessCrashIfNeeded() {
AssertOnTaskQueue();
if (mState.IsRecoverEngine()) {
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;
}
LOG("CDM process crashed, recover the engine again (last time=%" PRId64 ")",
mCurrentPosition.Ref().ToMicroseconds());
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();
}
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 (mState.IsShutdownEngine()) {
return SetCDMPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
}
if (!mEngine || !mEngine->IsInited()) {
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());
LOG("SetCDMProxy=%p (key-system=%s)", aProxy, mKeySystem.get());
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);
}
bool ExternalEngineStateMachine::IsCDMProxySupported(CDMProxy* aProxy) {
#ifdef MOZ_WMF_CDM
MOZ_ASSERT(aProxy);
// 1=enabled encrypted and clear, 2=enabled encrytped
if (StaticPrefs::media_wmf_media_engine_enabled() != 1 &&
StaticPrefs::media_wmf_media_engine_enabled() != 2) {
return false;
}
// The CDM needs to be hosted in the same process of the external engine, and
// only WMFCDM meets this requirement.
return aProxy->AsWMFCDMProxy();
#else
return false;
#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);
}
glean::mfcdm::error.Record(Some(extraData));
if (MOZ_LOG_TEST(gMediaDecoderLog, LogLevel::Debug)) {
nsPrintfCString logMessage{"MFCDM Error event, error=%s",
aError.ErrorName().get()};
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