forked from mirrors/gecko-dev
We will postpone any seek happening before the engine initialization is finished, and will resolve the seek promise based on the latest seek if there are multiple seek being requested. Differential Revision: https://phabricator.services.mozilla.com/D205017
1376 lines
48 KiB
C++
1376 lines
48 KiB
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
|
|
#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 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!");
|
|
}
|
|
}
|
|
|
|
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();
|
|
state->mInitPromise = mEngine->Init(*mInfo, !mMinimizePreroll);
|
|
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;
|
|
// TODO : Should fallback to the normal playback with media engine.
|
|
ReportTelemetry(NS_ERROR_DOM_MEDIA_FATAL_ERR);
|
|
DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_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() &&
|
|
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::TypeToStr(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();
|
|
// TODO : implement audible check
|
|
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();
|
|
}
|
|
}
|
|
|
|
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());
|
|
LOGV("OnRequestAudio");
|
|
|
|
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;
|
|
}
|
|
|
|
LOGV("Start requesting audio");
|
|
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();
|
|
LOGV("Completed requesting audio");
|
|
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());
|
|
LOGV("OnRequestVideo");
|
|
|
|
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;
|
|
}
|
|
|
|
LOGV("Start requesting video");
|
|
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();
|
|
LOGV("Completed requesting video");
|
|
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());
|
|
} else {
|
|
mVideoFrameContainer->SetCurrentFrame(
|
|
mVideoDisplay, aVideo->mImage, TimeStamp::Now());
|
|
}
|
|
},
|
|
[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 {
|
|
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");
|
|
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);
|
|
}
|
|
|
|
#undef FMT
|
|
#undef LOG
|
|
#undef LOGV
|
|
#undef LOGW
|
|
#undef LOGE
|
|
|
|
} // namespace mozilla
|