/* 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 "MediaElementEventRunners.h" #include #include "MediaProfilerMarkers.h" #include "mozilla/Casting.h" #include "mozilla/ProfilerState.h" #include "mozilla/dom/HTMLMediaElement.h" #include "mozilla/dom/HTMLVideoElement.h" #include "mozilla/dom/MediaError.h" #include "mozilla/dom/TimeRanges.h" #include "mozilla/FlowMarkers.h" extern mozilla::LazyLogModule gMediaElementEventsLog; #define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg) namespace mozilla::dom { nsMediaEventRunner::nsMediaEventRunner(const char* aName, HTMLMediaElement* aElement, const nsAString& aEventName) : mElement(aElement), mName(aName), mEventName(aEventName), mLoadID(mElement->GetCurrentLoadID()) {} bool nsMediaEventRunner::IsCancelled() const { return !mElement || mElement->GetCurrentLoadID() != mLoadID; } nsresult nsMediaEventRunner::FireEvent(const nsAString& aName) { nsresult rv = NS_OK; if (mElement) { ReportProfilerMarker(); rv = RefPtr { mElement } -> FireEvent(aName); } return rv; } void nsMediaEventRunner::ReportProfilerMarker() { if (!profiler_is_collecting_markers()) { return; } // Report the buffered range. if (mEventName.EqualsLiteral("progress")) { RefPtr buffered = mElement->Buffered(); if (buffered && buffered->Length() > 0) { for (size_t i = 0; i < buffered->Length(); ++i) { profiler_add_marker("progress", geckoprofiler::category::MEDIA_PLAYBACK, {}, BufferedUpdateMarker{}, AssertedCast(buffered->Start(i) * 1000), AssertedCast(buffered->End(i) * 1000), GetElementDurationMs(), Flow::FromPointer(mElement.get())); } } } else if (mEventName.EqualsLiteral("resize")) { MOZ_ASSERT(mElement->HasVideo()); auto mediaInfo = mElement->GetMediaInfo(); profiler_add_marker("resize", geckoprofiler::category::MEDIA_PLAYBACK, {}, VideoResizeMarker{}, mediaInfo.mVideo.mDisplay.width, mediaInfo.mVideo.mDisplay.height, Flow::FromPointer(mElement.get())); } else if (mEventName.EqualsLiteral("loadedmetadata")) { nsString src; mElement->GetCurrentSrc(src); auto mediaInfo = mElement->GetMediaInfo(); profiler_add_marker("loadedmetadata", geckoprofiler::category::MEDIA_PLAYBACK, {}, MetadataMarker{}, src, mediaInfo.HasAudio() ? mediaInfo.mAudio.mMimeType : "none"_ns, mediaInfo.HasVideo() ? mediaInfo.mVideo.mMimeType : "none"_ns, Flow::FromPointer(mElement.get())); } else if (mEventName.EqualsLiteral("error")) { auto* error = mElement->GetError(); nsString message; error->GetMessage(message); profiler_add_marker("error", geckoprofiler::category::MEDIA_PLAYBACK, {}, ErrorMarker{}, message, Flow::FromPointer(mElement.get())); } else { auto eventName = NS_ConvertUTF16toUTF8(mEventName); PROFILER_MARKER(eventName, MEDIA_PLAYBACK, {}, FlowMarker, Flow::FromPointer(mElement.get())); } } uint64_t nsMediaEventRunner::GetElementDurationMs() const { MOZ_ASSERT(!IsCancelled()); double duration = mElement->Duration(); if (duration == std::numeric_limits::infinity()) { return std::numeric_limits::max(); } if (std::isnan(duration) || duration <= 0) { // Duration is unknown or invalid return 0; } return AssertedCast(duration * 1000); } NS_IMPL_CYCLE_COLLECTION(nsMediaEventRunner, mElement) NS_IMPL_CYCLE_COLLECTING_ADDREF(nsMediaEventRunner) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsMediaEventRunner) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsMediaEventRunner) NS_INTERFACE_MAP_ENTRY(nsINamed) NS_INTERFACE_MAP_ENTRY(nsIRunnable) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRunnable) NS_INTERFACE_MAP_END NS_IMETHODIMP nsAsyncEventRunner::Run() { // Silently cancel if our load has been cancelled or element has been CCed. return IsCancelled() ? NS_OK : FireEvent(mEventName); } nsResolveOrRejectPendingPlayPromisesRunner:: nsResolveOrRejectPendingPlayPromisesRunner( HTMLMediaElement* aElement, nsTArray>&& aPromises, nsresult aError) : nsMediaEventRunner("nsResolveOrRejectPendingPlayPromisesRunner", aElement), mPromises(std::move(aPromises)), mError(aError) { mElement->mPendingPlayPromisesRunners.AppendElement(this); } void nsResolveOrRejectPendingPlayPromisesRunner::ResolveOrReject() { if (NS_SUCCEEDED(mError)) { PlayPromise::ResolvePromisesWithUndefined(mPromises); } else { PlayPromise::RejectPromises(mPromises, mError); } } NS_IMETHODIMP nsResolveOrRejectPendingPlayPromisesRunner::Run() { if (!IsCancelled()) { ResolveOrReject(); } mElement->mPendingPlayPromisesRunners.RemoveElement(this); return NS_OK; } NS_IMETHODIMP nsNotifyAboutPlayingRunner::Run() { if (!IsCancelled()) { FireEvent(u"playing"_ns); } return nsResolveOrRejectPendingPlayPromisesRunner::Run(); } NS_IMPL_CYCLE_COLLECTION_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner, nsMediaEventRunner, mPromises) NS_IMPL_ADDREF_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner, nsMediaEventRunner) NS_IMPL_RELEASE_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner, nsMediaEventRunner) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( nsResolveOrRejectPendingPlayPromisesRunner) NS_INTERFACE_MAP_END_INHERITING(nsMediaEventRunner) NS_IMETHODIMP nsSourceErrorEventRunner::Run() { // Silently cancel if our load has been cancelled. if (IsCancelled()) { return NS_OK; } LOG_EVENT(LogLevel::Debug, ("%p Dispatching simple event source error", mElement.get())); if (profiler_is_collecting_markers()) { profiler_add_marker("sourceerror", geckoprofiler::category::MEDIA_PLAYBACK, {}, ErrorMarker{}, mErrorDetails, Flow::FromPointer(mElement.get())); } return nsContentUtils::DispatchTrustedEvent(mElement->OwnerDoc(), mSource, u"error"_ns, CanBubble::eNo, Cancelable::eNo); } NS_IMPL_CYCLE_COLLECTION_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner, mSource) NS_IMPL_ADDREF_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner) NS_IMPL_RELEASE_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsSourceErrorEventRunner) NS_INTERFACE_MAP_END_INHERITING(nsMediaEventRunner) NS_IMETHODIMP nsTimeupdateRunner::Run() { if (IsCancelled() || !ShouldDispatchTimeupdate()) { return NS_OK; } // After dispatching `timeupdate`, if the timeupdate event listener takes lots // of time then we end up spending all time handling just timeupdate events. // The spec is vague in this situation, so we choose to update time after we // dispatch the event in order to solve that issue. nsresult rv = FireEvent(mEventName); if (NS_WARN_IF(NS_FAILED(rv))) { LOG_EVENT(LogLevel::Debug, ("%p Failed to dispatch 'timeupdate'", mElement.get())); } else { mElement->UpdateLastTimeupdateDispatchTime(); } return rv; } bool nsTimeupdateRunner::ShouldDispatchTimeupdate() const { if (mIsMandatory) { return true; } // If the main thread is busy, tasks may be delayed and dispatched at // unexpected times. Ensure we don't dispatch `timeupdate` more often // than once per `TIMEUPDATE_MS`. const TimeStamp& lastTime = mElement->LastTimeupdateDispatchTime(); return lastTime.IsNull() || TimeStamp::Now() - lastTime > TimeDuration::FromMilliseconds(TIMEUPDATE_MS); } void nsTimeupdateRunner::ReportProfilerMarker() { if (!profiler_is_collecting_markers()) { return; } auto* videoElement = mElement->AsHTMLVideoElement(); profiler_add_marker("timeupdate", geckoprofiler::category::MEDIA_PLAYBACK, {}, TimeUpdateMarker{}, AssertedCast(mElement->CurrentTime() * 1000), GetElementDurationMs(), videoElement ? videoElement->MozPaintedFrames() : 0, Flow::FromPointer(mElement.get())); } #undef LOG_EVENT } // namespace mozilla::dom