diff options
Diffstat (limited to 'dom/html/HTMLMediaElement.cpp')
-rw-r--r-- | dom/html/HTMLMediaElement.cpp | 7881 |
1 files changed, 7881 insertions, 0 deletions
diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp new file mode 100644 index 0000000000..78e9a7b861 --- /dev/null +++ b/dom/html/HTMLMediaElement.cpp @@ -0,0 +1,7881 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifdef XP_WIN +# include "objbase.h" +#endif + +#include "mozilla/dom/HTMLMediaElement.h" + +#include <unordered_map> + +#include "AudioDeviceInfo.h" +#include "AudioStreamTrack.h" +#include "AutoplayPolicy.h" +#include "ChannelMediaDecoder.h" +#include "CrossGraphPort.h" +#include "DOMMediaStream.h" +#include "DecoderDoctorDiagnostics.h" +#include "DecoderDoctorLogger.h" +#include "DecoderTraits.h" +#include "FrameStatistics.h" +#include "GMPCrashHelper.h" +#include "GVAutoplayPermissionRequest.h" +#ifdef MOZ_ANDROID_HLS_SUPPORT +# include "HLSDecoder.h" +#endif +#include "HTMLMediaElement.h" +#include "ImageContainer.h" +#include "MP4Decoder.h" +#include "MediaContainerType.h" +#include "MediaError.h" +#include "MediaManager.h" +#include "MediaMetadataManager.h" +#include "MediaResource.h" +#include "MediaShutdownManager.h" +#include "MediaSourceDecoder.h" +#include "MediaStreamError.h" +#include "MediaTrackGraphImpl.h" +#include "MediaTrackListener.h" +#include "MediaStreamWindowCapturer.h" +#include "MediaTrack.h" +#include "MediaTrackList.h" +#include "Navigator.h" +#include "TimeRanges.h" +#include "VideoFrameContainer.h" +#include "VideoOutput.h" +#include "VideoStreamTrack.h" +#include "base/basictypes.h" +#include "jsapi.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "mozilla/ArrayUtils.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/NotNull.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/AudioTrack.h" +#include "mozilla/dom/AudioTrackList.h" +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/ContentMediaController.h" +#include "mozilla/dom/ElementInlines.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/HTMLAudioElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLMediaElementBinding.h" +#include "mozilla/dom/HTMLSourceElement.h" +#include "mozilla/dom/HTMLVideoElement.h" +#include "mozilla/dom/MediaControlUtils.h" +#include "mozilla/dom/MediaDevices.h" +#include "mozilla/dom/MediaEncryptedEvent.h" +#include "mozilla/dom/MediaErrorBinding.h" +#include "mozilla/dom/MediaSource.h" +#include "mozilla/dom/PlayPromise.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/TextTrack.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/VideoPlaybackQuality.h" +#include "mozilla/dom/VideoTrack.h" +#include "mozilla/dom/VideoTrackList.h" +#include "mozilla/dom/WakeLock.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/power/PowerManagerService.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "nsAttrValueInlines.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDisplayList.h" +#include "nsDocShell.h" +#include "nsError.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsICachingChannel.h" +#include "nsIClassOfService.h" +#include "nsIContentPolicy.h" +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" +#include "nsIHttpChannel.h" +#include "nsIObserverService.h" +#include "nsIRequest.h" +#include "nsIScriptError.h" +#include "nsISupportsPrimitives.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsITimer.h" +#include "nsJSUtils.h" +#include "nsLayoutUtils.h" +#include "nsMediaFragmentURIParser.h" +#include "nsMimeTypes.h" +#include "nsNetUtil.h" +#include "nsNodeInfoManager.h" +#include "nsPresContext.h" +#include "nsQueryObject.h" +#include "nsRange.h" +#include "nsSize.h" +#include "nsThreadUtils.h" +#include "nsURIHashKey.h" +#include "nsURLHelper.h" +#include "nsVideoFrame.h" +#include "ReferrerInfo.h" +#include "TimeUnits.h" +#include "xpcpublic.h" +#include <algorithm> +#include <cmath> +#include <limits> +#include <type_traits> + +mozilla::LazyLogModule gMediaElementLog("HTMLMediaElement"); +mozilla::LazyLogModule gMediaElementEventsLog("HTMLMediaElementEvents"); + +extern mozilla::LazyLogModule gAutoplayPermissionLog; +#define AUTOPLAY_LOG(msg, ...) \ + MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +// avoid redefined macro in unified build +#undef MEDIACONTROL_LOG +#define MEDIACONTROL_LOG(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("HTMLMediaElement=%p, " msg, this, ##__VA_ARGS__)) + +#undef CONTROLLER_TIMER_LOG +#define CONTROLLER_TIMER_LOG(element, msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("HTMLMediaElement=%p, " msg, element, ##__VA_ARGS__)) + +#define LOG(type, msg) MOZ_LOG(gMediaElementLog, type, msg) +#define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg) + +using namespace mozilla::layers; +using mozilla::net::nsMediaFragmentURIParser; +using namespace mozilla::dom::HTMLMediaElement_Binding; + +namespace mozilla::dom { + +using AudibleState = AudioChannelService::AudibleState; +using SinkInfoPromise = MediaDevices::SinkInfoPromise; + +// Number of milliseconds between progress events as defined by spec +static const uint32_t PROGRESS_MS = 350; + +// Number of milliseconds of no data before a stall event is fired as defined by +// spec +static const uint32_t STALL_MS = 3000; + +// Used by AudioChannel for suppresssing the volume to this ratio. +#define FADED_VOLUME_RATIO 0.25 + +// These constants are arbitrary +// Minimum playbackRate for a media +static const double MIN_PLAYBACKRATE = 1.0 / 16; +// Maximum playbackRate for a media +static const double MAX_PLAYBACKRATE = 16.0; + +static double ClampPlaybackRate(double aPlaybackRate) { + MOZ_ASSERT(aPlaybackRate >= 0.0); + + if (aPlaybackRate == 0.0) { + return aPlaybackRate; + } + if (aPlaybackRate < MIN_PLAYBACKRATE) { + return MIN_PLAYBACKRATE; + } + if (aPlaybackRate > MAX_PLAYBACKRATE) { + return MAX_PLAYBACKRATE; + } + return aPlaybackRate; +} + +// Media error values. These need to match the ones in MediaError.webidl. +static const unsigned short MEDIA_ERR_ABORTED = 1; +static const unsigned short MEDIA_ERR_NETWORK = 2; +static const unsigned short MEDIA_ERR_DECODE = 3; +static const unsigned short MEDIA_ERR_SRC_NOT_SUPPORTED = 4; + +/** + * EventBlocker helps media element to postpone the event delivery by storing + * the event runner, and execute them once media element decides not to postpone + * the event delivery. If media element never resumes the event delivery, then + * those runner would be cancelled. + * For example, we postpone the event delivery when media element entering to + * the bf-cache. + */ +class HTMLMediaElement::EventBlocker final : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL + NS_DECL_CYCLE_COLLECTION_CLASS(EventBlocker) + + explicit EventBlocker(HTMLMediaElement* aElement) : mElement(aElement) {} + + void SetBlockEventDelivery(bool aShouldBlock) { + MOZ_ASSERT(NS_IsMainThread()); + if (mShouldBlockEventDelivery == aShouldBlock) { + return; + } + LOG_EVENT(LogLevel::Debug, + ("%p %s event delivery", mElement.get(), + mShouldBlockEventDelivery ? "block" : "unblock")); + mShouldBlockEventDelivery = aShouldBlock; + if (!mShouldBlockEventDelivery) { + DispatchPendingMediaEvents(); + } + } + + void PostponeEvent(nsMediaEventRunner* aRunner) { + MOZ_ASSERT(NS_IsMainThread()); + // Element has been CCed, which would break the weak pointer. + if (!mElement) { + return; + } + MOZ_ASSERT(mShouldBlockEventDelivery); + MOZ_ASSERT(mElement); + LOG_EVENT(LogLevel::Debug, + ("%p postpone runner %s for %s", mElement.get(), + NS_ConvertUTF16toUTF8(aRunner->Name()).get(), + NS_ConvertUTF16toUTF8(aRunner->EventName()).get())); + mPendingEventRunners.AppendElement(aRunner); + } + + void Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& runner : mPendingEventRunners) { + runner->Cancel(); + } + mPendingEventRunners.Clear(); + } + + bool ShouldBlockEventDelivery() const { + MOZ_ASSERT(NS_IsMainThread()); + return mShouldBlockEventDelivery; + } + + size_t SizeOfExcludingThis(MallocSizeOf& aMallocSizeOf) const { + MOZ_ASSERT(NS_IsMainThread()); + size_t total = 0; + for (const auto& runner : mPendingEventRunners) { + total += aMallocSizeOf(runner); + } + return total; + } + + private: + ~EventBlocker() = default; + + void DispatchPendingMediaEvents() { + MOZ_ASSERT(mElement); + for (auto& runner : mPendingEventRunners) { + LOG_EVENT(LogLevel::Debug, + ("%p execute runner %s for %s", mElement.get(), + NS_ConvertUTF16toUTF8(runner->Name()).get(), + NS_ConvertUTF16toUTF8(runner->EventName()).get())); + GetMainThreadSerialEventTarget()->Dispatch(runner.forget()); + } + mPendingEventRunners.Clear(); + } + + WeakPtr<HTMLMediaElement> mElement; + bool mShouldBlockEventDelivery = false; + // Contains event runners which should not be run for now because we want + // to block all events delivery. They would be dispatched once media element + // decides unblocking them. + nsTArray<RefPtr<nsMediaEventRunner>> mPendingEventRunners; +}; + +NS_IMPL_CYCLE_COLLECTION(HTMLMediaElement::EventBlocker, mPendingEventRunners) +NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLMediaElement::EventBlocker) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLMediaElement::EventBlocker) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLMediaElement::EventBlocker) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/** + * We use MediaControlKeyListener to listen to media control key in order to + * play and pause media element when user press media control keys and update + * media's playback and audible state to the media controller. + * + * Use `Start()` to start listening event and use `Stop()` to stop listening + * event. In addition, notifying any change to media controller MUST be done + * after successfully calling `Start()`. + */ +class HTMLMediaElement::MediaControlKeyListener final + : public ContentMediaControlKeyReceiver { + public: + NS_INLINE_DECL_REFCOUNTING(MediaControlKeyListener, override) + + MOZ_INIT_OUTSIDE_CTOR explicit MediaControlKeyListener( + HTMLMediaElement* aElement) + : mElement(aElement) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aElement); + } + + /** + * Start listening to the media control keys which would make media being able + * to be controlled via pressing media control keys. + */ + void Start() { + MOZ_ASSERT(NS_IsMainThread()); + if (IsStarted()) { + // We have already been started, do not notify start twice. + return; + } + + // Fail to init media agent, we are not able to notify the media controller + // any update and also are not able to receive media control key events. + if (!InitMediaAgent()) { + MEDIACONTROL_LOG("Failed to start due to not able to init media agent!"); + return; + } + + NotifyPlaybackStateChanged(MediaPlaybackState::eStarted); + // If owner has started playing before the listener starts, we should update + // the playing state as well. Eg. media starts inaudily and becomes audible + // later. + if (!Owner()->Paused()) { + NotifyMediaStartedPlaying(); + } + if (StaticPrefs::media_mediacontrol_testingevents_enabled()) { + auto dispatcher = MakeRefPtr<AsyncEventDispatcher>( + Owner(), u"MozStartMediaControl"_ns, CanBubble::eYes, + ChromeOnlyDispatch::eYes); + dispatcher->PostDOMEvent(); + } + } + + /** + * Stop listening to the media control keys which would make media not be able + * to be controlled via pressing media control keys. If we haven't started + * listening to the media control keys, then nothing would happen. + */ + void StopIfNeeded() { + MOZ_ASSERT(NS_IsMainThread()); + if (!IsStarted()) { + // We have already been stopped, do not notify stop twice. + return; + } + NotifyMediaStoppedPlaying(); + NotifyPlaybackStateChanged(MediaPlaybackState::eStopped); + + // Remove ourselves from media agent, which would stop receiving event. + mControlAgent->RemoveReceiver(this); + mControlAgent = nullptr; + } + + bool IsStarted() const { return mState != MediaPlaybackState::eStopped; } + + bool IsPlaying() const override { + return Owner() ? !Owner()->Paused() : false; + } + + /** + * Following methods should only be used after starting listener. + */ + void NotifyMediaStartedPlaying() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + if (mState == MediaPlaybackState::eStarted || + mState == MediaPlaybackState::ePaused) { + NotifyPlaybackStateChanged(MediaPlaybackState::ePlayed); + // If media is `inaudible` in the beginning, then we don't need to notify + // the state, because notifying `inaudible` should always come after + // notifying `audible`. + if (mIsOwnerAudible) { + NotifyAudibleStateChanged(MediaAudibleState::eAudible); + } + } + } + + void NotifyMediaStoppedPlaying() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + if (mState == MediaPlaybackState::ePlayed) { + NotifyPlaybackStateChanged(MediaPlaybackState::ePaused); + // As media are going to be paused, so no sound is possible to be heard. + if (mIsOwnerAudible) { + NotifyAudibleStateChanged(MediaAudibleState::eInaudible); + } + } + } + + // This method can be called before the listener starts, which would cache + // the audible state and update after the listener starts. + void UpdateMediaAudibleState(bool aIsOwnerAudible) { + MOZ_ASSERT(NS_IsMainThread()); + if (mIsOwnerAudible == aIsOwnerAudible) { + return; + } + mIsOwnerAudible = aIsOwnerAudible; + MEDIACONTROL_LOG("Media becomes %s", + mIsOwnerAudible ? "audible" : "inaudible"); + // If media hasn't started playing, it doesn't make sense to update media + // audible state. Therefore, in that case we would noitfy the audible state + // when media starts playing. + if (mState == MediaPlaybackState::ePlayed) { + NotifyAudibleStateChanged(mIsOwnerAudible + ? MediaAudibleState::eAudible + : MediaAudibleState::eInaudible); + } + } + + void SetPictureInPictureModeEnabled(bool aIsEnabled) { + MOZ_ASSERT(NS_IsMainThread()); + if (mIsPictureInPictureEnabled == aIsEnabled) { + return; + } + // PIP state changes might happen before the listener starts or stops where + // we haven't call `InitMediaAgent()` yet. Eg. Reset the PIP video's src, + // then cancel the PIP. In addition, not like playback and audible state + // which should be restricted to update via the same agent in order to keep + // those states correct in each `ContextMediaInfo`, PIP state can be updated + // through any browsing context, so we would use `ContentMediaAgent::Get()` + // directly to update PIP state. + mIsPictureInPictureEnabled = aIsEnabled; + if (RefPtr<IMediaInfoUpdater> updater = + ContentMediaAgent::Get(GetCurrentBrowsingContext())) { + updater->SetIsInPictureInPictureMode(mOwnerBrowsingContextId, + mIsPictureInPictureEnabled); + } + } + + void HandleMediaKey(MediaControlKey aKey) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + MEDIACONTROL_LOG("HandleEvent '%s'", ToMediaControlKeyStr(aKey)); + if (aKey == MediaControlKey::Play) { + Owner()->Play(); + } else if (aKey == MediaControlKey::Pause) { + Owner()->Pause(); + } else { + MOZ_ASSERT(aKey == MediaControlKey::Stop, + "Not supported key for media element!"); + Owner()->Pause(); + StopIfNeeded(); + } + } + + void UpdateOwnerBrowsingContextIfNeeded() { + // Has not notified any information about the owner context yet. + if (!IsStarted()) { + return; + } + + BrowsingContext* currentBC = GetCurrentBrowsingContext(); + MOZ_ASSERT(currentBC); + // Still in the same browsing context, no need to update. + if (currentBC->Id() == mOwnerBrowsingContextId) { + return; + } + MEDIACONTROL_LOG("Change browsing context from %" PRIu64 " to %" PRIu64, + mOwnerBrowsingContextId, currentBC->Id()); + // This situation would happen when we start a media in an original browsing + // context, then we move it to another browsing context, such as an iframe, + // so its owner browsing context would be changed. Therefore, we should + // reset the media status for the previous browsing context by calling + // `Stop()`, in which the listener would notify `ePaused` (if it's playing) + // and `eStop`. Then calls `Start()`, in which the listener would notify + // `eStart` to the new browsing context. If the media was playing before, + // we would also notify `ePlayed`. + bool wasInPlayingState = mState == MediaPlaybackState::ePlayed; + StopIfNeeded(); + Start(); + if (wasInPlayingState) { + NotifyMediaStartedPlaying(); + } + } + + private: + ~MediaControlKeyListener() = default; + + // The media can be moved around different browsing contexts, so this context + // might be different from the one that we used to initialize + // `ContentMediaAgent`. + BrowsingContext* GetCurrentBrowsingContext() const { + // Owner has been CCed, which would break the link of the weaker pointer. + if (!Owner()) { + return nullptr; + } + nsPIDOMWindowInner* window = Owner()->OwnerDoc()->GetInnerWindow(); + return window ? window->GetBrowsingContext() : nullptr; + } + + bool InitMediaAgent() { + MOZ_ASSERT(NS_IsMainThread()); + BrowsingContext* currentBC = GetCurrentBrowsingContext(); + mControlAgent = ContentMediaAgent::Get(currentBC); + if (!mControlAgent) { + return false; + } + MOZ_ASSERT(currentBC); + mOwnerBrowsingContextId = currentBC->Id(); + MEDIACONTROL_LOG("Init agent in browsing context %" PRIu64, + mOwnerBrowsingContextId); + mControlAgent->AddReceiver(this); + return true; + } + + HTMLMediaElement* Owner() const { + // `mElement` would be clear during CC unlinked, but it would only happen + // after stopping the listener. + MOZ_ASSERT(mElement || !IsStarted()); + return mElement.get(); + } + + void NotifyPlaybackStateChanged(MediaPlaybackState aState) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mControlAgent); + MEDIACONTROL_LOG("NotifyMediaState from state='%s' to state='%s'", + ToMediaPlaybackStateStr(mState), + ToMediaPlaybackStateStr(aState)); + MOZ_ASSERT(mState != aState, "Should not notify same state again!"); + mState = aState; + mControlAgent->NotifyMediaPlaybackChanged(mOwnerBrowsingContextId, mState); + } + + void NotifyAudibleStateChanged(MediaAudibleState aState) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + mControlAgent->NotifyMediaAudibleChanged(mOwnerBrowsingContextId, aState); + } + + MediaPlaybackState mState = MediaPlaybackState::eStopped; + WeakPtr<HTMLMediaElement> mElement; + RefPtr<ContentMediaAgent> mControlAgent; + bool mIsPictureInPictureEnabled = false; + bool mIsOwnerAudible = false; + MOZ_INIT_OUTSIDE_CTOR uint64_t mOwnerBrowsingContextId; +}; + +class HTMLMediaElement::MediaStreamTrackListener + : public DOMMediaStream::TrackListener { + public: + explicit MediaStreamTrackListener(HTMLMediaElement* aElement) + : mElement(aElement) {} + + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override { + if (!mElement) { + return; + } + mElement->NotifyMediaStreamTrackAdded(aTrack); + } + + void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override { + if (!mElement) { + return; + } + mElement->NotifyMediaStreamTrackRemoved(aTrack); + } + + void OnActive() { + MOZ_ASSERT(mElement); + + // mediacapture-main says: + // Note that once ended equals true the HTMLVideoElement will not play media + // even if new MediaStreamTracks are added to the MediaStream (causing it to + // return to the active state) unless autoplay is true or the web + // application restarts the element, e.g., by calling play(). + // + // This is vague on exactly how to go from becoming active to playing, when + // autoplaying. However, per the media element spec, to play an autoplaying + // media element, we must load the source and reach readyState + // HAVE_ENOUGH_DATA [1]. Hence, a MediaStream being assigned to a media + // element and becoming active runs the load algorithm, so that it can + // eventually be played. + // + // [1] + // https://html.spec.whatwg.org/multipage/media.html#ready-states:event-media-play + + LOG(LogLevel::Debug, ("%p, mSrcStream %p became active, checking if we " + "need to run the load algorithm", + mElement.get(), mElement->mSrcStream.get())); + if (!mElement->IsPlaybackEnded()) { + return; + } + if (!mElement->Autoplay()) { + return; + } + LOG(LogLevel::Info, ("%p, mSrcStream %p became active on autoplaying, " + "ended element. Reloading.", + mElement.get(), mElement->mSrcStream.get())); + mElement->DoLoad(); + } + + void NotifyActive() override { + if (!mElement) { + return; + } + + if (!mElement->IsVideo()) { + // Audio elements use NotifyAudible(). + return; + } + + OnActive(); + } + + void NotifyAudible() override { + if (!mElement) { + return; + } + + if (mElement->IsVideo()) { + // Video elements use NotifyActive(). + return; + } + + OnActive(); + } + + void OnInactive() { + MOZ_ASSERT(mElement); + + if (mElement->IsPlaybackEnded()) { + return; + } + LOG(LogLevel::Debug, ("%p, mSrcStream %p became inactive", mElement.get(), + mElement->mSrcStream.get())); + + mElement->PlaybackEnded(); + } + + void NotifyInactive() override { + if (!mElement) { + return; + } + + if (!mElement->IsVideo()) { + // Audio elements use NotifyInaudible(). + return; + } + + OnInactive(); + } + + void NotifyInaudible() override { + if (!mElement) { + return; + } + + if (mElement->IsVideo()) { + // Video elements use NotifyInactive(). + return; + } + + OnInactive(); + } + + protected: + const WeakPtr<HTMLMediaElement> mElement; +}; + +/** + * Helper class that manages audio and video outputs for all enabled tracks in a + * media element. It also manages calculating the current time when playing a + * MediaStream. + */ +class HTMLMediaElement::MediaStreamRenderer + : public DOMMediaStream::TrackListener { + public: + NS_INLINE_DECL_REFCOUNTING(MediaStreamRenderer) + + MediaStreamRenderer(AbstractThread* aMainThread, + VideoFrameContainer* aVideoContainer, + FirstFrameVideoOutput* aFirstFrameVideoOutput, + void* aAudioOutputKey) + : mVideoContainer(aVideoContainer), + mAudioOutputKey(aAudioOutputKey), + mWatchManager(this, aMainThread), + mFirstFrameVideoOutput(aFirstFrameVideoOutput) { + if (mFirstFrameVideoOutput) { + mWatchManager.Watch(mFirstFrameVideoOutput->mFirstFrameRendered, + &MediaStreamRenderer::SetFirstFrameRendered); + } + } + + void Shutdown() { + for (const auto& t : mAudioTracks.Clone()) { + if (t) { + RemoveTrack(t->AsAudioStreamTrack()); + } + } + if (mVideoTrack) { + RemoveTrack(mVideoTrack->AsVideoStreamTrack()); + } + mWatchManager.Shutdown(); + mFirstFrameVideoOutput = nullptr; + } + + void UpdateGraphTime() { + mGraphTime = + mGraphTimeDummy->mTrack->Graph()->CurrentTime() - *mGraphTimeOffset; + } + + void SetFirstFrameRendered() { + if (!mFirstFrameVideoOutput) { + return; + } + if (mVideoTrack) { + mVideoTrack->AsVideoStreamTrack()->RemoveVideoOutput( + mFirstFrameVideoOutput); + } + mWatchManager.Unwatch(mFirstFrameVideoOutput->mFirstFrameRendered, + &MediaStreamRenderer::SetFirstFrameRendered); + mFirstFrameVideoOutput = nullptr; + } + + void SetProgressingCurrentTime(bool aProgress) { + if (aProgress == mProgressingCurrentTime) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mGraphTimeDummy); + mProgressingCurrentTime = aProgress; + MediaTrackGraph* graph = mGraphTimeDummy->mTrack->Graph(); + if (mProgressingCurrentTime) { + mGraphTimeOffset = Some(graph->CurrentTime().Ref() - mGraphTime); + mWatchManager.Watch(graph->CurrentTime(), + &MediaStreamRenderer::UpdateGraphTime); + } else { + mWatchManager.Unwatch(graph->CurrentTime(), + &MediaStreamRenderer::UpdateGraphTime); + } + } + + void Start() { + if (mRendering) { + return; + } + + LOG(LogLevel::Info, ("MediaStreamRenderer=%p Start", this)); + mRendering = true; + + if (!mGraphTimeDummy) { + return; + } + + for (const auto& t : mAudioTracks) { + if (t) { + t->AsAudioStreamTrack()->AddAudioOutput(mAudioOutputKey, + mAudioOutputSink); + t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey, + mAudioOutputVolume); + } + } + + if (mVideoTrack) { + mVideoTrack->AsVideoStreamTrack()->AddVideoOutput(mVideoContainer); + } + } + + void Stop() { + if (!mRendering) { + return; + } + + LOG(LogLevel::Info, ("MediaStreamRenderer=%p Stop", this)); + mRendering = false; + + if (!mGraphTimeDummy) { + return; + } + + for (const auto& t : mAudioTracks) { + if (t) { + t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey); + } + } + // There is no longer an audio output that needs the device so the + // device may not start. Ensure the promise is resolved. + ResolveAudioDevicePromiseIfExists(__func__); + + if (mVideoTrack) { + mVideoTrack->AsVideoStreamTrack()->RemoveVideoOutput(mVideoContainer); + } + } + + void SetAudioOutputVolume(float aVolume) { + if (mAudioOutputVolume == aVolume) { + return; + } + mAudioOutputVolume = aVolume; + if (!mRendering) { + return; + } + for (const auto& t : mAudioTracks) { + if (t) { + t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey, + mAudioOutputVolume); + } + } + } + + RefPtr<GenericPromise> SetAudioOutputDevice(AudioDeviceInfo* aSink) { + MOZ_ASSERT(aSink); + MOZ_ASSERT(mAudioOutputSink != aSink); + LOG(LogLevel::Info, + ("MediaStreamRenderer=%p SetAudioOutputDevice name=%s\n", this, + NS_ConvertUTF16toUTF8(aSink->Name()).get())); + + mAudioOutputSink = aSink; + + if (!mRendering) { + MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty()); + return GenericPromise::CreateAndResolve(true, __func__); + } + + nsTArray<RefPtr<GenericPromise>> promises; + for (const auto& t : mAudioTracks) { + t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey); + promises.AppendElement(t->AsAudioStreamTrack()->AddAudioOutput( + mAudioOutputKey, mAudioOutputSink)); + t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey, + mAudioOutputVolume); + } + if (!promises.Length()) { + // Not active track, save it for later + MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty()); + return GenericPromise::CreateAndResolve(true, __func__); + } + + // Resolve any existing promise for a previous device so that promises + // resolve in order of setSinkId() invocation. + ResolveAudioDevicePromiseIfExists(__func__); + + RefPtr promise = mSetAudioDevicePromise.Ensure(__func__); + GenericPromise::AllSettled(GetCurrentSerialEventTarget(), promises) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr{this}, + this](const GenericPromise::AllSettledPromiseType:: + ResolveOrRejectValue& aValue) { + // This handler should have been disconnected if + // mSetAudioDevicePromise has been settled. + MOZ_ASSERT(!mSetAudioDevicePromise.IsEmpty()); + mDeviceStartedRequest.Complete(); + // The AudioStreamTrack::AddAudioOutput() promise is rejected + // either when the graph no longer needs the device, in which + // case this handler would have already been disconnected, or + // the graph is force shutdown. + // mSetAudioDevicePromise is resolved regardless of whether + // the AddAudioOutput() promises resolve or reject because + // the underlying device has been changed. + LOG(LogLevel::Info, + ("MediaStreamRenderer=%p SetAudioOutputDevice settled", + this)); + mSetAudioDevicePromise.Resolve(true, __func__); + }) + ->Track(mDeviceStartedRequest); + + return promise; + } + + void AddTrack(AudioStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(!mAudioTracks.Contains(aTrack)); + mAudioTracks.AppendElement(aTrack); + EnsureGraphTimeDummy(); + if (mRendering) { + aTrack->AddAudioOutput(mAudioOutputKey, mAudioOutputSink); + aTrack->SetAudioOutputVolume(mAudioOutputKey, mAudioOutputVolume); + } + } + void AddTrack(VideoStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(!mVideoTrack); + if (!mVideoContainer) { + return; + } + mVideoTrack = aTrack; + EnsureGraphTimeDummy(); + if (mFirstFrameVideoOutput) { + // Add the first frame output even if we are rendering. It will only + // accept one frame. If we are rendering, then the main output will + // overwrite that with the same frame (and possibly more frames). + aTrack->AddVideoOutput(mFirstFrameVideoOutput); + } + if (mRendering) { + aTrack->AddVideoOutput(mVideoContainer); + } + } + + void RemoveTrack(AudioStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(mAudioTracks.Contains(aTrack)); + if (mRendering) { + aTrack->RemoveAudioOutput(mAudioOutputKey); + } + mAudioTracks.RemoveElement(aTrack); + + if (mAudioTracks.IsEmpty()) { + // There is no longer an audio output that needs the device so the + // device may not start. Ensure the promise is resolved. + ResolveAudioDevicePromiseIfExists(__func__); + } + } + void RemoveTrack(VideoStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(mVideoTrack == aTrack); + if (!mVideoContainer) { + return; + } + if (mFirstFrameVideoOutput) { + aTrack->RemoveVideoOutput(mFirstFrameVideoOutput); + } + if (mRendering) { + aTrack->RemoveVideoOutput(mVideoContainer); + } + mVideoTrack = nullptr; + } + + double CurrentTime() const { + if (!mGraphTimeDummy) { + return 0.0; + } + + return mGraphTimeDummy->mTrack->GraphImpl()->MediaTimeToSeconds(mGraphTime); + } + + Watchable<GraphTime>& CurrentGraphTime() { return mGraphTime; } + + // Set if we're rendering video. + const RefPtr<VideoFrameContainer> mVideoContainer; + + // Set if we're rendering audio, nullptr otherwise. + void* const mAudioOutputKey; + + private: + ~MediaStreamRenderer() { Shutdown(); } + + void EnsureGraphTimeDummy() { + if (mGraphTimeDummy) { + return; + } + + MediaTrackGraph* graph = nullptr; + for (const auto& t : mAudioTracks) { + if (t && !t->Ended()) { + graph = t->Graph(); + break; + } + } + + if (!graph && mVideoTrack && !mVideoTrack->Ended()) { + graph = mVideoTrack->Graph(); + } + + if (!graph) { + return; + } + + // This dummy keeps `graph` alive and ensures access to it. + mGraphTimeDummy = MakeRefPtr<SharedDummyTrack>( + graph->CreateSourceTrack(MediaSegment::AUDIO)); + } + + void ResolveAudioDevicePromiseIfExists(const char* aMethodName) { + if (mSetAudioDevicePromise.IsEmpty()) { + return; + } + LOG(LogLevel::Info, + ("MediaStreamRenderer=%p resolve audio device promise", this)); + mSetAudioDevicePromise.Resolve(true, aMethodName); + mDeviceStartedRequest.Disconnect(); + } + + // True when all tracks are being rendered, i.e., when the media element is + // playing. + bool mRendering = false; + + // True while we're progressing mGraphTime. False otherwise. + bool mProgressingCurrentTime = false; + + // The audio output volume for all audio tracks. + float mAudioOutputVolume = 1.0f; + + // The sink device for all audio tracks. + RefPtr<AudioDeviceInfo> mAudioOutputSink; + // The promise returned from SetAudioOutputDevice() when an output is + // active. + MozPromiseHolder<GenericPromise> mSetAudioDevicePromise; + // Request tracking the promise to indicate when the device passed to + // SetAudioOutputDevice() is running. + MozPromiseRequestHolder<GenericPromise::AllSettledPromiseType> + mDeviceStartedRequest; + + // WatchManager for mGraphTime. + WatchManager<MediaStreamRenderer> mWatchManager; + + // A dummy MediaTrack to guarantee a MediaTrackGraph is kept alive while + // we're actively rendering, so we can track the graph's current time. Set + // when the first track is added, never unset. + RefPtr<SharedDummyTrack> mGraphTimeDummy; + + // Watchable that relays the graph's currentTime updates to the media element + // only while we're rendering. This is the current time of the rendering in + // GraphTime units. + Watchable<GraphTime> mGraphTime = {0, "MediaStreamRenderer::mGraphTime"}; + + // Nothing until a track has been added. Then, the current GraphTime at the + // time when we were last Start()ed. + Maybe<GraphTime> mGraphTimeOffset; + + // Currently enabled (and rendered) audio tracks. + nsTArray<WeakPtr<MediaStreamTrack>> mAudioTracks; + + // Currently selected (and rendered) video track. + WeakPtr<MediaStreamTrack> mVideoTrack; + + // Holds a reference to the first-frame-getting video output attached to + // mVideoTrack. Set by the constructor, unset when the media element tells us + // it has rendered the first frame. + RefPtr<FirstFrameVideoOutput> mFirstFrameVideoOutput; +}; + +static uint32_t sDecoderCaptureSourceId = 0; +static uint32_t sStreamCaptureSourceId = 0; +class HTMLMediaElement::MediaElementTrackSource + : public MediaStreamTrackSource, + public MediaStreamTrackSource::Sink, + public MediaStreamTrackConsumer { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaElementTrackSource, + MediaStreamTrackSource) + + /* MediaDecoder track source */ + MediaElementTrackSource(ProcessedMediaTrack* aTrack, nsIPrincipal* aPrincipal, + OutputMuteState aMuteState, bool aHasAlpha) + : MediaStreamTrackSource( + aPrincipal, nsString(), + TrackingId(TrackingId::Source::MediaElementDecoder, + sDecoderCaptureSourceId++, + TrackingId::TrackAcrossProcesses::Yes)), + mTrack(aTrack), + mIntendedElementMuteState(aMuteState), + mElementMuteState(aMuteState), + mMediaDecoderHasAlpha(Some(aHasAlpha)) { + MOZ_ASSERT(mTrack); + } + + /* MediaStream track source */ + MediaElementTrackSource(MediaStreamTrack* aCapturedTrack, + MediaStreamTrackSource* aCapturedTrackSource, + ProcessedMediaTrack* aTrack, MediaInputPort* aPort, + OutputMuteState aMuteState) + : MediaStreamTrackSource( + aCapturedTrackSource->GetPrincipal(), nsString(), + TrackingId(TrackingId::Source::MediaElementStream, + sStreamCaptureSourceId++, + TrackingId::TrackAcrossProcesses::Yes)), + mCapturedTrack(aCapturedTrack), + mCapturedTrackSource(aCapturedTrackSource), + mTrack(aTrack), + mPort(aPort), + mIntendedElementMuteState(aMuteState), + mElementMuteState(aMuteState) { + MOZ_ASSERT(mTrack); + MOZ_ASSERT(mCapturedTrack); + MOZ_ASSERT(mCapturedTrackSource); + MOZ_ASSERT(mPort); + + mCapturedTrack->AddConsumer(this); + mCapturedTrackSource->RegisterSink(this); + } + + void SetEnabled(bool aEnabled) { + if (!mTrack) { + return; + } + mTrack->SetDisabledTrackMode(aEnabled ? DisabledTrackMode::ENABLED + : DisabledTrackMode::SILENCE_FREEZE); + } + + void SetPrincipal(RefPtr<nsIPrincipal> aPrincipal) { + mPrincipal = std::move(aPrincipal); + MediaStreamTrackSource::PrincipalChanged(); + } + + void SetMutedByElement(OutputMuteState aMuteState) { + if (mIntendedElementMuteState == aMuteState) { + return; + } + mIntendedElementMuteState = aMuteState; + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "MediaElementTrackSource::SetMutedByElement", + [self = RefPtr<MediaElementTrackSource>(this), this, aMuteState] { + mElementMuteState = aMuteState; + MediaStreamTrackSource::MutedChanged(Muted()); + })); + } + + void Destroy() override { + if (mCapturedTrack) { + mCapturedTrack->RemoveConsumer(this); + mCapturedTrack = nullptr; + } + if (mCapturedTrackSource) { + mCapturedTrackSource->UnregisterSink(this); + mCapturedTrackSource = nullptr; + } + if (mTrack && !mTrack->IsDestroyed()) { + mTrack->Destroy(); + } + if (mPort) { + mPort->Destroy(); + mPort = nullptr; + } + } + + MediaSourceEnum GetMediaSource() const override { + return MediaSourceEnum::Other; + } + + void Stop() override { + // Do nothing. There may appear new output streams + // that need tracks sourced from this source, so we + // cannot destroy things yet. + } + + /** + * Do not keep the track source alive. The source lifetime is controlled by + * its associated tracks. + */ + bool KeepsSourceAlive() const override { return false; } + + /** + * Do not keep the track source on. It is controlled by its associated tracks. + */ + bool Enabled() const override { return false; } + + void Disable() override {} + + void Enable() override {} + + void PrincipalChanged() override { + if (!mCapturedTrackSource) { + // This could happen during shutdown. + return; + } + + SetPrincipal(mCapturedTrackSource->GetPrincipal()); + } + + void MutedChanged(bool aNewState) override { + MediaStreamTrackSource::MutedChanged(Muted()); + } + + void OverrideEnded() override { + Destroy(); + MediaStreamTrackSource::OverrideEnded(); + } + + void NotifyEnabledChanged(MediaStreamTrack* aTrack, bool aEnabled) override { + MediaStreamTrackSource::MutedChanged(Muted()); + } + + bool Muted() const { + return mElementMuteState == OutputMuteState::Muted || + (mCapturedTrack && + (mCapturedTrack->Muted() || !mCapturedTrack->Enabled())); + } + + bool HasAlpha() const override { + if (mCapturedTrack) { + return mCapturedTrack->AsVideoStreamTrack() + ? mCapturedTrack->AsVideoStreamTrack()->HasAlpha() + : false; + } + return mMediaDecoderHasAlpha.valueOr(false); + } + + ProcessedMediaTrack* Track() const { return mTrack; } + + private: + virtual ~MediaElementTrackSource() { Destroy(); }; + + RefPtr<MediaStreamTrack> mCapturedTrack; + RefPtr<MediaStreamTrackSource> mCapturedTrackSource; + const RefPtr<ProcessedMediaTrack> mTrack; + RefPtr<MediaInputPort> mPort; + // The mute state as intended by the media element. + OutputMuteState mIntendedElementMuteState; + // The mute state as applied to this track source. It is applied async, so + // needs to be tracked separately from the intended state. + OutputMuteState mElementMuteState; + // Some<bool> if this is a MediaDecoder track source. + const Maybe<bool> mMediaDecoderHasAlpha; +}; + +HTMLMediaElement::OutputMediaStream::OutputMediaStream( + RefPtr<DOMMediaStream> aStream, bool aCapturingAudioOnly, + bool aFinishWhenEnded) + : mStream(std::move(aStream)), + mCapturingAudioOnly(aCapturingAudioOnly), + mFinishWhenEnded(aFinishWhenEnded) {} +HTMLMediaElement::OutputMediaStream::~OutputMediaStream() = default; + +void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + HTMLMediaElement::OutputMediaStream& aField, + const char* aName, uint32_t aFlags) { + ImplCycleCollectionTraverse(aCallback, aField.mStream, "mStream", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mLiveTracks, "mLiveTracks", + aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedLoadingSrc, + "mFinishWhenEndedLoadingSrc", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedAttrStream, + "mFinishWhenEndedAttrStream", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedMediaSource, + "mFinishWhenEndedMediaSource", aFlags); +} + +void ImplCycleCollectionUnlink(HTMLMediaElement::OutputMediaStream& aField) { + ImplCycleCollectionUnlink(aField.mStream); + ImplCycleCollectionUnlink(aField.mLiveTracks); + ImplCycleCollectionUnlink(aField.mFinishWhenEndedLoadingSrc); + ImplCycleCollectionUnlink(aField.mFinishWhenEndedAttrStream); + ImplCycleCollectionUnlink(aField.mFinishWhenEndedMediaSource); +} + +NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::MediaElementTrackSource, + MediaStreamTrackSource) +NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::MediaElementTrackSource, + MediaStreamTrackSource) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + HTMLMediaElement::MediaElementTrackSource) +NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource) +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement::MediaElementTrackSource) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED( + HTMLMediaElement::MediaElementTrackSource, MediaStreamTrackSource) + tmp->Destroy(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCapturedTrack) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCapturedTrackSource) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + HTMLMediaElement::MediaElementTrackSource, MediaStreamTrackSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCapturedTrack) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCapturedTrackSource) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +/** + * There is a reference cycle involving this class: MediaLoadListener + * holds a reference to the HTMLMediaElement, which holds a reference + * to an nsIChannel, which holds a reference to this listener. + * We break the reference cycle in OnStartRequest by clearing mElement. + */ +class HTMLMediaElement::MediaLoadListener final + : public nsIChannelEventSink, + public nsIInterfaceRequestor, + public nsIObserver, + public nsIThreadRetargetableStreamListener { + ~MediaLoadListener() = default; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + public: + explicit MediaLoadListener(HTMLMediaElement* aElement) + : mElement(aElement), mLoadID(aElement->GetCurrentLoadID()) { + MOZ_ASSERT(mElement, "Must pass an element to call back"); + } + + private: + RefPtr<HTMLMediaElement> mElement; + nsCOMPtr<nsIStreamListener> mNextListener; + const uint32_t mLoadID; +}; + +NS_IMPL_ISUPPORTS(HTMLMediaElement::MediaLoadListener, nsIRequestObserver, + nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor, + nsIObserver, nsIThreadRetargetableStreamListener) + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + nsContentUtils::UnregisterShutdownObserver(this); + + // Clear mElement to break cycle so we don't leak on shutdown + mElement = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnStartRequest(nsIRequest* aRequest) { + nsContentUtils::UnregisterShutdownObserver(this); + + if (!mElement) { + // We've been notified by the shutdown observer, and are shutting down. + return NS_BINDING_ABORTED; + } + + // The element is only needed until we've had a chance to call + // InitializeDecoderForChannel. So make sure mElement is cleared here. + RefPtr<HTMLMediaElement> element; + element.swap(mElement); + + if (mLoadID != element->GetCurrentLoadID()) { + // The channel has been cancelled before we had a chance to create + // a decoder. Abort, don't dispatch an "error" event, as the new load + // may not be in an error state. + return NS_BINDING_ABORTED; + } + + // Don't continue to load if the request failed or has been canceled. + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(status)) { + if (element) { + // Handle media not loading error because source was a tracking URL (or + // fingerprinting, cryptomining, etc). + // We make a note of this media node by including it in a dedicated + // array of blocked tracking nodes under its parent document. + if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode( + status)) { + element->OwnerDoc()->AddBlockedNodeByClassifier(element); + } + element->NotifyLoadError( + nsPrintfCString("%u: %s", uint32_t(status), "Request failed")); + } + return status; + } + + nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(aRequest); + bool succeeded; + if (hc && NS_SUCCEEDED(hc->GetRequestSucceeded(&succeeded)) && !succeeded) { + uint32_t responseStatus = 0; + Unused << hc->GetResponseStatus(&responseStatus); + nsAutoCString statusText; + Unused << hc->GetResponseStatusText(statusText); + // we need status text for resist fingerprinting mode's message allowlist + if (statusText.IsEmpty()) { + net_GetDefaultStatusTextForCode(responseStatus, statusText); + } + element->NotifyLoadError( + nsPrintfCString("%u: %s", responseStatus, statusText.get())); + + nsAutoString code; + code.AppendInt(responseStatus); + nsAutoString src; + element->GetCurrentSrc(src); + AutoTArray<nsString, 2> params = {code, src}; + element->ReportLoadError("MediaLoadHttpError", params); + return NS_BINDING_ABORTED; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel && + NS_SUCCEEDED(rv = element->InitializeDecoderForChannel( + channel, getter_AddRefs(mNextListener))) && + mNextListener) { + rv = mNextListener->OnStartRequest(aRequest); + } else { + // If InitializeDecoderForChannel() returned an error, fire a network error. + if (NS_FAILED(rv) && !mNextListener) { + // Load failed, attempt to load the next candidate resource. If there + // are none, this will trigger a MEDIA_ERR_SRC_NOT_SUPPORTED error. + element->NotifyLoadError("Failed to init decoder"_ns); + } + // If InitializeDecoderForChannel did not return a listener (but may + // have otherwise succeeded), we abort the connection since we aren't + // interested in keeping the channel alive ourselves. + rv = NS_BINDING_ABORTED; + } + + return rv; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatus) { + if (mNextListener) { + return mNextListener->OnStopRequest(aRequest, aStatus); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aOffset, + uint32_t aCount) { + if (!mNextListener) { + NS_ERROR( + "Must have a chained listener; OnStartRequest should have " + "canceled this request"); + return NS_BINDING_ABORTED; + } + return mNextListener->OnDataAvailable(aRequest, aStream, aOffset, aCount); +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnDataFinished(nsresult aStatus) { + if (!mNextListener) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable = + do_QueryInterface(mNextListener); + if (retargetable) { + return retargetable->OnDataFinished(aStatus); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* cb) { + // TODO is this really correct?? See bug #579329. + if (mElement) { + mElement->OnChannelRedirect(aOldChannel, aNewChannel, aFlags); + } + nsCOMPtr<nsIChannelEventSink> sink = do_QueryInterface(mNextListener); + if (sink) { + return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, cb); + } + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::CheckListenerChain() { + MOZ_ASSERT(mNextListener); + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable = + do_QueryInterface(mNextListener); + if (retargetable) { + return retargetable->CheckListenerChain(); + } + return NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::GetInterface(const nsIID& aIID, + void** aResult) { + return QueryInterface(aIID, aResult); +} + +void HTMLMediaElement::ReportLoadError(const char* aMsg, + const nsTArray<nsString>& aParams) { + ReportToConsole(nsIScriptError::warningFlag, aMsg, aParams); +} + +void HTMLMediaElement::ReportToConsole( + uint32_t aErrorFlags, const char* aMsg, + const nsTArray<nsString>& aParams) const { + nsContentUtils::ReportToConsole(aErrorFlags, "Media"_ns, OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, aMsg, + aParams); +} + +class HTMLMediaElement::AudioChannelAgentCallback final + : public nsIAudioChannelAgentCallback { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(AudioChannelAgentCallback) + + explicit AudioChannelAgentCallback(HTMLMediaElement* aOwner) + : mOwner(aOwner), + mAudioChannelVolume(1.0), + mPlayingThroughTheAudioChannel(false), + mIsOwnerAudible(IsOwnerAudible()), + mIsShutDown(false) { + MOZ_ASSERT(mOwner); + MaybeCreateAudioChannelAgent(); + } + + void UpdateAudioChannelPlayingState() { + MOZ_ASSERT(!mIsShutDown); + bool playingThroughTheAudioChannel = IsPlayingThroughTheAudioChannel(); + + if (playingThroughTheAudioChannel != mPlayingThroughTheAudioChannel) { + if (!MaybeCreateAudioChannelAgent()) { + return; + } + + mPlayingThroughTheAudioChannel = playingThroughTheAudioChannel; + if (mPlayingThroughTheAudioChannel) { + StartAudioChannelAgent(); + } else { + StopAudioChanelAgent(); + } + } + } + + void NotifyPlayStateChanged() { + MOZ_ASSERT(!mIsShutDown); + UpdateAudioChannelPlayingState(); + } + + NS_IMETHODIMP WindowVolumeChanged(float aVolume, bool aMuted) override { + MOZ_ASSERT(mAudioChannelAgent); + + MOZ_LOG( + AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("HTMLMediaElement::AudioChannelAgentCallback, WindowVolumeChanged, " + "this = %p, aVolume = %f, aMuted = %s\n", + this, aVolume, aMuted ? "true" : "false")); + + if (mAudioChannelVolume != aVolume) { + mAudioChannelVolume = aVolume; + mOwner->SetVolumeInternal(); + } + + const uint32_t muted = mOwner->mMuted; + if (aMuted && !mOwner->ComputedMuted()) { + mOwner->SetMutedInternal(muted | MUTED_BY_AUDIO_CHANNEL); + } else if (!aMuted && mOwner->ComputedMuted()) { + mOwner->SetMutedInternal(muted & ~MUTED_BY_AUDIO_CHANNEL); + } + + return NS_OK; + } + + NS_IMETHODIMP WindowSuspendChanged(SuspendTypes aSuspend) override { + // Currently this method is only be used for delaying autoplay, and we've + // separated related codes to `MediaPlaybackDelayPolicy`. + return NS_OK; + } + + NS_IMETHODIMP WindowAudioCaptureChanged(bool aCapture) override { + MOZ_ASSERT(mAudioChannelAgent); + AudioCaptureTrackChangeIfNeeded(); + return NS_OK; + } + + void AudioCaptureTrackChangeIfNeeded() { + MOZ_ASSERT(!mIsShutDown); + if (!IsPlayingStarted()) { + return; + } + + MOZ_ASSERT(mAudioChannelAgent); + bool isCapturing = mAudioChannelAgent->IsWindowAudioCapturingEnabled(); + mOwner->AudioCaptureTrackChange(isCapturing); + } + + void NotifyAudioPlaybackChanged(AudibleChangedReasons aReason) { + MOZ_ASSERT(!mIsShutDown); + AudibleState newAudibleState = IsOwnerAudible(); + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("HTMLMediaElement::AudioChannelAgentCallback, " + "NotifyAudioPlaybackChanged, this=%p, current=%s, new=%s", + this, AudibleStateToStr(mIsOwnerAudible), + AudibleStateToStr(newAudibleState))); + if (mIsOwnerAudible == newAudibleState) { + return; + } + + mIsOwnerAudible = newAudibleState; + if (IsPlayingStarted()) { + mAudioChannelAgent->NotifyStartedAudible(mIsOwnerAudible, aReason); + } + } + + void Shutdown() { + MOZ_ASSERT(!mIsShutDown); + if (mAudioChannelAgent && mAudioChannelAgent->IsPlayingStarted()) { + StopAudioChanelAgent(); + } + mAudioChannelAgent = nullptr; + mIsShutDown = true; + } + + float GetEffectiveVolume() const { + MOZ_ASSERT(!mIsShutDown); + return static_cast<float>(mOwner->Volume()) * mAudioChannelVolume; + } + + private: + ~AudioChannelAgentCallback() { MOZ_ASSERT(mIsShutDown); }; + + bool MaybeCreateAudioChannelAgent() { + if (mAudioChannelAgent) { + return true; + } + + mAudioChannelAgent = new AudioChannelAgent(); + nsresult rv = + mAudioChannelAgent->Init(mOwner->OwnerDoc()->GetInnerWindow(), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mAudioChannelAgent = nullptr; + MOZ_LOG( + AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("HTMLMediaElement::AudioChannelAgentCallback, Fail to initialize " + "the audio channel agent, this = %p\n", + this)); + return false; + } + + return true; + } + + void StartAudioChannelAgent() { + MOZ_ASSERT(mAudioChannelAgent); + MOZ_ASSERT(!mAudioChannelAgent->IsPlayingStarted()); + if (NS_WARN_IF(NS_FAILED( + mAudioChannelAgent->NotifyStartedPlaying(IsOwnerAudible())))) { + return; + } + mAudioChannelAgent->PullInitialUpdate(); + } + + void StopAudioChanelAgent() { + MOZ_ASSERT(mAudioChannelAgent); + MOZ_ASSERT(mAudioChannelAgent->IsPlayingStarted()); + mAudioChannelAgent->NotifyStoppedPlaying(); + // If we have started audio capturing before, we have to tell media element + // to clear the output capturing track. + mOwner->AudioCaptureTrackChange(false); + } + + bool IsPlayingStarted() { + if (MaybeCreateAudioChannelAgent()) { + return mAudioChannelAgent->IsPlayingStarted(); + } + return false; + } + + AudibleState IsOwnerAudible() const { + // paused media doesn't produce any sound. + if (mOwner->mPaused) { + return AudibleState::eNotAudible; + } + return mOwner->IsAudible() ? AudibleState::eAudible + : AudibleState::eNotAudible; + } + + bool IsPlayingThroughTheAudioChannel() const { + // If we have an error, we are not playing. + if (mOwner->GetError()) { + return false; + } + + // We should consider any bfcached page or inactive document as non-playing. + if (!mOwner->OwnerDoc()->IsActive()) { + return false; + } + + // Media is suspended by the docshell. + if (mOwner->ShouldBeSuspendedByInactiveDocShell()) { + return false; + } + + // Are we paused + if (mOwner->mPaused) { + return false; + } + + // No audio track + if (!mOwner->HasAudio()) { + return false; + } + + // A loop always is playing + if (mOwner->HasAttr(nsGkAtoms::loop)) { + return true; + } + + // If we are actually playing... + if (mOwner->IsCurrentlyPlaying()) { + return true; + } + + // If we are playing an external stream. + if (mOwner->mSrcAttrStream) { + return true; + } + + return false; + } + + RefPtr<AudioChannelAgent> mAudioChannelAgent; + HTMLMediaElement* mOwner; + + // The audio channel volume + float mAudioChannelVolume; + // Is this media element playing? + bool mPlayingThroughTheAudioChannel; + // Indicate whether media element is audible for users. + AudibleState mIsOwnerAudible; + bool mIsShutDown; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement::AudioChannelAgentCallback) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN( + HTMLMediaElement::AudioChannelAgentCallback) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelAgent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN( + HTMLMediaElement::AudioChannelAgentCallback) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelAgent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + HTMLMediaElement::AudioChannelAgentCallback) + NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgentCallback) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLMediaElement::AudioChannelAgentCallback) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLMediaElement::AudioChannelAgentCallback) + +class HTMLMediaElement::ChannelLoader final { + public: + NS_INLINE_DECL_REFCOUNTING(ChannelLoader); + + void LoadInternal(HTMLMediaElement* aElement) { + if (mCancelled) { + return; + } + + // determine what security checks need to be performed in AsyncOpen(). + nsSecurityFlags securityFlags = + aElement->ShouldCheckAllowOrigin() + ? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + + if (aElement->GetCORSMode() == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + + securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + + MOZ_ASSERT( + aElement->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video)); + nsContentPolicyType contentPolicyType = + aElement->IsHTMLElement(nsGkAtoms::audio) + ? nsIContentPolicy::TYPE_INTERNAL_AUDIO + : nsIContentPolicy::TYPE_INTERNAL_VIDEO; + + // If aElement has 'triggeringprincipal' attribute, we will use the value as + // triggeringPrincipal for the channel, otherwise it will default to use + // aElement->NodePrincipal(). + // This function returns true when aElement has 'triggeringprincipal', so if + // setAttrs is true we will override the origin attributes on the channel + // later. + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + bool setAttrs = nsContentUtils::QueryTriggeringPrincipal( + aElement, aElement->mLoadingSrcTriggeringPrincipal, + getter_AddRefs(triggeringPrincipal)); + + nsCOMPtr<nsILoadGroup> loadGroup = aElement->GetDocumentLoadGroup(); + nsCOMPtr<nsIChannel> channel; + nsresult rv = NS_NewChannelWithTriggeringPrincipal( + getter_AddRefs(channel), aElement->mLoadingSrc, + static_cast<Element*>(aElement), triggeringPrincipal, securityFlags, + contentPolicyType, + nullptr, // aPerformanceStorage + loadGroup, + nullptr, // aCallbacks + nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY | + nsIChannel::LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE | + nsIChannel::LOAD_CALL_CONTENT_SNIFFERS); + + if (NS_FAILED(rv)) { + // Notify load error so the element will try next resource candidate. + aElement->NotifyLoadError("Fail to create channel"_ns); + return; + } + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + if (setAttrs) { + // The function simply returns NS_OK, so we ignore the return value. + Unused << loadInfo->SetOriginAttributes( + triggeringPrincipal->OriginAttributesRef()); + } + loadInfo->SetIsMediaRequest(true); + loadInfo->SetIsMediaInitialRequest(true); + + nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel)); + if (cos) { + if (aElement->mUseUrgentStartForChannel) { + cos->AddClassFlags(nsIClassOfService::UrgentStart); + + // Reset the flag to avoid loading again without initiated by user + // interaction. + aElement->mUseUrgentStartForChannel = false; + } + + // Unconditionally disable throttling since we want the media to fluently + // play even when we switch the tab to background. + cos->AddClassFlags(nsIClassOfService::DontThrottle); + } + + // The listener holds a strong reference to us. This creates a + // reference cycle, once we've set mChannel, which is manually broken + // in the listener's OnStartRequest method after it is finished with + // the element. The cycle will also be broken if we get a shutdown + // notification before OnStartRequest fires. Necko guarantees that + // OnStartRequest will eventually fire if we don't shut down first. + RefPtr<MediaLoadListener> loadListener = new MediaLoadListener(aElement); + + channel->SetNotificationCallbacks(loadListener); + + nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(channel); + if (hc) { + // Use a byte range request from the start of the resource. + // This enables us to detect if the stream supports byte range + // requests, and therefore seeking, early. + rv = hc->SetRequestHeader("Range"_ns, "bytes=0-"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + aElement->SetRequestHeaders(hc); + } + + rv = channel->AsyncOpen(loadListener); + if (NS_FAILED(rv)) { + // Notify load error so the element will try next resource candidate. + aElement->NotifyLoadError("Failed to open channel"_ns); + return; + } + + // Else the channel must be open and starting to download. If it encounters + // a non-catastrophic failure, it will set a new task to continue loading + // another candidate. It's safe to set it as mChannel now. + mChannel = channel; + + // loadListener will be unregistered either on shutdown or when + // OnStartRequest for the channel we just opened fires. + nsContentUtils::RegisterShutdownObserver(loadListener); + } + + nsresult Load(HTMLMediaElement* aElement) { + MOZ_ASSERT(aElement); + // Per bug 1235183 comment 8, we can't spin the event loop from stable + // state. Defer NS_NewChannel() to a new regular runnable. + return aElement->OwnerDoc()->Dispatch(NewRunnableMethod<HTMLMediaElement*>( + "ChannelLoader::LoadInternal", this, &ChannelLoader::LoadInternal, + aElement)); + } + + void Cancel() { + mCancelled = true; + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "HTMLMediaElement::ChannelLoader::Cancel"_ns); + mChannel = nullptr; + } + } + + void Done() { + MOZ_ASSERT(mChannel); + // Decoder successfully created, the decoder now owns the MediaResource + // which owns the channel. + mChannel = nullptr; + } + + nsresult Redirect(nsIChannel* aChannel, nsIChannel* aNewChannel, + uint32_t aFlags) { + NS_ASSERTION(aChannel == mChannel, "Channels should match!"); + mChannel = aNewChannel; + + // Handle forwarding of Range header so that the intial detection + // of seeking support (via result code 206) works across redirects. + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel); + NS_ENSURE_STATE(http); + + constexpr auto rangeHdr = "Range"_ns; + + nsAutoCString rangeVal; + if (NS_SUCCEEDED(http->GetRequestHeader(rangeHdr, rangeVal))) { + NS_ENSURE_STATE(!rangeVal.IsEmpty()); + + http = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(http); + + nsresult rv = http->SetRequestHeader(rangeHdr, rangeVal, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + private: + ~ChannelLoader() { MOZ_ASSERT(!mChannel); } + // Holds a reference to the first channel we open to the media resource. + // Once the decoder is created, control over the channel passes to the + // decoder, and we null out this reference. We must store this in case + // we need to cancel the channel before control of it passes to the decoder. + nsCOMPtr<nsIChannel> mChannel; + + bool mCancelled = false; +}; + +class HTMLMediaElement::ErrorSink { + public: + explicit ErrorSink(HTMLMediaElement* aOwner) : mOwner(aOwner) { + MOZ_ASSERT(mOwner); + } + + void SetError(uint16_t aErrorCode, const nsACString& aErrorDetails) { + // Since we have multiple paths calling into DecodeError, e.g. + // MediaKeys::Terminated and EMEH264Decoder::Error. We should take the 1st + // one only in order not to fire multiple 'error' events. + if (mError) { + return; + } + + if (!IsValidErrorCode(aErrorCode)) { + NS_ASSERTION(false, "Undefined MediaError codes!"); + return; + } + + mError = new MediaError(mOwner, aErrorCode, aErrorDetails); + mOwner->DispatchAsyncEvent(u"error"_ns); + if (mOwner->ReadyState() == HAVE_NOTHING && + aErrorCode == MEDIA_ERR_ABORTED) { + // https://html.spec.whatwg.org/multipage/embedded-content.html#media-data-processing-steps-list + // "If the media data fetching process is aborted by the user" + mOwner->DispatchAsyncEvent(u"abort"_ns); + mOwner->ChangeNetworkState(NETWORK_EMPTY); + mOwner->DispatchAsyncEvent(u"emptied"_ns); + if (mOwner->mDecoder) { + mOwner->ShutdownDecoder(); + } + } else if (aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED) { + mOwner->ChangeNetworkState(NETWORK_NO_SOURCE); + } else { + mOwner->ChangeNetworkState(NETWORK_IDLE); + } + } + + void ResetError() { mError = nullptr; } + + RefPtr<MediaError> mError; + + private: + bool IsValidErrorCode(const uint16_t& aErrorCode) const { + return (aErrorCode == MEDIA_ERR_DECODE || aErrorCode == MEDIA_ERR_NETWORK || + aErrorCode == MEDIA_ERR_ABORTED || + aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED); + } + + // Media elememt's life cycle would be longer than error sink, so we use the + // raw pointer and this class would only be referenced by media element. + HTMLMediaElement* mOwner; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcMediaSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcAttrStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourcePointer) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoadBlockedDoc) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceLoadCandidate) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelWrapper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mErrorSink->mError) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputStreams) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputTrackSources); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlayed); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioTrackList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoTrackList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingMediaKeys) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPlayPromises) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSeekDOMPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSetMediaKeysDOMPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventBlocker) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, + nsGenericHTMLElement) + tmp->RemoveMutationObserver(tmp); + if (tmp->mSrcStream) { + // Need to unhook everything that EndSrcMediaStreamPlayback would normally + // do, without creating any new strong references. + if (tmp->mSelectedVideoStreamTrack) { + tmp->mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(tmp); + } + if (tmp->mMediaStreamRenderer) { + tmp->mMediaStreamRenderer->Shutdown(); + // We null out mMediaStreamRenderer here since Shutdown() will shut down + // its WatchManager, and UpdateSrcStreamPotentiallyPlaying() contains a + // guard for this. + tmp->mMediaStreamRenderer = nullptr; + } + if (tmp->mSecondaryMediaStreamRenderer) { + tmp->mSecondaryMediaStreamRenderer->Shutdown(); + tmp->mSecondaryMediaStreamRenderer = nullptr; + } + if (tmp->mMediaStreamTrackListener) { + tmp->mSrcStream->UnregisterTrackListener( + tmp->mMediaStreamTrackListener.get()); + } + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcAttrStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcMediaSource) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourcePointer) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoadBlockedDoc) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceLoadCandidate) + if (tmp->mAudioChannelWrapper) { + tmp->mAudioChannelWrapper->Shutdown(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelWrapper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mErrorSink->mError) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputTrackSources) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingMediaKeys) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPlayPromises) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSeekDOMPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSetMediaKeysDOMPromise) + if (tmp->mMediaControlKeyListener) { + tmp->mMediaControlKeyListener->StopIfNeeded(); + } + if (tmp->mEventBlocker) { + tmp->mEventBlocker->Shutdown(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLMediaElement, + nsGenericHTMLElement) + +void HTMLMediaElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes, + size_t* aNodeSize) const { + nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize); + + // There are many other fields that might be worth reporting, but as seen in + // bug 1595603, the event we postpone to dispatch can grow to be very large + // sometimes, so at least report that. + if (mEventBlocker) { + *aNodeSize += + mEventBlocker->SizeOfExcludingThis(aSizes.mState.mMallocSizeOf); + } +} + +void HTMLMediaElement::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (aChild == mSourcePointer) { + mSourcePointer = aPreviousSibling; + } +} + +already_AddRefed<MediaSource> HTMLMediaElement::GetMozMediaSourceObject() + const { + RefPtr<MediaSource> source = mMediaSource; + return source.forget(); +} + +already_AddRefed<Promise> HTMLMediaElement::MozRequestDebugInfo( + ErrorResult& aRv) { + RefPtr<Promise> promise = CreateDOMPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + auto result = MakeUnique<dom::HTMLMediaElementDebugInfo>(); + if (mMediaKeys) { + GetEMEInfo(result->mEMEInfo); + } + if (mVideoFrameContainer) { + result->mCompositorDroppedFrames = + mVideoFrameContainer->GetDroppedImageCount(); + } + if (mDecoder) { + mDecoder->RequestDebugInfo(result->mDecoder) + ->Then( + AbstractMainThread(), __func__, + [promise, ptr = std::move(result)]() { + promise->MaybeResolve(ptr.get()); + }, + []() { + MOZ_ASSERT_UNREACHABLE("Unexpected RequestDebugInfo() rejection"); + }); + } else { + promise->MaybeResolve(result.get()); + } + return promise.forget(); +} + +/* static */ +void HTMLMediaElement::MozEnableDebugLog(const GlobalObject&) { + DecoderDoctorLogger::EnableLogging(); +} + +already_AddRefed<Promise> HTMLMediaElement::MozRequestDebugLog( + ErrorResult& aRv) { + RefPtr<Promise> promise = CreateDOMPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + DecoderDoctorLogger::RetrieveMessages(this)->Then( + AbstractMainThread(), __func__, + [promise](const nsACString& aString) { + promise->MaybeResolve(NS_ConvertUTF8toUTF16(aString)); + }, + [promise](nsresult rv) { promise->MaybeReject(rv); }); + + return promise.forget(); +} + +void HTMLMediaElement::SetVisible(bool aVisible) { + mForcedHidden = !aVisible; + if (mDecoder) { + mDecoder->SetForcedHidden(!aVisible); + } +} + +bool HTMLMediaElement::IsVideoDecodingSuspended() const { + return mDecoder && mDecoder->IsVideoDecodingSuspended(); +} + +double HTMLMediaElement::TotalVideoPlayTime() const { + return mDecoder ? mDecoder->GetTotalVideoPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::TotalVideoHDRPlayTime() const { + return mDecoder ? mDecoder->GetTotalVideoHDRPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::VisiblePlayTime() const { + return mDecoder ? mDecoder->GetVisibleVideoPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::InvisiblePlayTime() const { + return mDecoder ? mDecoder->GetInvisibleVideoPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::TotalAudioPlayTime() const { + return mDecoder ? mDecoder->GetTotalAudioPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::AudiblePlayTime() const { + return mDecoder ? mDecoder->GetAudiblePlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::InaudiblePlayTime() const { + return mDecoder ? mDecoder->GetInaudiblePlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::MutedPlayTime() const { + return mDecoder ? mDecoder->GetMutedPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::VideoDecodeSuspendedTime() const { + return mDecoder ? mDecoder->GetVideoDecodeSuspendedTimeInSeconds() : -1.0; +} + +void HTMLMediaElement::SetFormatDiagnosticsReportForMimeType( + const nsAString& aMimeType, DecoderDoctorReportType aType) { + DecoderDoctorDiagnostics diagnostics; + diagnostics.SetDecoderDoctorReportType(aType); + diagnostics.StoreFormatDiagnostics(OwnerDoc(), aMimeType, false /* can play*/, + __func__); +} + +void HTMLMediaElement::SetDecodeError(const nsAString& aError, + ErrorResult& aRv) { + // The reason we use this map-ish structure is because we can't use + // `CR.NS_ERROR.*` directly in test. In order to use them in test, we have to + // add them into `xpc.msg`. As we won't use `CR.NS_ERROR.*` in the production + // code, adding them to `xpc.msg` seems an overdesign and adding maintenance + // effort (exposing them in CR also needs to add a description, which is + // useless because we won't show them to users) + static struct { + const char* mName; + nsresult mResult; + } kSupportedErrorList[] = { + {"NS_ERROR_DOM_MEDIA_ABORT_ERR", NS_ERROR_DOM_MEDIA_ABORT_ERR}, + {"NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR", + NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR}, + {"NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR", + NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR}, + {"NS_ERROR_DOM_MEDIA_DECODE_ERR", NS_ERROR_DOM_MEDIA_DECODE_ERR}, + {"NS_ERROR_DOM_MEDIA_FATAL_ERR", NS_ERROR_DOM_MEDIA_FATAL_ERR}, + {"NS_ERROR_DOM_MEDIA_METADATA_ERR", NS_ERROR_DOM_MEDIA_METADATA_ERR}, + {"NS_ERROR_DOM_MEDIA_OVERFLOW_ERR", NS_ERROR_DOM_MEDIA_OVERFLOW_ERR}, + {"NS_ERROR_DOM_MEDIA_MEDIASINK_ERR", NS_ERROR_DOM_MEDIA_MEDIASINK_ERR}, + {"NS_ERROR_DOM_MEDIA_DEMUXER_ERR", NS_ERROR_DOM_MEDIA_DEMUXER_ERR}, + {"NS_ERROR_DOM_MEDIA_CDM_ERR", NS_ERROR_DOM_MEDIA_CDM_ERR}, + {"NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR", + NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR}}; + for (auto& error : kSupportedErrorList) { + if (strcmp(error.mName, NS_ConvertUTF16toUTF8(aError).get()) == 0) { + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreDecodeError(OwnerDoc(), error.mResult, u""_ns, __func__); + return; + } + } + aRv.Throw(NS_ERROR_FAILURE); +} + +void HTMLMediaElement::SetAudioSinkFailedStartup() { + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreEvent(OwnerDoc(), + {DecoderDoctorEvent::eAudioSinkStartup, + NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR}, + __func__); +} + +already_AddRefed<layers::Image> HTMLMediaElement::GetCurrentImage() { + MarkAsTainted(); + + // TODO: In bug 1345404, handle case when video decoder is already suspended. + ImageContainer* container = GetImageContainer(); + if (!container) { + return nullptr; + } + + AutoLockImage lockImage(container); + RefPtr<layers::Image> image = lockImage.GetImage(TimeStamp::Now()); + return image.forget(); +} + +bool HTMLMediaElement::HasSuspendTaint() const { + MOZ_ASSERT(!mDecoder || (mDecoder->HasSuspendTaint() == mHasSuspendTaint)); + return mHasSuspendTaint; +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::GetSrcObject() const { + return do_AddRef(mSrcAttrStream); +} + +void HTMLMediaElement::SetSrcObject(DOMMediaStream& aValue) { + SetSrcObject(&aValue); +} + +void HTMLMediaElement::SetSrcObject(DOMMediaStream* aValue) { + for (auto& outputStream : mOutputStreams) { + if (aValue == outputStream.mStream) { + ReportToConsole(nsIScriptError::warningFlag, + "MediaElementStreamCaptureCycle"); + return; + } + } + mSrcAttrStream = aValue; + UpdateAudioChannelPlayingState(); + DoLoad(); +} + +bool HTMLMediaElement::Ended() { + return (mDecoder && mDecoder->IsEnded()) || + (mSrcStream && mSrcStreamReportPlaybackEnded); +} + +void HTMLMediaElement::GetCurrentSrc(nsAString& aCurrentSrc) { + nsAutoCString src; + GetCurrentSpec(src); + CopyUTF8toUTF16(src, aCurrentSrc); +} + +nsresult HTMLMediaElement::OnChannelRedirect(nsIChannel* aChannel, + nsIChannel* aNewChannel, + uint32_t aFlags) { + MOZ_ASSERT(mChannelLoader); + return mChannelLoader->Redirect(aChannel, aNewChannel, aFlags); +} + +void HTMLMediaElement::ShutdownDecoder() { + RemoveMediaElementFromURITable(); + NS_ASSERTION(mDecoder, "Must have decoder to shut down"); + + mWaitingForKeyListener.DisconnectIfExists(); + if (mMediaSource) { + mMediaSource->CompletePendingTransactions(); + } + mDecoder->Shutdown(); + DDUNLINKCHILD(mDecoder.get()); + mDecoder = nullptr; +} + +void HTMLMediaElement::AbortExistingLoads() { + // Abort any already-running instance of the resource selection algorithm. + mLoadWaitStatus = NOT_WAITING; + + // Set a new load ID. This will cause events which were enqueued + // with a different load ID to silently be cancelled. + mCurrentLoadID++; + + // Immediately reject or resolve the already-dispatched + // nsResolveOrRejectPendingPlayPromisesRunners. These runners won't be + // executed again later since the mCurrentLoadID had been changed. + for (auto& runner : mPendingPlayPromisesRunners) { + runner->ResolveOrReject(); + } + mPendingPlayPromisesRunners.Clear(); + + if (mChannelLoader) { + mChannelLoader->Cancel(); + mChannelLoader = nullptr; + } + + bool fireTimeUpdate = false; + + if (mDecoder) { + fireTimeUpdate = mDecoder->GetCurrentTime() != 0.0; + ShutdownDecoder(); + } + if (mSrcStream) { + EndSrcMediaStreamPlayback(); + } + + if (mMediaSource) { + OwnerDoc()->RemoveMediaElementWithMSE(); + } + + RemoveMediaElementFromURITable(); + mLoadingSrcTriggeringPrincipal = nullptr; + DDLOG(DDLogCategory::Property, "loading_src", ""); + DDUNLINKCHILD(mMediaSource.get()); + mMediaSource = nullptr; + + if (mNetworkState == NETWORK_LOADING || mNetworkState == NETWORK_IDLE) { + DispatchAsyncEvent(u"abort"_ns); + } + + bool hadVideo = HasVideo(); + mErrorSink->ResetError(); + mCurrentPlayRangeStart = -1.0; + mPlayed = new TimeRanges(ToSupports(OwnerDoc())); + mLoadedDataFired = false; + mCanAutoplayFlag = true; + mIsLoadingFromSourceChildren = false; + mSuspendedAfterFirstFrame = false; + mAllowSuspendAfterFirstFrame = true; + mHaveQueuedSelectResource = false; + mSuspendedForPreloadNone = false; + mDownloadSuspendedByCache = false; + mMediaInfo = MediaInfo(); + mIsEncrypted = false; + mPendingEncryptedInitData.Reset(); + mWaitingForKey = NOT_WAITING_FOR_KEY; + mSourcePointer = nullptr; + mIsBlessed = false; + SetAudibleState(false); + + mTags = nullptr; + + if (mNetworkState != NETWORK_EMPTY) { + NS_ASSERTION(!mDecoder && !mSrcStream, + "How did someone setup a new stream/decoder already?"); + + DispatchAsyncEvent(u"emptied"_ns); + + // ChangeNetworkState() will call UpdateAudioChannelPlayingState() + // indirectly which depends on mPaused. So we need to update mPaused first. + if (!mPaused) { + mPaused = true; + PlayPromise::RejectPromises(TakePendingPlayPromises(), + NS_ERROR_DOM_MEDIA_ABORT_ERR); + } + ChangeNetworkState(NETWORK_EMPTY); + RemoveMediaTracks(); + UpdateOutputTrackSources(); + ChangeReadyState(HAVE_NOTHING); + + // TODO: Apply the rules for text track cue rendering Bug 865407 + if (mTextTrackManager) { + mTextTrackManager->GetTextTracks()->SetCuesInactive(); + } + + if (fireTimeUpdate) { + // Since we destroyed the decoder above, the current playback position + // will now be reported as 0. The playback position was non-zero when + // we destroyed the decoder, so fire a timeupdate event so that the + // change will be reflected in the controls. + FireTimeUpdate(TimeupdateType::eMandatory); + } + UpdateAudioChannelPlayingState(); + } + + if (IsVideo() && hadVideo) { + // Ensure we render transparent black after resetting video resolution. + Maybe<nsIntSize> size = Some(nsIntSize(0, 0)); + Invalidate(ImageSizeChanged::Yes, size, ForceInvalidate::No); + } + + // As aborting current load would stop current playback, so we have no need to + // resume a paused media element. + ClearResumeDelayedMediaPlaybackAgentIfNeeded(); + + mMediaControlKeyListener->StopIfNeeded(); + + // We may have changed mPaused, mCanAutoplayFlag, and other + // things which can affect AddRemoveSelfReference + AddRemoveSelfReference(); + + mIsRunningSelectResource = false; + + AssertReadyStateIsNothing(); +} + +void HTMLMediaElement::NoSupportedMediaSourceError( + const nsACString& aErrorDetails) { + if (mDecoder) { + ShutdownDecoder(); + } + + bool isSameOriginLoad = false; + nsresult rv = NS_ERROR_NOT_AVAILABLE; + if (mSrcAttrTriggeringPrincipal && mLoadingSrc) { + rv = mSrcAttrTriggeringPrincipal->IsSameOrigin(mLoadingSrc, + &isSameOriginLoad); + } + + if (NS_SUCCEEDED(rv) && !isSameOriginLoad) { + // aErrorDetails can include sensitive details like MimeType or HTTP Status + // Code. In case we're loading a 3rd party resource we should not leak this + // and pass a Generic Error Message + mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, + "Failed to open media"_ns); + } else { + mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails); + } + + RemoveMediaTracks(); + ChangeDelayLoadStatus(false); + UpdateAudioChannelPlayingState(); + PlayPromise::RejectPromises(TakePendingPlayPromises(), + NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR); +} + +// Runs a "synchronous section", a function that must run once the event loop +// has reached a "stable state" +// http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#synchronous-section +void HTMLMediaElement::RunInStableState(nsIRunnable* aRunnable) { + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction( + "HTMLMediaElement::RunInStableState", + [self = RefPtr<HTMLMediaElement>(this), loadId = GetCurrentLoadID(), + runnable = RefPtr<nsIRunnable>(aRunnable)]() { + if (self->GetCurrentLoadID() != loadId) { + return; + } + runnable->Run(); + }); + nsContentUtils::RunInStableState(task.forget()); +} + +void HTMLMediaElement::QueueLoadFromSourceTask() { + if (!mIsLoadingFromSourceChildren || mShuttingDown) { + return; + } + + if (mDecoder) { + // Reset readyState to HAVE_NOTHING since we're going to load a new decoder. + ShutdownDecoder(); + ChangeReadyState(HAVE_NOTHING); + } + + AssertReadyStateIsNothing(); + + ChangeDelayLoadStatus(true); + ChangeNetworkState(NETWORK_LOADING); + RefPtr<Runnable> r = + NewRunnableMethod("HTMLMediaElement::LoadFromSourceChildren", this, + &HTMLMediaElement::LoadFromSourceChildren); + RunInStableState(r); +} + +void HTMLMediaElement::QueueSelectResourceTask() { + // Don't allow multiple async select resource calls to be queued. + if (mHaveQueuedSelectResource) return; + mHaveQueuedSelectResource = true; + ChangeNetworkState(NETWORK_NO_SOURCE); + RefPtr<Runnable> r = + NewRunnableMethod("HTMLMediaElement::SelectResourceWrapper", this, + &HTMLMediaElement::SelectResourceWrapper); + RunInStableState(r); +} + +static bool HasSourceChildren(nsIContent* aElement) { + for (nsIContent* child = aElement->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsHTMLElement(nsGkAtoms::source)) { + return true; + } + } + return false; +} + +static nsCString DocumentOrigin(Document* aDoc) { + if (!aDoc) { + return "null"_ns; + } + nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal(); + if (!principal) { + return "null"_ns; + } + nsCString origin; + if (NS_FAILED(principal->GetOrigin(origin))) { + return "null"_ns; + } + return origin; +} + +void HTMLMediaElement::Load() { + LOG(LogLevel::Debug, + ("%p Load() hasSrcAttrStream=%d hasSrcAttr=%d hasSourceChildren=%d " + "handlingInput=%d hasAutoplayAttr=%d AllowedToPlay=%d " + "ownerDoc=%p (%s) ownerDocUserActivated=%d " + "muted=%d volume=%f", + this, !!mSrcAttrStream, HasAttr(nsGkAtoms::src), HasSourceChildren(this), + UserActivation::IsHandlingUserInput(), HasAttr(nsGkAtoms::autoplay), + AllowedToPlay(), OwnerDoc(), DocumentOrigin(OwnerDoc()).get(), + OwnerDoc()->HasBeenUserGestureActivated(), mMuted, mVolume)); + + if (mIsRunningLoadMethod) { + return; + } + + mIsDoingExplicitLoad = true; + DoLoad(); +} + +void HTMLMediaElement::DoLoad() { + // Check if media is allowed for the docshell. + nsCOMPtr<nsIDocShell> docShell = OwnerDoc()->GetDocShell(); + if (docShell && !docShell->GetAllowMedia()) { + LOG(LogLevel::Debug, ("%p Media not allowed", this)); + return; + } + + if (mIsRunningLoadMethod) { + return; + } + + if (UserActivation::IsHandlingUserInput()) { + // Detect if user has interacted with element so that play will not be + // blocked when initiated by a script. This enables sites to capture user + // intent to play by calling load() in the click handler of a "catalog + // view" of a gallery of videos. + mIsBlessed = true; + // Mark the channel as urgent-start when autoplay so that it will play the + // media from src after loading enough resource. + if (HasAttr(nsGkAtoms::autoplay)) { + mUseUrgentStartForChannel = true; + } + } + + SetPlayedOrSeeked(false); + mIsRunningLoadMethod = true; + AbortExistingLoads(); + SetPlaybackRate(mDefaultPlaybackRate, IgnoreErrors()); + QueueSelectResourceTask(); + ResetState(); + mIsRunningLoadMethod = false; +} + +void HTMLMediaElement::ResetState() { + // There might be a pending MediaDecoder::PlaybackPositionChanged() which + // will overwrite |mMediaInfo.mVideo.mDisplay| in UpdateMediaSize() to give + // staled videoWidth and videoHeight. We have to call ForgetElement() here + // such that the staled callbacks won't reach us. + if (mVideoFrameContainer) { + mVideoFrameContainer->ForgetElement(); + mVideoFrameContainer = nullptr; + } + if (mMediaStreamRenderer) { + // mMediaStreamRenderer, has a strong reference to mVideoFrameContainer. + mMediaStreamRenderer->Shutdown(); + mMediaStreamRenderer = nullptr; + } + if (mSecondaryMediaStreamRenderer) { + // mSecondaryMediaStreamRenderer, has a strong reference to + // the secondary VideoFrameContainer. + mSecondaryMediaStreamRenderer->Shutdown(); + mSecondaryMediaStreamRenderer = nullptr; + } +} + +void HTMLMediaElement::SelectResourceWrapper() { + SelectResource(); + MaybeBeginCloningVisually(); + mIsRunningSelectResource = false; + mHaveQueuedSelectResource = false; + mIsDoingExplicitLoad = false; +} + +void HTMLMediaElement::SelectResource() { + if (!mSrcAttrStream && !HasAttr(nsGkAtoms::src) && !HasSourceChildren(this)) { + // The media element has neither a src attribute nor any source + // element children, abort the load. + ChangeNetworkState(NETWORK_EMPTY); + ChangeDelayLoadStatus(false); + return; + } + + ChangeDelayLoadStatus(true); + + ChangeNetworkState(NETWORK_LOADING); + DispatchAsyncEvent(u"loadstart"_ns); + + // Delay setting mIsRunningSeletResource until after UpdatePreloadAction + // so that we don't lose our state change by bailing out of the preload + // state update + UpdatePreloadAction(); + mIsRunningSelectResource = true; + + // If we have a 'src' attribute, use that exclusively. + nsAutoString src; + if (mSrcAttrStream) { + SetupSrcMediaStreamPlayback(mSrcAttrStream); + } else if (GetAttr(nsGkAtoms::src, src)) { + nsCOMPtr<nsIURI> uri; + MediaResult rv = NewURIFromString(src, getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv)) { + LOG(LogLevel::Debug, ("%p Trying load from src=%s", this, + NS_ConvertUTF16toUTF8(src).get())); + NS_ASSERTION( + !mIsLoadingFromSourceChildren, + "Should think we're not loading from source children by default"); + + RemoveMediaElementFromURITable(); + if (!mSrcMediaSource) { + mLoadingSrc = uri; + } else { + mLoadingSrc = nullptr; + } + mLoadingSrcTriggeringPrincipal = mSrcAttrTriggeringPrincipal; + DDLOG(DDLogCategory::Property, "loading_src", + nsCString(NS_ConvertUTF16toUTF8(src))); + bool hadMediaSource = !!mMediaSource; + mMediaSource = mSrcMediaSource; + if (mMediaSource && !hadMediaSource) { + OwnerDoc()->AddMediaElementWithMSE(); + } + DDLINKCHILD("mediasource", mMediaSource.get()); + UpdatePreloadAction(); + if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) { + // preload:none media, suspend the load here before we make any + // network requests. + SuspendLoad(); + return; + } + + rv = LoadResource(); + if (NS_SUCCEEDED(rv)) { + return; + } + } else { + AutoTArray<nsString, 1> params = {src}; + ReportLoadError("MediaLoadInvalidURI", params); + rv = MediaResult(rv.Code(), "MediaLoadInvalidURI"); + } + // The media element has neither a src attribute nor a source element child: + // set the networkState to NETWORK_EMPTY, and abort these steps; the + // synchronous section ends. + GetMainThreadSerialEventTarget()->Dispatch(NewRunnableMethod<nsCString>( + "HTMLMediaElement::NoSupportedMediaSourceError", this, + &HTMLMediaElement::NoSupportedMediaSourceError, rv.Description())); + } else { + // Otherwise, the source elements will be used. + mIsLoadingFromSourceChildren = true; + LoadFromSourceChildren(); + } +} + +void HTMLMediaElement::NotifyLoadError(const nsACString& aErrorDetails) { + if (!mIsLoadingFromSourceChildren) { + LOG(LogLevel::Debug, ("NotifyLoadError(), no supported media error")); + NoSupportedMediaSourceError(aErrorDetails); + } else if (mSourceLoadCandidate) { + DispatchAsyncSourceError(mSourceLoadCandidate); + QueueLoadFromSourceTask(); + } else { + NS_WARNING("Should know the source we were loading from!"); + } +} + +void HTMLMediaElement::NotifyMediaTrackAdded(dom::MediaTrack* aTrack) { + // The set of tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::NotifyMediaTrackRemoved(dom::MediaTrack* aTrack) { + // The set of tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::NotifyMediaTrackEnabled(dom::MediaTrack* aTrack) { + MOZ_ASSERT(aTrack); + if (!aTrack) { + return; + } +#ifdef DEBUG + nsString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("MediaElement %p %sTrack with id %s enabled", this, + aTrack->AsAudioTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); +#endif + + MOZ_ASSERT((aTrack->AsAudioTrack() && aTrack->AsAudioTrack()->Enabled()) || + (aTrack->AsVideoTrack() && aTrack->AsVideoTrack()->Selected())); + + if (aTrack->AsAudioTrack()) { + SetMutedInternal(mMuted & ~MUTED_BY_AUDIO_TRACK); + } else if (aTrack->AsVideoTrack()) { + if (!IsVideo()) { + MOZ_ASSERT(false); + return; + } + mDisableVideo = false; + } else { + MOZ_ASSERT(false, "Unknown track type"); + } + + if (mSrcStream) { + if (AudioTrack* t = aTrack->AsAudioTrack()) { + if (mMediaStreamRenderer) { + mMediaStreamRenderer->AddTrack(t->GetAudioStreamTrack()); + } + } else if (VideoTrack* t = aTrack->AsVideoTrack()) { + MOZ_ASSERT(!mSelectedVideoStreamTrack); + + mSelectedVideoStreamTrack = t->GetVideoStreamTrack(); + mSelectedVideoStreamTrack->AddPrincipalChangeObserver(this); + if (mMediaStreamRenderer) { + mMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack); + } + if (mMediaInfo.HasVideo()) { + mMediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha()); + } + nsContentUtils::CombineResourcePrincipals( + &mSrcStreamVideoPrincipal, mSelectedVideoStreamTrack->GetPrincipal()); + } + } + + // The set of enabled/selected tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::NotifyMediaTrackDisabled(dom::MediaTrack* aTrack) { + MOZ_ASSERT(aTrack); + if (!aTrack) { + return; + } + + nsString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("MediaElement %p %sTrack with id %s disabled", this, + aTrack->AsAudioTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); + + MOZ_ASSERT((!aTrack->AsAudioTrack() || !aTrack->AsAudioTrack()->Enabled()) && + (!aTrack->AsVideoTrack() || !aTrack->AsVideoTrack()->Selected())); + + if (AudioTrack* t = aTrack->AsAudioTrack()) { + if (mSrcStream) { + if (mMediaStreamRenderer) { + mMediaStreamRenderer->RemoveTrack(t->GetAudioStreamTrack()); + } + } + // If we don't have any live tracks, we don't need to mute MediaElement. + MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked"); + if (AudioTracks()->Length() > 0) { + bool shouldMute = true; + for (uint32_t i = 0; i < AudioTracks()->Length(); ++i) { + if ((*AudioTracks())[i]->Enabled()) { + shouldMute = false; + break; + } + } + + if (shouldMute) { + SetMutedInternal(mMuted | MUTED_BY_AUDIO_TRACK); + } + } + } else if (aTrack->AsVideoTrack()) { + if (mSrcStream) { + MOZ_DIAGNOSTIC_ASSERT(mSelectedVideoStreamTrack == + aTrack->AsVideoTrack()->GetVideoStreamTrack()); + if (mMediaStreamRenderer) { + mMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack); + } + mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(this); + mSelectedVideoStreamTrack = nullptr; + } + } + + // The set of enabled/selected tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::DealWithFailedElement(nsIContent* aSourceElement) { + if (mShuttingDown) { + return; + } + + DispatchAsyncSourceError(aSourceElement); + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod("HTMLMediaElement::QueueLoadFromSourceTask", this, + &HTMLMediaElement::QueueLoadFromSourceTask)); +} + +void HTMLMediaElement::LoadFromSourceChildren() { + NS_ASSERTION(mDelayingLoadEvent, + "Should delay load event (if in document) during load"); + NS_ASSERTION(mIsLoadingFromSourceChildren, + "Must remember we're loading from source children"); + + AddMutationObserverUnlessExists(this); + + RemoveMediaTracks(); + + while (true) { + HTMLSourceElement* child = GetNextSource(); + if (!child) { + // Exhausted candidates, wait for more candidates to be appended to + // the media element. + mLoadWaitStatus = WAITING_FOR_SOURCE; + ChangeNetworkState(NETWORK_NO_SOURCE); + ChangeDelayLoadStatus(false); + ReportLoadError("MediaLoadExhaustedCandidates"); + return; + } + + // Must have src attribute. + nsAutoString src; + if (!child->GetAttr(nsGkAtoms::src, src)) { + ReportLoadError("MediaLoadSourceMissingSrc"); + DealWithFailedElement(child); + return; + } + + // If we have a type attribute, it must be a supported type. + nsAutoString type; + if (child->GetAttr(nsGkAtoms::type, type) && !type.IsEmpty()) { + DecoderDoctorDiagnostics diagnostics; + CanPlayStatus canPlay = GetCanPlay(type, &diagnostics); + diagnostics.StoreFormatDiagnostics(OwnerDoc(), type, + canPlay != CANPLAY_NO, __func__); + if (canPlay == CANPLAY_NO) { + // Check that at least one other source child exists and report that + // we will try to load that one next. + nsIContent* nextChild = mSourcePointer->GetNextSibling(); + AutoTArray<nsString, 2> params = {type, src}; + + while (nextChild) { + if (nextChild && nextChild->IsHTMLElement(nsGkAtoms::source)) { + ReportLoadError("MediaLoadUnsupportedTypeAttributeLoadingNextChild", + params); + break; + } + + nextChild = nextChild->GetNextSibling(); + }; + + if (!nextChild) { + ReportLoadError("MediaLoadUnsupportedTypeAttribute", params); + } + + DealWithFailedElement(child); + return; + } + } + nsAutoString media; + child->GetAttr(nsGkAtoms::media, media); + HTMLSourceElement* childSrc = HTMLSourceElement::FromNode(child); + MOZ_ASSERT(childSrc, "Expect child to be HTMLSourceElement"); + if (childSrc && !childSrc->MatchesCurrentMedia()) { + AutoTArray<nsString, 2> params = {media, src}; + ReportLoadError("MediaLoadSourceMediaNotMatched", params); + DealWithFailedElement(child); + LOG(LogLevel::Debug, + ("%p Media did not match from <source>=%s type=%s media=%s", this, + NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(), + NS_ConvertUTF16toUTF8(media).get())); + return; + } + LOG(LogLevel::Debug, + ("%p Trying load from <source>=%s type=%s media=%s", this, + NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(), + NS_ConvertUTF16toUTF8(media).get())); + + nsCOMPtr<nsIURI> uri; + NewURIFromString(src, getter_AddRefs(uri)); + if (!uri) { + AutoTArray<nsString, 1> params = {src}; + ReportLoadError("MediaLoadInvalidURI", params); + DealWithFailedElement(child); + return; + } + + RemoveMediaElementFromURITable(); + mLoadingSrc = uri; + mLoadingSrcTriggeringPrincipal = child->GetSrcTriggeringPrincipal(); + DDLOG(DDLogCategory::Property, "loading_src", + nsCString(NS_ConvertUTF16toUTF8(src))); + bool hadMediaSource = !!mMediaSource; + mMediaSource = child->GetSrcMediaSource(); + if (mMediaSource && !hadMediaSource) { + OwnerDoc()->AddMediaElementWithMSE(); + } + DDLINKCHILD("mediasource", mMediaSource.get()); + NS_ASSERTION(mNetworkState == NETWORK_LOADING, + "Network state should be loading"); + + if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) { + // preload:none media, suspend the load here before we make any + // network requests. + SuspendLoad(); + return; + } + + if (NS_SUCCEEDED(LoadResource())) { + return; + } + + // If we fail to load, loop back and try loading the next resource. + DispatchAsyncSourceError(child); + } + MOZ_ASSERT_UNREACHABLE("Execution should not reach here!"); +} + +void HTMLMediaElement::SuspendLoad() { + mSuspendedForPreloadNone = true; + ChangeNetworkState(NETWORK_IDLE); + ChangeDelayLoadStatus(false); +} + +void HTMLMediaElement::ResumeLoad(PreloadAction aAction) { + NS_ASSERTION(mSuspendedForPreloadNone, + "Must be halted for preload:none to resume from preload:none " + "suspended load."); + mSuspendedForPreloadNone = false; + mPreloadAction = aAction; + ChangeDelayLoadStatus(true); + ChangeNetworkState(NETWORK_LOADING); + if (!mIsLoadingFromSourceChildren) { + // We were loading from the element's src attribute. + MediaResult rv = LoadResource(); + if (NS_FAILED(rv)) { + NoSupportedMediaSourceError(rv.Description()); + } + } else { + // We were loading from a child <source> element. Try to resume the + // load of that child, and if that fails, try the next child. + if (NS_FAILED(LoadResource())) { + LoadFromSourceChildren(); + } + } +} + +bool HTMLMediaElement::AllowedToPlay() const { + return media::AutoplayPolicy::IsAllowedToPlay(*this); +} + +uint32_t HTMLMediaElement::GetPreloadDefault() const { + if (mMediaSource) { + return HTMLMediaElement::PRELOAD_ATTR_METADATA; + } + if (OnCellularConnection()) { + return Preferences::GetInt("media.preload.default.cellular", + HTMLMediaElement::PRELOAD_ATTR_NONE); + } + return Preferences::GetInt("media.preload.default", + HTMLMediaElement::PRELOAD_ATTR_METADATA); +} + +uint32_t HTMLMediaElement::GetPreloadDefaultAuto() const { + if (OnCellularConnection()) { + return Preferences::GetInt("media.preload.auto.cellular", + HTMLMediaElement::PRELOAD_ATTR_METADATA); + } + return Preferences::GetInt("media.preload.auto", + HTMLMediaElement::PRELOAD_ENOUGH); +} + +void HTMLMediaElement::UpdatePreloadAction() { + PreloadAction nextAction = PRELOAD_UNDEFINED; + // If autoplay is set, or we're playing, we should always preload data, + // as we'll need it to play. + if ((AllowedToPlay() && HasAttr(nsGkAtoms::autoplay)) || !mPaused) { + nextAction = HTMLMediaElement::PRELOAD_ENOUGH; + } else { + // Find the appropriate preload action by looking at the attribute. + const nsAttrValue* val = + mAttrs.GetAttr(nsGkAtoms::preload, kNameSpaceID_None); + // MSE doesn't work if preload is none, so it ignores the pref when src is + // from MSE. + uint32_t preloadDefault = GetPreloadDefault(); + uint32_t preloadAuto = GetPreloadDefaultAuto(); + if (!val) { + // Attribute is not set. Use the preload action specified by the + // media.preload.default pref, or just preload metadata if not present. + nextAction = static_cast<PreloadAction>(preloadDefault); + } else if (val->Type() == nsAttrValue::eEnum) { + PreloadAttrValue attr = + static_cast<PreloadAttrValue>(val->GetEnumValue()); + if (attr == HTMLMediaElement::PRELOAD_ATTR_EMPTY || + attr == HTMLMediaElement::PRELOAD_ATTR_AUTO) { + nextAction = static_cast<PreloadAction>(preloadAuto); + } else if (attr == HTMLMediaElement::PRELOAD_ATTR_METADATA) { + nextAction = HTMLMediaElement::PRELOAD_METADATA; + } else if (attr == HTMLMediaElement::PRELOAD_ATTR_NONE) { + nextAction = HTMLMediaElement::PRELOAD_NONE; + } + } else { + // Use the suggested "missing value default" of "metadata", or the value + // specified by the media.preload.default, if present. + nextAction = static_cast<PreloadAction>(preloadDefault); + } + } + + if (nextAction == HTMLMediaElement::PRELOAD_NONE && mIsDoingExplicitLoad) { + nextAction = HTMLMediaElement::PRELOAD_METADATA; + } + + mPreloadAction = nextAction; + + if (nextAction == HTMLMediaElement::PRELOAD_ENOUGH) { + if (mSuspendedForPreloadNone) { + // Our load was previouly suspended due to the media having preload + // value "none". The preload value has changed to preload:auto, so + // resume the load. + ResumeLoad(PRELOAD_ENOUGH); + } else { + // Preload as much of the video as we can, i.e. don't suspend after + // the first frame. + StopSuspendingAfterFirstFrame(); + } + + } else if (nextAction == HTMLMediaElement::PRELOAD_METADATA) { + // Ensure that the video can be suspended after first frame. + mAllowSuspendAfterFirstFrame = true; + if (mSuspendedForPreloadNone) { + // Our load was previouly suspended due to the media having preload + // value "none". The preload value has changed to preload:metadata, so + // resume the load. We'll pause the load again after we've read the + // metadata. + ResumeLoad(PRELOAD_METADATA); + } + } +} + +MediaResult HTMLMediaElement::LoadResource() { + NS_ASSERTION(mDelayingLoadEvent, + "Should delay load event (if in document) during load"); + + if (mChannelLoader) { + mChannelLoader->Cancel(); + mChannelLoader = nullptr; + } + + // Set the media element's CORS mode only when loading a resource + mCORSMode = AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)); + + HTMLMediaElement* other = LookupMediaElementURITable(mLoadingSrc); + if (other && other->mDecoder) { + // Clone it. + // TODO: remove the cast by storing ChannelMediaDecoder in the URI table. + nsresult rv = InitializeDecoderAsClone( + static_cast<ChannelMediaDecoder*>(other->mDecoder.get())); + if (NS_SUCCEEDED(rv)) return rv; + } + + if (mMediaSource) { + MediaDecoderInit decoderInit( + this, this, mMuted ? 0.0 : mVolume, mPreservesPitch, + ClampPlaybackRate(mPlaybackRate), + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint, + HasAttr(nsGkAtoms::loop), + MediaContainerType(MEDIAMIMETYPE("application/x.mediasource"))); + + RefPtr<MediaSourceDecoder> decoder = new MediaSourceDecoder(decoderInit); + if (!mMediaSource->Attach(decoder)) { + // TODO: Handle failure: run "If the media data cannot be fetched at + // all, due to network errors, causing the user agent to give up + // trying to fetch the resource" section of resource fetch algorithm. + decoder->Shutdown(); + return MediaResult(NS_ERROR_FAILURE, "Failed to attach MediaSource"); + } + ChangeDelayLoadStatus(false); + nsresult rv = decoder->Load(mMediaSource->GetPrincipal()); + if (NS_FAILED(rv)) { + decoder->Shutdown(); + LOG(LogLevel::Debug, + ("%p Failed to load for decoder %p", this, decoder.get())); + return MediaResult(rv, "Fail to load decoder"); + } + rv = FinishDecoderSetup(decoder); + return MediaResult(rv, "Failed to set up decoder"); + } + + AssertReadyStateIsNothing(); + + RefPtr<ChannelLoader> loader = new ChannelLoader; + nsresult rv = loader->Load(this); + if (NS_SUCCEEDED(rv)) { + mChannelLoader = std::move(loader); + } + return MediaResult(rv, "Failed to load channel"); +} + +nsresult HTMLMediaElement::LoadWithChannel(nsIChannel* aChannel, + nsIStreamListener** aListener) { + NS_ENSURE_ARG_POINTER(aChannel); + NS_ENSURE_ARG_POINTER(aListener); + + *aListener = nullptr; + + // Make sure we don't reenter during synchronous abort events. + if (mIsRunningLoadMethod) return NS_OK; + mIsRunningLoadMethod = true; + AbortExistingLoads(); + mIsRunningLoadMethod = false; + + mLoadingSrcTriggeringPrincipal = nullptr; + nsresult rv = aChannel->GetOriginalURI(getter_AddRefs(mLoadingSrc)); + NS_ENSURE_SUCCESS(rv, rv); + + ChangeDelayLoadStatus(true); + rv = InitializeDecoderForChannel(aChannel, aListener); + if (NS_FAILED(rv)) { + ChangeDelayLoadStatus(false); + return rv; + } + + SetPlaybackRate(mDefaultPlaybackRate, IgnoreErrors()); + DispatchAsyncEvent(u"loadstart"_ns); + + return NS_OK; +} + +bool HTMLMediaElement::Seeking() const { + return mDecoder && mDecoder->IsSeeking(); +} + +double HTMLMediaElement::CurrentTime() const { + if (mMediaStreamRenderer) { + return ToMicrosecondResolution(mMediaStreamRenderer->CurrentTime()); + } + + if (mDefaultPlaybackStartPosition == 0.0 && mDecoder) { + return std::clamp(mDecoder->GetCurrentTime(), 0.0, mDecoder->GetDuration()); + } + + return mDefaultPlaybackStartPosition; +} + +void HTMLMediaElement::FastSeek(double aTime, ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p FastSeek(%f) called by JS", this, aTime)); + Seek(aTime, SeekTarget::PrevSyncPoint, IgnoreErrors()); +} + +already_AddRefed<Promise> HTMLMediaElement::SeekToNextFrame(ErrorResult& aRv) { + /* This will cause JIT code to be kept around longer, to help performance + * when using SeekToNextFrame to iterate through every frame of a video. + */ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + + if (win) { + if (JSObject* obj = win->AsGlobal()->GetGlobalJSObject()) { + js::NotifyAnimationActivity(obj); + } + } + + Seek(CurrentTime(), SeekTarget::NextFrame, aRv); + if (aRv.Failed()) { + return nullptr; + } + + mSeekDOMPromise = CreateDOMPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return do_AddRef(mSeekDOMPromise); +} + +void HTMLMediaElement::SetCurrentTime(double aCurrentTime, ErrorResult& aRv) { + LOG(LogLevel::Debug, + ("%p SetCurrentTime(%lf) called by JS", this, aCurrentTime)); + Seek(aCurrentTime, SeekTarget::Accurate, IgnoreErrors()); +} + +/** + * Check if aValue is inside a range of aRanges, and if so returns true + * and puts the range index in aIntervalIndex. If aValue is not + * inside a range, returns false, and aIntervalIndex + * is set to the index of the range which starts immediately after aValue + * (and can be aRanges.Length() if aValue is after the last range). + */ +static bool IsInRanges(TimeRanges& aRanges, double aValue, + uint32_t& aIntervalIndex) { + uint32_t length = aRanges.Length(); + + for (uint32_t i = 0; i < length; i++) { + double start = aRanges.Start(i); + if (start > aValue) { + aIntervalIndex = i; + return false; + } + double end = aRanges.End(i); + if (aValue <= end) { + aIntervalIndex = i; + return true; + } + } + aIntervalIndex = length; + return false; +} + +void HTMLMediaElement::Seek(double aTime, SeekTarget::Type aSeekType, + ErrorResult& aRv) { + // Note: Seek is called both by synchronous code that expects errors thrown in + // aRv, as well as asynchronous code that expects a promise. Make sure all + // synchronous errors are returned using aRv, not promise rejections. + + // aTime should be non-NaN. + MOZ_ASSERT(!std::isnan(aTime)); + + // Seeking step1, Set the media element's show poster flag to false. + // https://html.spec.whatwg.org/multipage/media.html#dom-media-seek + mShowPoster = false; + + // Detect if user has interacted with element by seeking so that + // play will not be blocked when initiated by a script. + if (UserActivation::IsHandlingUserInput()) { + mIsBlessed = true; + } + + StopSuspendingAfterFirstFrame(); + + if (mSrcAttrStream) { + // do nothing since media streams have an empty Seekable range. + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mPlayed && mCurrentPlayRangeStart != -1.0) { + double rangeEndTime = CurrentTime(); + LOG(LogLevel::Debug, ("%p Adding \'played\' a range : [%f, %f]", this, + mCurrentPlayRangeStart, rangeEndTime)); + // Multiple seek without playing, or seek while playing. + if (mCurrentPlayRangeStart != rangeEndTime) { + // Don't round the left of the interval: it comes from script and needs + // to be exact. + mPlayed->Add(mCurrentPlayRangeStart, rangeEndTime); + } + // Reset the current played range start time. We'll re-set it once + // the seek completes. + mCurrentPlayRangeStart = -1.0; + } + + if (mReadyState == HAVE_NOTHING) { + mDefaultPlaybackStartPosition = aTime; + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (!mDecoder) { + // mDecoder must always be set in order to reach this point. + NS_ASSERTION(mDecoder, "SetCurrentTime failed: no decoder"); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Clamp the seek target to inside the seekable ranges. + media::TimeRanges seekableRanges = mDecoder->GetSeekableTimeRanges(); + if (seekableRanges.IsInvalid()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + RefPtr<TimeRanges> seekable = + new TimeRanges(ToSupports(OwnerDoc()), seekableRanges); + uint32_t length = seekable->Length(); + if (length == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // If the position we want to seek to is not in a seekable range, we seek + // to the closest position in the seekable ranges instead. If two positions + // are equally close, we seek to the closest position from the currentTime. + // See seeking spec, point 7 : + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#seeking + uint32_t range = 0; + bool isInRange = IsInRanges(*seekable, aTime, range); + if (!isInRange) { + if (range == 0) { + // aTime is before the first range in |seekable|, the closest point we can + // seek to is the start of the first range. + aTime = seekable->Start(0); + } else if (range == length) { + // Seek target is after the end last range in seekable data. + // Clamp the seek target to the end of the last seekable range. + aTime = seekable->End(length - 1); + } else { + double leftBound = seekable->End(range - 1); + double rightBound = seekable->Start(range); + double distanceLeft = Abs(leftBound - aTime); + double distanceRight = Abs(rightBound - aTime); + if (distanceLeft == distanceRight) { + double currentTime = CurrentTime(); + distanceLeft = Abs(leftBound - currentTime); + distanceRight = Abs(rightBound - currentTime); + } + aTime = (distanceLeft < distanceRight) ? leftBound : rightBound; + } + } + + // TODO: The spec requires us to update the current time to reflect the + // actual seek target before beginning the synchronous section, but + // that requires changing all MediaDecoderReaders to support telling + // us the fastSeek target, and it's currently not possible to get + // this information as we don't yet control the demuxer for all + // MediaDecoderReaders. + + mPlayingBeforeSeek = IsPotentiallyPlaying(); + + // The media backend is responsible for dispatching the timeupdate + // event if it changes the playback position as a result of the seek. + LOG(LogLevel::Debug, ("%p SetCurrentTime(%f) starting seek", this, aTime)); + mDecoder->Seek(aTime, aSeekType); + + // We changed whether we're seeking so we need to AddRemoveSelfReference. + AddRemoveSelfReference(); +} + +double HTMLMediaElement::Duration() const { + if (mSrcStream) { + if (mSrcStreamPlaybackEnded) { + return CurrentTime(); + } + return std::numeric_limits<double>::infinity(); + } + + if (mDecoder) { + return mDecoder->GetDuration(); + } + + return std::numeric_limits<double>::quiet_NaN(); +} + +already_AddRefed<TimeRanges> HTMLMediaElement::Seekable() const { + media::TimeRanges seekable = + mDecoder ? mDecoder->GetSeekableTimeRanges() : media::TimeRanges(); + RefPtr<TimeRanges> ranges = new TimeRanges( + ToSupports(OwnerDoc()), seekable.ToMicrosecondResolution()); + return ranges.forget(); +} + +already_AddRefed<TimeRanges> HTMLMediaElement::Played() { + RefPtr<TimeRanges> ranges = new TimeRanges(ToSupports(OwnerDoc())); + + uint32_t timeRangeCount = 0; + if (mPlayed) { + timeRangeCount = mPlayed->Length(); + } + for (uint32_t i = 0; i < timeRangeCount; i++) { + double begin = mPlayed->Start(i); + double end = mPlayed->End(i); + ranges->Add(begin, end); + } + + if (mCurrentPlayRangeStart != -1.0) { + double now = CurrentTime(); + if (mCurrentPlayRangeStart != now) { + // Don't round the left of the interval: it comes from script and needs + // to be exact. + ranges->Add(mCurrentPlayRangeStart, now); + } + } + + ranges->Normalize(); + return ranges.forget(); +} + +void HTMLMediaElement::Pause(ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p Pause() called by JS", this)); + if (mNetworkState == NETWORK_EMPTY) { + LOG(LogLevel::Debug, ("Loading due to Pause()")); + DoLoad(); + } + PauseInternal(); +} + +void HTMLMediaElement::PauseInternal() { + if (mDecoder && mNetworkState != NETWORK_EMPTY) { + mDecoder->Pause(); + } + bool oldPaused = mPaused; + mPaused = true; + // Step 1, + // https://html.spec.whatwg.org/multipage/media.html#internal-pause-steps + mCanAutoplayFlag = false; + // We changed mPaused and mCanAutoplayFlag which can affect + // AddRemoveSelfReference + AddRemoveSelfReference(); + UpdateSrcMediaStreamPlaying(); + if (mAudioChannelWrapper) { + mAudioChannelWrapper->NotifyPlayStateChanged(); + } + + // We don't need to resume media which is paused explicitly by user. + ClearResumeDelayedMediaPlaybackAgentIfNeeded(); + + if (!oldPaused) { + FireTimeUpdate(TimeupdateType::eMandatory); + DispatchAsyncEvent(u"pause"_ns); + AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_ABORT_ERR); + } +} + +void HTMLMediaElement::SetVolume(double aVolume, ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p SetVolume(%f) called by JS", this, aVolume)); + + if (aVolume < 0.0 || aVolume > 1.0) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + if (aVolume == mVolume) return; + + mVolume = aVolume; + + // Here we want just to update the volume. + SetVolumeInternal(); + + DispatchAsyncEvent(u"volumechange"_ns); + + // We allow inaudible autoplay. But changing our volume may make this + // media audible. So pause if we are no longer supposed to be autoplaying. + PauseIfShouldNotBePlaying(); +} + +void HTMLMediaElement::MozGetMetadata(JSContext* aCx, + JS::MutableHandle<JSObject*> aResult, + ErrorResult& aRv) { + if (mReadyState < HAVE_METADATA) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + JS::Rooted<JSObject*> tags(aCx, JS_NewPlainObject(aCx)); + if (!tags) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + if (mTags) { + for (const auto& entry : *mTags) { + nsString wideValue; + CopyUTF8toUTF16(entry.GetData(), wideValue); + JS::Rooted<JSString*> string(aCx, + JS_NewUCStringCopyZ(aCx, wideValue.Data())); + if (!string || !JS_DefineProperty(aCx, tags, entry.GetKey().Data(), + string, JSPROP_ENUMERATE)) { + NS_WARNING("couldn't create metadata object!"); + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } + } + + aResult.set(tags); +} + +void HTMLMediaElement::SetMutedInternal(uint32_t aMuted) { + uint32_t oldMuted = mMuted; + mMuted = aMuted; + + if (!!aMuted == !!oldMuted) { + return; + } + + SetVolumeInternal(); +} + +void HTMLMediaElement::PauseIfShouldNotBePlaying() { + if (GetPaused()) { + return; + } + if (!AllowedToPlay()) { + AUTOPLAY_LOG("pause because not allowed to play, element=%p", this); + ErrorResult rv; + Pause(rv); + } +} + +void HTMLMediaElement::SetVolumeInternal() { + float effectiveVolume = ComputedVolume(); + + if (mDecoder) { + mDecoder->SetVolume(effectiveVolume); + } else if (mMediaStreamRenderer) { + mMediaStreamRenderer->SetAudioOutputVolume(effectiveVolume); + } + + NotifyAudioPlaybackChanged( + AudioChannelService::AudibleChangedReasons::eVolumeChanged); +} + +void HTMLMediaElement::SetMuted(bool aMuted) { + LOG(LogLevel::Debug, ("%p SetMuted(%d) called by JS", this, aMuted)); + if (aMuted == Muted()) { + return; + } + + if (aMuted) { + SetMutedInternal(mMuted | MUTED_BY_CONTENT); + } else { + SetMutedInternal(mMuted & ~MUTED_BY_CONTENT); + } + + DispatchAsyncEvent(u"volumechange"_ns); + + // We allow inaudible autoplay. But changing our mute status may make this + // media audible. So pause if we are no longer supposed to be autoplaying. + PauseIfShouldNotBePlaying(); +} + +void HTMLMediaElement::GetAllEnabledMediaTracks( + nsTArray<RefPtr<MediaTrack>>& aTracks) { + if (AudioTrackList* tracks = AudioTracks()) { + for (size_t i = 0; i < tracks->Length(); ++i) { + AudioTrack* track = (*tracks)[i]; + if (track->Enabled()) { + aTracks.AppendElement(track); + } + } + } + if (IsVideo()) { + if (VideoTrackList* tracks = VideoTracks()) { + for (size_t i = 0; i < tracks->Length(); ++i) { + VideoTrack* track = (*tracks)[i]; + if (track->Selected()) { + aTracks.AppendElement(track); + } + } + } + } +} + +void HTMLMediaElement::SetCapturedOutputStreamsEnabled(bool aEnabled) { + for (const auto& entry : mOutputTrackSources.Values()) { + entry->SetEnabled(aEnabled); + } +} + +HTMLMediaElement::OutputMuteState HTMLMediaElement::OutputTracksMuted() { + return mPaused || mReadyState <= HAVE_CURRENT_DATA ? OutputMuteState::Muted + : OutputMuteState::Unmuted; +} + +void HTMLMediaElement::UpdateOutputTracksMuting() { + for (const auto& entry : mOutputTrackSources.Values()) { + entry->SetMutedByElement(OutputTracksMuted()); + } +} + +void HTMLMediaElement::AddOutputTrackSourceToOutputStream( + MediaElementTrackSource* aSource, OutputMediaStream& aOutputStream, + AddTrackMode aMode) { + if (aOutputStream.mStream == mSrcStream) { + // Cycle detected. This can happen since tracks are added async. + // We avoid forwarding it to the output here or we'd get into an infloop. + LOG(LogLevel::Warning, + ("NOT adding output track source %p to output stream " + "%p -- cycle detected", + aSource, aOutputStream.mStream.get())); + return; + } + + LOG(LogLevel::Debug, ("Adding output track source %p to output stream %p", + aSource, aOutputStream.mStream.get())); + + RefPtr<MediaStreamTrack> domTrack; + if (aSource->Track()->mType == MediaSegment::AUDIO) { + domTrack = new AudioStreamTrack( + aOutputStream.mStream->GetOwner(), aSource->Track(), aSource, + MediaStreamTrackState::Live, aSource->Muted()); + } else { + domTrack = new VideoStreamTrack( + aOutputStream.mStream->GetOwner(), aSource->Track(), aSource, + MediaStreamTrackState::Live, aSource->Muted()); + } + + aOutputStream.mLiveTracks.AppendElement(domTrack); + + switch (aMode) { + case AddTrackMode::ASYNC: + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod<StoreRefPtrPassByPtr<MediaStreamTrack>>( + "DOMMediaStream::AddTrackInternal", aOutputStream.mStream, + &DOMMediaStream::AddTrackInternal, domTrack)); + break; + case AddTrackMode::SYNC: + aOutputStream.mStream->AddTrackInternal(domTrack); + break; + default: + MOZ_CRASH("Unexpected mode"); + } + + LOG(LogLevel::Debug, + ("Created capture %s track %p", + domTrack->AsAudioStreamTrack() ? "audio" : "video", domTrack.get())); +} + +void HTMLMediaElement::UpdateOutputTrackSources() { + // This updates the track sources in mOutputTrackSources so they're in sync + // with the tracks being currently played, and state saying whether we should + // be capturing tracks. This method is long so here is a breakdown: + // - Figure out the tracks that should be captured + // - Diff those against currently captured tracks (mOutputTrackSources), into + // tracks-to-add, and tracks-to-remove + // - Remove the tracks in tracks-to-remove and dispatch "removetrack" and + // "ended" events for them + // - If playback has ended, or there is no longer a media provider object, + // remove any OutputMediaStreams that have the finish-when-ended flag set + // - Create track sources for, and add to OutputMediaStreams, the tracks in + // tracks-to-add + + const bool shouldHaveTrackSources = mTracksCaptured.Ref() && + !IsPlaybackEnded() && + mReadyState >= HAVE_METADATA; + + // Add track sources for all enabled/selected MediaTracks. + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return; + } + + if (mDecoder) { + if (!mTracksCaptured.Ref()) { + mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::None); + } else if (!AudioTracks() || !VideoTracks() || !shouldHaveTrackSources) { + // We've been unlinked, or tracks are not yet known. + mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Halt); + } else { + mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Capture, + mTracksCaptured.Ref().get()); + } + } + + // Start with all MediaTracks + AutoTArray<RefPtr<MediaTrack>, 4> mediaTracksToAdd; + if (shouldHaveTrackSources) { + GetAllEnabledMediaTracks(mediaTracksToAdd); + } + + // ...and all MediaElementTrackSources. + auto trackSourcesToRemove = + ToTArray<AutoTArray<nsString, 4>>(mOutputTrackSources.Keys()); + + // Then work out the differences. + mediaTracksToAdd.RemoveLastElements( + mediaTracksToAdd.end() - + std::remove_if(mediaTracksToAdd.begin(), mediaTracksToAdd.end(), + [this, &trackSourcesToRemove](const auto& track) { + const bool remove = + mOutputTrackSources.GetWeak(track->GetId()); + if (remove) { + trackSourcesToRemove.RemoveElement(track->GetId()); + } + return remove; + })); + + // First remove stale track sources. + for (const auto& id : trackSourcesToRemove) { + RefPtr<MediaElementTrackSource> source = mOutputTrackSources.GetWeak(id); + + LOG(LogLevel::Debug, ("Removing output track source %p for track %s", + source.get(), NS_ConvertUTF16toUTF8(id).get())); + + if (mDecoder) { + mDecoder->RemoveOutputTrack(source->Track()); + } + + // The source of this track just ended. Force-notify that it ended. + // If we bounce it to the MediaTrackGraph it might not be picked up, + // for instance if the MediaInputPort was destroyed in the same + // iteration as it was added. + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod("MediaElementTrackSource::OverrideEnded", source, + &MediaElementTrackSource::OverrideEnded)); + + // Remove the track from the MediaStream after it ended. + for (OutputMediaStream& ms : mOutputStreams) { + if (source->Track()->mType == MediaSegment::VIDEO && + ms.mCapturingAudioOnly) { + continue; + } + DebugOnly<size_t> length = ms.mLiveTracks.Length(); + ms.mLiveTracks.RemoveElementsBy( + [&](const RefPtr<MediaStreamTrack>& aTrack) { + if (&aTrack->GetSource() != source) { + return false; + } + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod<RefPtr<MediaStreamTrack>>( + "DOMMediaStream::RemoveTrackInternal", ms.mStream, + &DOMMediaStream::RemoveTrackInternal, aTrack)); + return true; + }); + MOZ_ASSERT(ms.mLiveTracks.Length() == length - 1); + } + + mOutputTrackSources.Remove(id); + } + + // Then update finish-when-ended output streams as needed. + for (size_t i = mOutputStreams.Length(); i-- > 0;) { + if (!mOutputStreams[i].mFinishWhenEnded) { + continue; + } + + if (!mOutputStreams[i].mFinishWhenEndedLoadingSrc && + !mOutputStreams[i].mFinishWhenEndedAttrStream && + !mOutputStreams[i].mFinishWhenEndedMediaSource) { + // This finish-when-ended stream has not seen any source loaded yet. + // Update the loading src if it's time. + if (!IsPlaybackEnded()) { + if (mLoadingSrc) { + mOutputStreams[i].mFinishWhenEndedLoadingSrc = mLoadingSrc; + } else if (mSrcAttrStream) { + mOutputStreams[i].mFinishWhenEndedAttrStream = mSrcAttrStream; + } else if (mSrcMediaSource) { + mOutputStreams[i].mFinishWhenEndedMediaSource = mSrcMediaSource; + } + } + continue; + } + + // Discard finish-when-ended output streams with a loading src set as + // needed. + if (!IsPlaybackEnded() && + mLoadingSrc == mOutputStreams[i].mFinishWhenEndedLoadingSrc) { + continue; + } + if (!IsPlaybackEnded() && + mSrcAttrStream == mOutputStreams[i].mFinishWhenEndedAttrStream) { + continue; + } + if (!IsPlaybackEnded() && + mSrcMediaSource == mOutputStreams[i].mFinishWhenEndedMediaSource) { + continue; + } + LOG(LogLevel::Debug, + ("Playback ended or source changed. Discarding stream %p", + mOutputStreams[i].mStream.get())); + mOutputStreams.RemoveElementAt(i); + if (mOutputStreams.IsEmpty()) { + mTracksCaptured = nullptr; + // mTracksCaptured is one of the Watchables triggering this method. + // Unsetting it here means we'll run through this method again very soon. + return; + } + } + + // Finally add new MediaTracks. + for (const auto& mediaTrack : mediaTracksToAdd) { + nsAutoString id; + mediaTrack->GetId(id); + + MediaSegment::Type type; + if (mediaTrack->AsAudioTrack()) { + type = MediaSegment::AUDIO; + } else if (mediaTrack->AsVideoTrack()) { + type = MediaSegment::VIDEO; + } else { + MOZ_CRASH("Unknown track type"); + } + + RefPtr<ProcessedMediaTrack> track; + RefPtr<MediaElementTrackSource> source; + if (mDecoder) { + track = mTracksCaptured.Ref()->mTrack->Graph()->CreateForwardedInputTrack( + type); + RefPtr<nsIPrincipal> principal = GetCurrentPrincipal(); + if (!principal || IsCORSSameOrigin()) { + principal = NodePrincipal(); + } + source = MakeAndAddRef<MediaElementTrackSource>( + track, principal, OutputTracksMuted(), + type == MediaSegment::VIDEO + ? HTMLVideoElement::FromNode(this)->HasAlpha() + : false); + mDecoder->AddOutputTrack(track); + } else if (mSrcStream) { + MediaStreamTrack* inputTrack; + if (AudioTrack* t = mediaTrack->AsAudioTrack()) { + inputTrack = t->GetAudioStreamTrack(); + } else if (VideoTrack* t = mediaTrack->AsVideoTrack()) { + inputTrack = t->GetVideoStreamTrack(); + } else { + MOZ_CRASH("Unknown track type"); + } + MOZ_ASSERT(inputTrack); + if (!inputTrack) { + NS_ERROR("Input track not found in source stream"); + return; + } + MOZ_DIAGNOSTIC_ASSERT(!inputTrack->Ended()); + + track = inputTrack->Graph()->CreateForwardedInputTrack(type); + RefPtr<MediaInputPort> port = inputTrack->ForwardTrackContentsTo(track); + source = MakeAndAddRef<MediaElementTrackSource>( + inputTrack, &inputTrack->GetSource(), track, port, + OutputTracksMuted()); + + // Track is muted initially, so we don't leak data if it's added while + // paused and an MTG iteration passes before the mute comes into effect. + source->SetEnabled(mSrcStreamIsPlaying); + } else { + MOZ_CRASH("Unknown source"); + } + + LOG(LogLevel::Debug, ("Adding output track source %p for track %s", + source.get(), NS_ConvertUTF16toUTF8(id).get())); + + track->QueueSetAutoend(false); + MOZ_DIAGNOSTIC_ASSERT(!mOutputTrackSources.Contains(id)); + mOutputTrackSources.InsertOrUpdate(id, RefPtr{source}); + + // Add the new track source to any existing output streams + for (OutputMediaStream& ms : mOutputStreams) { + if (source->Track()->mType == MediaSegment::VIDEO && + ms.mCapturingAudioOnly) { + // If the output stream is for audio only we ignore video sources. + continue; + } + AddOutputTrackSourceToOutputStream(source, ms); + } + } +} + +bool HTMLMediaElement::CanBeCaptured(StreamCaptureType aCaptureType) { + // Don't bother capturing when the document has gone away + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return false; + } + + // Prevent capturing restricted video + if (aCaptureType == StreamCaptureType::CAPTURE_ALL_TRACKS && + ContainsRestrictedContent()) { + return false; + } + return true; +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::CaptureStreamInternal( + StreamCaptureBehavior aFinishBehavior, StreamCaptureType aStreamCaptureType, + MediaTrackGraph* aGraph) { + MOZ_ASSERT(CanBeCaptured(aStreamCaptureType)); + + LogVisibility(CallerAPI::CAPTURE_STREAM); + MarkAsTainted(); + + if (mTracksCaptured.Ref()) { + // Already have an output stream. Check whether the graph rate matches if + // specified. + if (aGraph && aGraph != mTracksCaptured.Ref()->mTrack->Graph()) { + return nullptr; + } + } else { + // This is the first output stream, or there are no tracks. If the former, + // start capturing all tracks. If the latter, they will be added later. + MediaTrackGraph* graph = aGraph; + if (!graph) { + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return nullptr; + } + + MediaTrackGraph::GraphDriverType graphDriverType = + HasAudio() ? MediaTrackGraph::AUDIO_THREAD_DRIVER + : MediaTrackGraph::SYSTEM_THREAD_DRIVER; + graph = MediaTrackGraph::GetInstance( + graphDriverType, window, MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + MediaTrackGraph::DEFAULT_OUTPUT_DEVICE); + } + mTracksCaptured = MakeRefPtr<SharedDummyTrack>( + graph->CreateSourceTrack(MediaSegment::AUDIO)); + UpdateOutputTrackSources(); + } + + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + OutputMediaStream* out = mOutputStreams.EmplaceBack( + MakeRefPtr<DOMMediaStream>(window), + aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO, + aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED); + + if (aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED && + !mOutputTrackSources.IsEmpty()) { + // This output stream won't receive any more tracks when playback of the + // current src of this media element ends, or when the src of this media + // element changes. If we're currently playing something (i.e., if there are + // tracks currently captured), set the current src on the output stream so + // this can be tracked. If we're not playing anything, + // UpdateOutputTrackSources will set the current src when it becomes + // available later. + if (mLoadingSrc) { + out->mFinishWhenEndedLoadingSrc = mLoadingSrc; + } + if (mSrcAttrStream) { + out->mFinishWhenEndedAttrStream = mSrcAttrStream; + } + if (mSrcMediaSource) { + out->mFinishWhenEndedMediaSource = mSrcMediaSource; + } + MOZ_ASSERT(out->mFinishWhenEndedLoadingSrc || + out->mFinishWhenEndedAttrStream || + out->mFinishWhenEndedMediaSource); + } + + if (aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO) { + if (mSrcStream) { + // We don't support applying volume and mute to the captured stream, when + // capturing a MediaStream. + ReportToConsole(nsIScriptError::errorFlag, + "MediaElementAudioCaptureOfMediaStreamError"); + } + + // mAudioCaptured tells the user that the audio played by this media element + // is being routed to the captureStreams *instead* of being played to + // speakers. + mAudioCaptured = true; + } + + for (const RefPtr<MediaElementTrackSource>& source : + mOutputTrackSources.Values()) { + if (source->Track()->mType == MediaSegment::VIDEO) { + // Only add video tracks if we're a video element and the output stream + // wants video. + if (!IsVideo()) { + continue; + } + if (out->mCapturingAudioOnly) { + continue; + } + } + AddOutputTrackSourceToOutputStream(source, *out, AddTrackMode::SYNC); + } + + return do_AddRef(out->mStream); +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::CaptureAudio( + ErrorResult& aRv, MediaTrackGraph* aGraph) { + MOZ_RELEASE_ASSERT(aGraph); + + if (!CanBeCaptured(StreamCaptureType::CAPTURE_AUDIO)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED, + StreamCaptureType::CAPTURE_AUDIO, aGraph); + if (!stream) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return stream.forget(); +} + +RefPtr<GenericNonExclusivePromise> HTMLMediaElement::GetAllowedToPlayPromise() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mOutputStreams.IsEmpty(), + "This method should only be called during stream capturing!"); + if (AllowedToPlay()) { + AUTOPLAY_LOG("MediaElement %p has allowed to play, resolve promise", this); + return GenericNonExclusivePromise::CreateAndResolve(true, __func__); + } + AUTOPLAY_LOG("create allow-to-play promise for MediaElement %p", this); + return mAllowedToPlayPromise.Ensure(__func__); +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::MozCaptureStream( + ErrorResult& aRv) { + if (!CanBeCaptured(StreamCaptureType::CAPTURE_ALL_TRACKS)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED, + StreamCaptureType::CAPTURE_ALL_TRACKS, nullptr); + if (!stream) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return stream.forget(); +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::MozCaptureStreamUntilEnded( + ErrorResult& aRv) { + if (!CanBeCaptured(StreamCaptureType::CAPTURE_ALL_TRACKS)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::FINISH_WHEN_ENDED, + StreamCaptureType::CAPTURE_ALL_TRACKS, nullptr); + if (!stream) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return stream.forget(); +} + +class MediaElementSetForURI : public nsURIHashKey { + public: + explicit MediaElementSetForURI(const nsIURI* aKey) : nsURIHashKey(aKey) {} + MediaElementSetForURI(MediaElementSetForURI&& aOther) noexcept + : nsURIHashKey(std::move(aOther)), + mElements(std::move(aOther.mElements)) {} + nsTArray<HTMLMediaElement*> mElements; +}; + +using MediaElementURITable = nsTHashtable<MediaElementSetForURI>; +// Elements in this table must have non-null mDecoder and mLoadingSrc, and those +// can't change while the element is in the table. The table is keyed by +// the element's mLoadingSrc. Each entry has a list of all elements with the +// same mLoadingSrc. +static MediaElementURITable* gElementTable; + +#ifdef DEBUG +static bool URISafeEquals(nsIURI* a1, nsIURI* a2) { + if (!a1 || !a2) { + // Consider two empty URIs *not* equal! + return false; + } + bool equal = false; + nsresult rv = a1->Equals(a2, &equal); + return NS_SUCCEEDED(rv) && equal; +} +// Returns the number of times aElement appears in the media element table +// for aURI. If this returns other than 0 or 1, there's a bug somewhere! +static unsigned MediaElementTableCount(HTMLMediaElement* aElement, + nsIURI* aURI) { + if (!gElementTable || !aElement) { + return 0; + } + uint32_t uriCount = 0; + uint32_t otherCount = 0; + for (const auto& entry : *gElementTable) { + uint32_t count = 0; + for (const auto& elem : entry.mElements) { + if (elem == aElement) { + count++; + } + } + if (URISafeEquals(aURI, entry.GetKey())) { + uriCount = count; + } else { + otherCount += count; + } + } + NS_ASSERTION(otherCount == 0, "Should not have entries for unknown URIs"); + return uriCount; +} +#endif + +void HTMLMediaElement::AddMediaElementToURITable() { + NS_ASSERTION(mDecoder, "Call this only with decoder Load called"); + NS_ASSERTION( + MediaElementTableCount(this, mLoadingSrc) == 0, + "Should not have entry for element in element table before addition"); + if (!gElementTable) { + gElementTable = new MediaElementURITable(); + } + MediaElementSetForURI* entry = gElementTable->PutEntry(mLoadingSrc); + entry->mElements.AppendElement(this); + NS_ASSERTION( + MediaElementTableCount(this, mLoadingSrc) == 1, + "Should have a single entry for element in element table after addition"); +} + +void HTMLMediaElement::RemoveMediaElementFromURITable() { + if (!mDecoder || !mLoadingSrc || !gElementTable) { + return; + } + MediaElementSetForURI* entry = gElementTable->GetEntry(mLoadingSrc); + if (!entry) { + return; + } + entry->mElements.RemoveElement(this); + if (entry->mElements.IsEmpty()) { + gElementTable->RemoveEntry(entry); + if (gElementTable->Count() == 0) { + delete gElementTable; + gElementTable = nullptr; + } + } + NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0, + "After remove, should no longer have an entry in element table"); +} + +HTMLMediaElement* HTMLMediaElement::LookupMediaElementURITable(nsIURI* aURI) { + if (!gElementTable) { + return nullptr; + } + MediaElementSetForURI* entry = gElementTable->GetEntry(aURI); + if (!entry) { + return nullptr; + } + for (uint32_t i = 0; i < entry->mElements.Length(); ++i) { + HTMLMediaElement* elem = entry->mElements[i]; + bool equal; + // Look for elements that have the same principal and CORS mode. + // Ditto for anything else that could cause us to send different headers. + if (NS_SUCCEEDED(elem->NodePrincipal()->Equals(NodePrincipal(), &equal)) && + equal && elem->mCORSMode == mCORSMode) { + // See SetupDecoder() below. We only add a element to the table when + // mDecoder is a ChannelMediaDecoder. + auto* decoder = static_cast<ChannelMediaDecoder*>(elem->mDecoder.get()); + NS_ASSERTION(decoder, "Decoder gone"); + if (decoder->CanClone()) { + return elem; + } + } + } + return nullptr; +} + +class HTMLMediaElement::ShutdownObserver : public nsIObserver { + enum class Phase : int8_t { Init, Subscribed, Unsubscribed }; + + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD Observe(nsISupports*, const char* aTopic, + const char16_t*) override { + if (mPhase != Phase::Subscribed) { + // Bail out if we are not subscribed for this might be called even after + // |nsContentUtils::UnregisterShutdownObserver(this)|. + return NS_OK; + } + MOZ_DIAGNOSTIC_ASSERT(mWeak); + if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + mWeak->NotifyShutdownEvent(); + } + return NS_OK; + } + void Subscribe(HTMLMediaElement* aPtr) { + MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Init); + MOZ_DIAGNOSTIC_ASSERT(!mWeak); + mWeak = aPtr; + nsContentUtils::RegisterShutdownObserver(this); + mPhase = Phase::Subscribed; + } + void Unsubscribe() { + MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Subscribed); + MOZ_DIAGNOSTIC_ASSERT(mWeak); + MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, + "ReleaseMediaElement should have been called first"); + mWeak = nullptr; + nsContentUtils::UnregisterShutdownObserver(this); + mPhase = Phase::Unsubscribed; + } + void AddRefMediaElement() { + MOZ_DIAGNOSTIC_ASSERT(mWeak); + MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, "Should only ever AddRef once"); + mWeak->AddRef(); + mAddRefed = true; + } + void ReleaseMediaElement() { + MOZ_DIAGNOSTIC_ASSERT(mWeak); + MOZ_DIAGNOSTIC_ASSERT(mAddRefed, "Should only release after AddRef"); + mWeak->Release(); + mAddRefed = false; + } + + private: + virtual ~ShutdownObserver() { + MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Unsubscribed); + MOZ_DIAGNOSTIC_ASSERT(!mWeak); + MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, + "ReleaseMediaElement should have been called first"); + } + // Guaranteed to be valid by HTMLMediaElement. + HTMLMediaElement* mWeak = nullptr; + Phase mPhase = Phase::Init; + bool mAddRefed = false; +}; + +NS_IMPL_ISUPPORTS(HTMLMediaElement::ShutdownObserver, nsIObserver) + +class HTMLMediaElement::TitleChangeObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + + explicit TitleChangeObserver(HTMLMediaElement* aElement) + : mElement(aElement) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aElement); + } + + NS_IMETHOD Observe(nsISupports*, const char* aTopic, + const char16_t*) override { + if (mElement) { + mElement->UpdateStreamName(); + } + + return NS_OK; + } + + void Subscribe() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "document-title-changed", false); + } + } + + void Unsubscribe() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, "document-title-changed"); + } + } + + private: + ~TitleChangeObserver() = default; + + WeakPtr<HTMLMediaElement> mElement; +}; + +NS_IMPL_ISUPPORTS(HTMLMediaElement::TitleChangeObserver, nsIObserver) + +HTMLMediaElement::HTMLMediaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mWatchManager(this, AbstractThread::MainThread()), + mShutdownObserver(new ShutdownObserver), + mTitleChangeObserver(new TitleChangeObserver(this)), + mEventBlocker(new EventBlocker(this)), + mPlayed(new TimeRanges(ToSupports(OwnerDoc()))), + mTracksCaptured(nullptr, "HTMLMediaElement::mTracksCaptured"), + mErrorSink(new ErrorSink(this)), + mAudioChannelWrapper(new AudioChannelAgentCallback(this)), + mSink(std::pair(nsString(), RefPtr<AudioDeviceInfo>())), + mShowPoster(IsVideo()), + mMediaControlKeyListener(new MediaControlKeyListener(this)) { + MOZ_ASSERT(GetMainThreadSerialEventTarget()); + // Please don't add anything to this constructor or the initialization + // list that can cause AddRef to be called. This prevents subclasses + // from overriding AddRef in a way that works with our refcount + // logging mechanisms. Put these things inside of the ::Init method + // instead. +} + +void HTMLMediaElement::Init() { + MOZ_ASSERT(mRefCnt == 0 && !mRefCnt.IsPurple(), + "HTMLMediaElement::Init called when AddRef has been called " + "at least once already, probably in the constructor. Please " + "see the documentation in the HTMLMediaElement constructor."); + MOZ_ASSERT(!mRefCnt.IsPurple()); + + mAudioTrackList = new AudioTrackList(OwnerDoc()->GetParentObject(), this); + mVideoTrackList = new VideoTrackList(OwnerDoc()->GetParentObject(), this); + + DecoderDoctorLogger::LogConstruction(this); + + mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateWakeLock); + mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateOutputTracksMuting); + mWatchManager.Watch( + mPaused, &HTMLMediaElement::NotifyMediaControlPlaybackStateChanged); + mWatchManager.Watch(mReadyState, &HTMLMediaElement::UpdateOutputTracksMuting); + + mWatchManager.Watch(mTracksCaptured, + &HTMLMediaElement::UpdateOutputTrackSources); + mWatchManager.Watch(mReadyState, &HTMLMediaElement::UpdateOutputTrackSources); + + mWatchManager.Watch(mDownloadSuspendedByCache, + &HTMLMediaElement::UpdateReadyStateInternal); + mWatchManager.Watch(mFirstFrameLoaded, + &HTMLMediaElement::UpdateReadyStateInternal); + mWatchManager.Watch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateReadyStateInternal); + + ErrorResult rv; + + double defaultVolume = Preferences::GetFloat("media.default_volume", 1.0); + SetVolume(defaultVolume, rv); + + RegisterActivityObserver(); + NotifyOwnerDocumentActivityChanged(); + + // We initialize the MediaShutdownManager as the HTMLMediaElement is always + // constructed on the main thread, and not during stable state. + // (MediaShutdownManager make use of nsIAsyncShutdownClient which is written + // in JS) + MediaShutdownManager::InitStatics(); + +#if defined(MOZ_WIDGET_ANDROID) + GVAutoplayPermissionRequestor::AskForPermissionIfNeeded( + OwnerDoc()->GetInnerWindow()); +#endif + + OwnerDoc()->SetDocTreeHadMedia(); + mShutdownObserver->Subscribe(this); + mInitialized = true; +} + +HTMLMediaElement::~HTMLMediaElement() { + MOZ_ASSERT(mInitialized, + "HTMLMediaElement must be initialized before it is destroyed."); + NS_ASSERTION( + !mHasSelfReference, + "How can we be destroyed if we're still holding a self reference?"); + + mWatchManager.Shutdown(); + + mShutdownObserver->Unsubscribe(); + + mTitleChangeObserver->Unsubscribe(); + + if (mVideoFrameContainer) { + mVideoFrameContainer->ForgetElement(); + } + UnregisterActivityObserver(); + + mSetCDMRequest.DisconnectIfExists(); + mAllowedToPlayPromise.RejectIfExists(NS_ERROR_FAILURE, __func__); + + if (mDecoder) { + ShutdownDecoder(); + } + if (mProgressTimer) { + StopProgress(); + } + if (mSrcStream) { + EndSrcMediaStreamPlayback(); + } + + NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0, + "Destroyed media element should no longer be in element table"); + + if (mChannelLoader) { + mChannelLoader->Cancel(); + } + + if (mAudioChannelWrapper) { + mAudioChannelWrapper->Shutdown(); + mAudioChannelWrapper = nullptr; + } + + if (mResumeDelayedPlaybackAgent) { + mResumePlaybackRequest.DisconnectIfExists(); + mResumeDelayedPlaybackAgent = nullptr; + } + + mMediaControlKeyListener->StopIfNeeded(); + mMediaControlKeyListener = nullptr; + + WakeLockRelease(); + + DecoderDoctorLogger::LogDestruction(this); +} + +void HTMLMediaElement::StopSuspendingAfterFirstFrame() { + mAllowSuspendAfterFirstFrame = false; + if (!mSuspendedAfterFirstFrame) return; + mSuspendedAfterFirstFrame = false; + if (mDecoder) { + mDecoder->Resume(); + } +} + +void HTMLMediaElement::SetPlayedOrSeeked(bool aValue) { + if (aValue == mHasPlayedOrSeeked) { + return; + } + + mHasPlayedOrSeeked = aValue; + + // Force a reflow so that the poster frame hides or shows immediately. + nsIFrame* frame = GetPrimaryFrame(); + if (!frame) { + return; + } + frame->PresShell()->FrameNeedsReflow(frame, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); +} + +void HTMLMediaElement::NotifyXPCOMShutdown() { ShutdownDecoder(); } + +already_AddRefed<Promise> HTMLMediaElement::Play(ErrorResult& aRv) { + LOG(LogLevel::Debug, + ("%p Play() called by JS readyState=%d", this, mReadyState.Ref())); + + // 4.8.12.8 + // When the play() method on a media element is invoked, the user agent must + // run the following steps. + + RefPtr<PlayPromise> promise = CreatePlayPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // 4.8.12.8 - Step 1: + // If the media element is not allowed to play, return a promise rejected + // with a "NotAllowedError" DOMException and abort these steps. + // NOTE: we may require requesting permission from the user, so we do the + // "not allowed" check below. + + // 4.8.12.8 - Step 2: + // If the media element's error attribute is not null and its code + // attribute has the value MEDIA_ERR_SRC_NOT_SUPPORTED, return a promise + // rejected with a "NotSupportedError" DOMException and abort these steps. + if (GetError() && GetError()->Code() == MEDIA_ERR_SRC_NOT_SUPPORTED) { + LOG(LogLevel::Debug, + ("%p Play() promise rejected because source not supported.", this)); + promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR); + return promise.forget(); + } + + // 4.8.12.8 - Step 3: + // Let promise be a new promise and append promise to the list of pending + // play promises. + // Note: Promise appended to list of pending promises as needed below. + + if (ShouldBeSuspendedByInactiveDocShell()) { + LOG(LogLevel::Debug, ("%p no allow to play by the docShell for now", this)); + mPendingPlayPromises.AppendElement(promise); + return promise.forget(); + } + + // We may delay starting playback of a media resource for an unvisited tab + // until it's going to foreground or being resumed by the play tab icon. + if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) { + CreateResumeDelayedMediaPlaybackAgentIfNeeded(); + LOG(LogLevel::Debug, ("%p delay Play() call", this)); + MaybeDoLoad(); + // When play is delayed, save a reference to the promise, and return it. + // The promise will be resolved when we resume play by either the tab is + // brought to the foreground, or the audio tab indicator is clicked. + mPendingPlayPromises.AppendElement(promise); + return promise.forget(); + } + + const bool handlingUserInput = UserActivation::IsHandlingUserInput(); + mPendingPlayPromises.AppendElement(promise); + + if (AllowedToPlay()) { + AUTOPLAY_LOG("allow MediaElement %p to play", this); + mAllowedToPlayPromise.ResolveIfExists(true, __func__); + PlayInternal(handlingUserInput); + UpdateCustomPolicyAfterPlayed(); + } else { + AUTOPLAY_LOG("reject MediaElement %p to play", this); + AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR); + } + return promise.forget(); +} + +void HTMLMediaElement::DispatchEventsWhenPlayWasNotAllowed() { + if (StaticPrefs::media_autoplay_block_event_enabled()) { + DispatchAsyncEvent(u"blocked"_ns); + } + DispatchBlockEventForVideoControl(); + if (!mHasEverBeenBlockedForAutoplay) { + MaybeNotifyAutoplayBlocked(); + ReportToConsole(nsIScriptError::warningFlag, "BlockAutoplayError"); + mHasEverBeenBlockedForAutoplay = true; + } +} + +void HTMLMediaElement::MaybeNotifyAutoplayBlocked() { + // This event is used to notify front-end side that we've blocked autoplay, + // so front-end side should show blocking icon as well. + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(OwnerDoc(), u"GloballyAutoplayBlocked"_ns, + CanBubble::eYes, ChromeOnlyDispatch::eYes); + asyncDispatcher->PostDOMEvent(); +} + +void HTMLMediaElement::DispatchBlockEventForVideoControl() { +#if defined(MOZ_WIDGET_ANDROID) + nsVideoFrame* videoFrame = do_QueryFrame(GetPrimaryFrame()); + if (!videoFrame || !videoFrame->GetVideoControls()) { + return; + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + videoFrame->GetVideoControls(), u"MozNoControlsBlockedVideo"_ns, + CanBubble::eYes); + asyncDispatcher->PostDOMEvent(); +#endif +} + +void HTMLMediaElement::PlayInternal(bool aHandlingUserInput) { + if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE) { + // The media load algorithm will be initiated by a user interaction. + // We want to boost the channel priority for better responsiveness. + // Note this must be done before UpdatePreloadAction() which will + // update |mPreloadAction|. + mUseUrgentStartForChannel = true; + } + + StopSuspendingAfterFirstFrame(); + SetPlayedOrSeeked(true); + + // 4.8.12.8 - Step 4: + // If the media element's networkState attribute has the value NETWORK_EMPTY, + // invoke the media element's resource selection algorithm. + MaybeDoLoad(); + if (mSuspendedForPreloadNone) { + ResumeLoad(PRELOAD_ENOUGH); + } + + // 4.8.12.8 - Step 5: + // If the playback has ended and the direction of playback is forwards, + // seek to the earliest possible position of the media resource. + + // Even if we just did Load() or ResumeLoad(), we could already have a decoder + // here if we managed to clone an existing decoder. + if (mDecoder) { + if (mDecoder->IsEnded()) { + SetCurrentTime(0); + } + if (!mSuspendedByInactiveDocOrDocshell) { + mDecoder->Play(); + } + } + + if (mCurrentPlayRangeStart == -1.0) { + mCurrentPlayRangeStart = CurrentTime(); + } + + const bool oldPaused = mPaused; + mPaused = false; + // Step 5, + // https://html.spec.whatwg.org/multipage/media.html#internal-play-steps + mCanAutoplayFlag = false; + + // We changed mPaused and mCanAutoplayFlag which can affect + // AddRemoveSelfReference and our preload status. + AddRemoveSelfReference(); + UpdatePreloadAction(); + UpdateSrcMediaStreamPlaying(); + StartMediaControlKeyListenerIfNeeded(); + + // Once play() has been called in a user generated event handler, + // it is allowed to autoplay. Note: we can reach here when not in + // a user generated event handler if our readyState has not yet + // reached HAVE_METADATA. + mIsBlessed |= aHandlingUserInput; + + // TODO: If the playback has ended, then the user agent must set + // seek to the effective start. + + // 4.8.12.8 - Step 6: + // If the media element's paused attribute is true, run the following steps: + if (oldPaused) { + // 6.1. Change the value of paused to false. (Already done.) + // This step is uplifted because the "block-media-playback" feature needs + // the mPaused to be false before UpdateAudioChannelPlayingState() being + // called. + + // 6.2. If the show poster flag is true, set the element's show poster flag + // to false and run the time marches on steps. + if (mShowPoster) { + mShowPoster = false; + if (mTextTrackManager) { + mTextTrackManager->TimeMarchesOn(); + } + } + + // 6.3. Queue a task to fire a simple event named play at the element. + DispatchAsyncEvent(u"play"_ns); + + // 6.4. If the media element's readyState attribute has the value + // HAVE_NOTHING, HAVE_METADATA, or HAVE_CURRENT_DATA, queue a task to + // fire a simple event named waiting at the element. + // Otherwise, the media element's readyState attribute has the value + // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA: notify about playing for the + // element. + switch (mReadyState) { + case HAVE_NOTHING: + DispatchAsyncEvent(u"waiting"_ns); + break; + case HAVE_METADATA: + case HAVE_CURRENT_DATA: + DispatchAsyncEvent(u"waiting"_ns); + break; + case HAVE_FUTURE_DATA: + case HAVE_ENOUGH_DATA: + NotifyAboutPlaying(); + break; + } + } else if (mReadyState >= HAVE_FUTURE_DATA) { + // 7. Otherwise, if the media element's readyState attribute has the value + // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and + // queue a task to resolve pending play promises with the result. + AsyncResolvePendingPlayPromises(); + } + + // 8. Set the media element's autoplaying flag to false. (Already done.) + + // 9. Return promise. + // (Done in caller.) +} + +void HTMLMediaElement::MaybeDoLoad() { + if (mNetworkState == NETWORK_EMPTY) { + DoLoad(); + } +} + +void HTMLMediaElement::UpdateWakeLock() { + MOZ_ASSERT(NS_IsMainThread()); + // Ensure we have a wake lock if we're playing audibly. This ensures the + // device doesn't sleep while playing. + bool playing = !mPaused; + bool isAudible = Volume() > 0.0 && !mMuted && mIsAudioTrackAudible; + // WakeLock when playing audible media. + if (playing && isAudible) { + CreateAudioWakeLockIfNeeded(); + } else { + ReleaseAudioWakeLockIfExists(); + } +} + +void HTMLMediaElement::CreateAudioWakeLockIfNeeded() { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return; + } + if (!mWakeLock) { + RefPtr<power::PowerManagerService> pmService = + power::PowerManagerService::GetInstance(); + NS_ENSURE_TRUE_VOID(pmService); + + ErrorResult rv; + mWakeLock = pmService->NewWakeLock(u"audio-playing"_ns, + OwnerDoc()->GetInnerWindow(), rv); + } +} + +void HTMLMediaElement::ReleaseAudioWakeLockIfExists() { + if (mWakeLock) { + ErrorResult rv; + mWakeLock->Unlock(rv); + rv.SuppressException(); + mWakeLock = nullptr; + } +} + +void HTMLMediaElement::WakeLockRelease() { ReleaseAudioWakeLockIfExists(); } + +void HTMLMediaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + if (!this->Controls() || !aVisitor.mEvent->mFlags.mIsTrusted) { + nsGenericHTMLElement::GetEventTargetParent(aVisitor); + return; + } + + // We will need to trap pointer, touch, and mouse events within the media + // element, allowing media control exclusive consumption on these events, + // and preventing the content from handling them. + switch (aVisitor.mEvent->mMessage) { + case ePointerDown: + case ePointerUp: + case eTouchEnd: + // Always prevent touchmove captured in video element from being handled by + // content, since we always do that for touchstart. + case eTouchMove: + case eTouchStart: + case eMouseClick: + case eMouseDoubleClick: + case eMouseDown: + case eMouseUp: + aVisitor.mCanHandle = false; + return; + + // The *move events however are only comsumed when the range input is being + // dragged. + case ePointerMove: + case eMouseMove: { + nsINode* node = + nsINode::FromEventTargetOrNull(aVisitor.mEvent->mOriginalTarget); + if (MOZ_UNLIKELY(!node)) { + return; + } + HTMLInputElement* el = nullptr; + if (node->ChromeOnlyAccess()) { + if (node->IsHTMLElement(nsGkAtoms::input)) { + // The node is a <input type="range"> + el = static_cast<HTMLInputElement*>(node); + } else if (node->GetParentNode() && + node->GetParentNode()->IsHTMLElement(nsGkAtoms::input)) { + // The node is a child of <input type="range"> + el = static_cast<HTMLInputElement*>(node->GetParentNode()); + } + } + if (el && el->IsDraggingRange()) { + aVisitor.mCanHandle = false; + return; + } + nsGenericHTMLElement::GetEventTargetParent(aVisitor); + return; + } + default: + nsGenericHTMLElement::GetEventTargetParent(aVisitor); + return; + } +} + +bool HTMLMediaElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + // Mappings from 'preload' attribute strings to an enumeration. + static const nsAttrValue::EnumTable kPreloadTable[] = { + {"", HTMLMediaElement::PRELOAD_ATTR_EMPTY}, + {"none", HTMLMediaElement::PRELOAD_ATTR_NONE}, + {"metadata", HTMLMediaElement::PRELOAD_ATTR_METADATA}, + {"auto", HTMLMediaElement::PRELOAD_ATTR_AUTO}, + {nullptr, 0}}; + + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::crossorigin) { + ParseCORSValue(aValue, aResult); + return true; + } + if (aAttribute == nsGkAtoms::preload) { + return aResult.ParseEnumValue(aValue, kPreloadTable, false); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLMediaElement::DoneCreatingElement() { + if (HasAttr(nsGkAtoms::muted)) { + mMuted |= MUTED_BY_CONTENT; + } +} + +bool HTMLMediaElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable, + aTabIndex)) { + return true; + } + + *aIsFocusable = true; + return false; +} + +int32_t HTMLMediaElement::TabIndexDefault() { return 0; } + +void HTMLMediaElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::src) { + mSrcMediaSource = nullptr; + mSrcAttrTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->GetStringValue() : EmptyString(), + aMaybeScriptedPrincipal); + if (aValue) { + nsString srcStr = aValue->GetStringValue(); + nsCOMPtr<nsIURI> uri; + NewURIFromString(srcStr, getter_AddRefs(uri)); + if (uri && IsMediaSourceURI(uri)) { + nsresult rv = NS_GetSourceForMediaSourceURI( + uri, getter_AddRefs(mSrcMediaSource)); + if (NS_FAILED(rv)) { + nsAutoString spec; + GetCurrentSrc(spec); + AutoTArray<nsString, 1> params = {spec}; + ReportLoadError("MediaLoadInvalidURI", params); + } + } + } + } else if (aName == nsGkAtoms::autoplay) { + if (aNotify) { + if (aValue) { + StopSuspendingAfterFirstFrame(); + CheckAutoplayDataReady(); + } + // This attribute can affect AddRemoveSelfReference + AddRemoveSelfReference(); + UpdatePreloadAction(); + } + } else if (aName == nsGkAtoms::preload) { + UpdatePreloadAction(); + } else if (aName == nsGkAtoms::loop) { + if (mDecoder) { + mDecoder->SetLooping(!!aValue); + } + } else if (aName == nsGkAtoms::controls && IsInComposedDoc()) { + NotifyUAWidgetSetupOrChange(); + } + } + + // Since AfterMaybeChangeAttr may call DoLoad, make sure that it is called + // *after* any possible changes to mSrcMediaSource. + if (aValue) { + AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify); + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLMediaElement::OnAttrSetButNotChanged(int32_t aNamespaceID, + nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + + return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName, + aValue, aNotify); +} + +void HTMLMediaElement::AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::src) { + DoLoad(); + } + } +} + +nsresult HTMLMediaElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + + if (IsInComposedDoc()) { + // Construct Shadow Root so web content can be hidden in the DOM. + AttachAndSetUAShadowRoot(); + + // The preload action depends on the value of the autoplay attribute. + // It's value may have changed, so update it. + UpdatePreloadAction(); + } + + NotifyDecoderActivityChanges(); + mMediaControlKeyListener->UpdateOwnerBrowsingContextIfNeeded(); + return rv; +} + +void HTMLMediaElement::UnbindFromTree(bool aNullParent) { + mVisibilityState = Visibility::Untracked; + + if (IsInComposedDoc()) { + NotifyUAWidgetTeardown(); + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + MOZ_ASSERT(IsActuallyInvisible()); + NotifyDecoderActivityChanges(); + + // https://html.spec.whatwg.org/#playing-the-media-resource:remove-an-element-from-a-document + // + // Dispatch a task to run once we're in a stable state which ensures we're + // paused if we're no longer in a document. Note that we need to dispatch this + // even if there are other tasks in flight for this because these can be + // cancelled if there's a new load. + // + // FIXME(emilio): Per that spec section, we should only do this if we used to + // be connected, though other browsers match our current behavior... + // + // Also, https://github.com/whatwg/html/issues/4928 + nsCOMPtr<nsIRunnable> task = + NS_NewRunnableFunction("dom::HTMLMediaElement::UnbindFromTree", + [self = RefPtr<HTMLMediaElement>(this)]() { + if (!self->IsInComposedDoc()) { + self->PauseInternal(); + self->mMediaControlKeyListener->StopIfNeeded(); + } + }); + RunInStableState(task); +} + +/* static */ +CanPlayStatus HTMLMediaElement::GetCanPlay( + const nsAString& aType, DecoderDoctorDiagnostics* aDiagnostics) { + Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType); + if (!containerType) { + return CANPLAY_NO; + } + CanPlayStatus status = + DecoderTraits::CanHandleContainerType(*containerType, aDiagnostics); + if (status == CANPLAY_YES && + (*containerType).ExtendedType().Codecs().IsEmpty()) { + // Per spec: 'Generally, a user agent should never return "probably" for a + // type that allows the `codecs` parameter if that parameter is not + // present.' As all our currently-supported types allow for `codecs`, we can + // do this check here. + // TODO: Instead, missing `codecs` should be checked in each decoder's + // `IsSupportedType` call from `CanHandleCodecsType()`. + // See bug 1399023. + return CANPLAY_MAYBE; + } + return status; +} + +void HTMLMediaElement::CanPlayType(const nsAString& aType, nsAString& aResult) { + DecoderDoctorDiagnostics diagnostics; + CanPlayStatus canPlay = GetCanPlay(aType, &diagnostics); + diagnostics.StoreFormatDiagnostics(OwnerDoc(), aType, canPlay != CANPLAY_NO, + __func__); + switch (canPlay) { + case CANPLAY_NO: + aResult.Truncate(); + break; + case CANPLAY_YES: + aResult.AssignLiteral("probably"); + break; + case CANPLAY_MAYBE: + aResult.AssignLiteral("maybe"); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected case."); + break; + } + + LOG(LogLevel::Debug, + ("%p CanPlayType(%s) = \"%s\"", this, NS_ConvertUTF16toUTF8(aType).get(), + NS_ConvertUTF16toUTF8(aResult).get())); +} + +void HTMLMediaElement::AssertReadyStateIsNothing() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (mReadyState != HAVE_NOTHING) { + char buf[1024]; + SprintfLiteral(buf, + "readyState=%d networkState=%d mLoadWaitStatus=%d " + "mSourceLoadCandidate=%d " + "mIsLoadingFromSourceChildren=%d mPreloadAction=%d " + "mSuspendedForPreloadNone=%d error=%d", + int(mReadyState), int(mNetworkState), int(mLoadWaitStatus), + !!mSourceLoadCandidate, mIsLoadingFromSourceChildren, + int(mPreloadAction), mSuspendedForPreloadNone, + GetError() ? GetError()->Code() : 0); + MOZ_CRASH_UNSAFE_PRINTF("ReadyState should be HAVE_NOTHING! %s", buf); + } +#endif +} + +nsresult HTMLMediaElement::InitializeDecoderAsClone( + ChannelMediaDecoder* aOriginal) { + NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set"); + NS_ASSERTION(mDecoder == nullptr, "Shouldn't have a decoder"); + AssertReadyStateIsNothing(); + + MediaDecoderInit decoderInit( + this, this, mMuted ? 0.0 : mVolume, mPreservesPitch, + ClampPlaybackRate(mPlaybackRate), + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint, + HasAttr(nsGkAtoms::loop), aOriginal->ContainerType()); + + RefPtr<ChannelMediaDecoder> decoder = aOriginal->Clone(decoderInit); + if (!decoder) return NS_ERROR_FAILURE; + + LOG(LogLevel::Debug, + ("%p Cloned decoder %p from %p", this, decoder.get(), aOriginal)); + + return FinishDecoderSetup(decoder); +} + +template <typename DecoderType, typename... LoadArgs> +nsresult HTMLMediaElement::SetupDecoder(DecoderType* aDecoder, + LoadArgs&&... aArgs) { + LOG(LogLevel::Debug, ("%p Created decoder %p for type %s", this, aDecoder, + aDecoder->ContainerType().OriginalString().Data())); + + nsresult rv = aDecoder->Load(std::forward<LoadArgs>(aArgs)...); + if (NS_FAILED(rv)) { + aDecoder->Shutdown(); + LOG(LogLevel::Debug, ("%p Failed to load for decoder %p", this, aDecoder)); + return rv; + } + + rv = FinishDecoderSetup(aDecoder); + // Only ChannelMediaDecoder supports resource cloning. + if (std::is_same_v<DecoderType, ChannelMediaDecoder> && NS_SUCCEEDED(rv)) { + AddMediaElementToURITable(); + NS_ASSERTION( + MediaElementTableCount(this, mLoadingSrc) == 1, + "Media element should have single table entry if decode initialized"); + } + + return rv; +} + +nsresult HTMLMediaElement::InitializeDecoderForChannel( + nsIChannel* aChannel, nsIStreamListener** aListener) { + NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set"); + AssertReadyStateIsNothing(); + + DecoderDoctorDiagnostics diagnostics; + + nsAutoCString mimeType; + aChannel->GetContentType(mimeType); + NS_ASSERTION(!mimeType.IsEmpty(), "We should have the Content-Type."); + NS_ConvertUTF8toUTF16 mimeUTF16(mimeType); + + RefPtr<HTMLMediaElement> self = this; + auto reportCanPlay = [&, self](bool aCanPlay) { + diagnostics.StoreFormatDiagnostics(self->OwnerDoc(), mimeUTF16, aCanPlay, + __func__); + if (!aCanPlay) { + nsAutoString src; + self->GetCurrentSrc(src); + AutoTArray<nsString, 2> params = {mimeUTF16, src}; + self->ReportLoadError("MediaLoadUnsupportedMimeType", params); + } + }; + + auto onExit = MakeScopeExit([self] { + if (self->mChannelLoader) { + self->mChannelLoader->Done(); + self->mChannelLoader = nullptr; + } + }); + + Maybe<MediaContainerType> containerType = MakeMediaContainerType(mimeType); + if (!containerType) { + reportCanPlay(false); + return NS_ERROR_FAILURE; + } + + MediaDecoderInit decoderInit( + this, this, mMuted ? 0.0 : mVolume, mPreservesPitch, + ClampPlaybackRate(mPlaybackRate), + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint, + HasAttr(nsGkAtoms::loop), *containerType); + +#ifdef MOZ_ANDROID_HLS_SUPPORT + if (HLSDecoder::IsSupportedType(*containerType)) { + RefPtr<HLSDecoder> decoder = HLSDecoder::Create(decoderInit); + if (!decoder) { + reportCanPlay(false); + return NS_ERROR_OUT_OF_MEMORY; + } + reportCanPlay(true); + return SetupDecoder(decoder.get(), aChannel); + } +#endif + + RefPtr<ChannelMediaDecoder> decoder = + ChannelMediaDecoder::Create(decoderInit, &diagnostics); + if (!decoder) { + reportCanPlay(false); + return NS_ERROR_FAILURE; + } + + reportCanPlay(true); + bool isPrivateBrowsing = NodePrincipal()->GetPrivateBrowsingId() > 0; + return SetupDecoder(decoder.get(), aChannel, isPrivateBrowsing, aListener); +} + +nsresult HTMLMediaElement::FinishDecoderSetup(MediaDecoder* aDecoder) { + ChangeNetworkState(NETWORK_LOADING); + + // Set mDecoder now so if methods like GetCurrentSrc get called between + // here and Load(), they work. + SetDecoder(aDecoder); + + // Notify the decoder of the initial activity status. + NotifyDecoderActivityChanges(); + + // Update decoder principal before we start decoding, since it + // can affect how we feed data to MediaStreams + NotifyDecoderPrincipalChanged(); + + // Set sink device if we have one. Otherwise the default is used. + if (mSink.second) { + mDecoder->SetSink(mSink.second); + } + + if (mMediaKeys) { + if (mMediaKeys->GetCDMProxy()) { + mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy()); + } else { + // CDM must have crashed. + ShutdownDecoder(); + return NS_ERROR_FAILURE; + } + } + + if (mChannelLoader) { + mChannelLoader->Done(); + mChannelLoader = nullptr; + } + + // We may want to suspend the new stream now. + // This will also do an AddRemoveSelfReference. + NotifyOwnerDocumentActivityChanged(); + + if (!mDecoder) { + // NotifyOwnerDocumentActivityChanged may shutdown the decoder if the + // owning document is inactive and we're in the EME case. We could try and + // handle this, but at the time of writing it's a pretty niche case, so just + // bail. + return NS_ERROR_FAILURE; + } + + if (mSuspendedByInactiveDocOrDocshell) { + mDecoder->Suspend(); + } + + if (!mPaused) { + SetPlayedOrSeeked(true); + if (!mSuspendedByInactiveDocOrDocshell) { + mDecoder->Play(); + } + } + + MaybeBeginCloningVisually(); + + return NS_OK; +} + +void HTMLMediaElement::UpdateSrcMediaStreamPlaying(uint32_t aFlags) { + if (!mSrcStream) { + return; + } + + bool shouldPlay = !(aFlags & REMOVING_SRC_STREAM) && !mPaused && + !mSuspendedByInactiveDocOrDocshell; + if (shouldPlay == mSrcStreamIsPlaying) { + return; + } + mSrcStreamIsPlaying = shouldPlay; + + LOG(LogLevel::Debug, + ("MediaElement %p %s playback of DOMMediaStream %p", this, + shouldPlay ? "Setting up" : "Removing", mSrcStream.get())); + + if (shouldPlay) { + mSrcStreamPlaybackEnded = false; + mSrcStreamReportPlaybackEnded = false; + + if (mMediaStreamRenderer) { + mMediaStreamRenderer->Start(); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Start(); + } + + SetCapturedOutputStreamsEnabled(true); // Unmute + // If the input is a media stream, we don't check its data and always regard + // it as audible when it's playing. + SetAudibleState(true); + } else { + if (mMediaStreamRenderer) { + mMediaStreamRenderer->Stop(); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Stop(); + } + SetCapturedOutputStreamsEnabled(false); // Mute + } +} + +void HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying() { + if (!mMediaStreamRenderer) { + // Notifications are async, the renderer could have been cleared. + return; + } + + mMediaStreamRenderer->SetProgressingCurrentTime(IsPotentiallyPlaying()); +} + +void HTMLMediaElement::UpdateSrcStreamTime() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mSrcStreamPlaybackEnded) { + // We do a separate FireTimeUpdate() when this is set. + return; + } + + FireTimeUpdate(TimeupdateType::ePeriodic); +} + +void HTMLMediaElement::SetupSrcMediaStreamPlayback(DOMMediaStream* aStream) { + NS_ASSERTION(!mSrcStream, "Should have been ended already"); + + mLoadingSrc = nullptr; + mSrcStream = aStream; + + VideoFrameContainer* container = GetVideoFrameContainer(); + RefPtr<FirstFrameVideoOutput> firstFrameOutput = + container ? MakeAndAddRef<FirstFrameVideoOutput>(container, + AbstractMainThread()) + : nullptr; + mMediaStreamRenderer = MakeAndAddRef<MediaStreamRenderer>( + AbstractMainThread(), container, firstFrameOutput, this); + mWatchManager.Watch(mPaused, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Watch(mReadyState, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Watch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Watch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded); + mWatchManager.Watch(mMediaStreamRenderer->CurrentGraphTime(), + &HTMLMediaElement::UpdateSrcStreamTime); + SetVolumeInternal(); + if (mSink.second) { + mMediaStreamRenderer->SetAudioOutputDevice(mSink.second); + } + + UpdateSrcMediaStreamPlaying(); + UpdateSrcStreamPotentiallyPlaying(); + mSrcStreamVideoPrincipal = NodePrincipal(); + + // If we pause this media element, track changes in the underlying stream + // will continue to fire events at this element and alter its track list. + // That's simpler than delaying the events, but probably confusing... + nsTArray<RefPtr<MediaStreamTrack>> tracks; + mSrcStream->GetTracks(tracks); + for (const RefPtr<MediaStreamTrack>& track : tracks) { + NotifyMediaStreamTrackAdded(track); + } + + mMediaStreamTrackListener = MakeUnique<MediaStreamTrackListener>(this); + mSrcStream->RegisterTrackListener(mMediaStreamTrackListener.get()); + + ChangeNetworkState(NETWORK_IDLE); + ChangeDelayLoadStatus(false); + + // FirstFrameLoaded() will be called when the stream has tracks. +} + +void HTMLMediaElement::EndSrcMediaStreamPlayback() { + MOZ_ASSERT(mSrcStream); + + UpdateSrcMediaStreamPlaying(REMOVING_SRC_STREAM); + + if (mSelectedVideoStreamTrack) { + mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(this); + } + mSelectedVideoStreamTrack = nullptr; + + MOZ_ASSERT_IF(mSecondaryMediaStreamRenderer, + !mMediaStreamRenderer == !mSecondaryMediaStreamRenderer); + if (mMediaStreamRenderer) { + mWatchManager.Unwatch(mPaused, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Unwatch(mReadyState, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Unwatch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Unwatch( + mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded); + mWatchManager.Unwatch(mMediaStreamRenderer->CurrentGraphTime(), + &HTMLMediaElement::UpdateSrcStreamTime); + mMediaStreamRenderer->Shutdown(); + mMediaStreamRenderer = nullptr; + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Shutdown(); + mSecondaryMediaStreamRenderer = nullptr; + } + + mSrcStream->UnregisterTrackListener(mMediaStreamTrackListener.get()); + mMediaStreamTrackListener = nullptr; + mSrcStreamPlaybackEnded = false; + mSrcStreamReportPlaybackEnded = false; + mSrcStreamVideoPrincipal = nullptr; + + mSrcStream = nullptr; +} + +static already_AddRefed<AudioTrack> CreateAudioTrack( + AudioStreamTrack* aStreamTrack, nsIGlobalObject* aOwnerGlobal) { + nsAutoString id; + nsAutoString label; + aStreamTrack->GetId(id); + aStreamTrack->GetLabel(label, CallerType::System); + + return MediaTrackList::CreateAudioTrack(aOwnerGlobal, id, u"main"_ns, label, + u""_ns, true, aStreamTrack); +} + +static already_AddRefed<VideoTrack> CreateVideoTrack( + VideoStreamTrack* aStreamTrack, nsIGlobalObject* aOwnerGlobal) { + nsAutoString id; + nsAutoString label; + aStreamTrack->GetId(id); + aStreamTrack->GetLabel(label, CallerType::System); + + return MediaTrackList::CreateVideoTrack(aOwnerGlobal, id, u"main"_ns, label, + u""_ns, aStreamTrack); +} + +void HTMLMediaElement::NotifyMediaStreamTrackAdded( + const RefPtr<MediaStreamTrack>& aTrack) { + MOZ_ASSERT(aTrack); + + if (aTrack->Ended()) { + return; + } + +#ifdef DEBUG + nsAutoString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("%p, Adding %sTrack with id %s", this, + aTrack->AsAudioStreamTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); +#endif + + if (AudioStreamTrack* t = aTrack->AsAudioStreamTrack()) { + MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked"); + RefPtr<AudioTrack> audioTrack = + CreateAudioTrack(t, AudioTracks()->GetOwnerGlobal()); + AudioTracks()->AddTrack(audioTrack); + } else if (VideoStreamTrack* t = aTrack->AsVideoStreamTrack()) { + // TODO: Fix this per the spec on bug 1273443. + if (!IsVideo()) { + return; + } + MOZ_DIAGNOSTIC_ASSERT(VideoTracks(), "Element can't have been unlinked"); + RefPtr<VideoTrack> videoTrack = + CreateVideoTrack(t, VideoTracks()->GetOwnerGlobal()); + VideoTracks()->AddTrack(videoTrack); + // New MediaStreamTrack added, set the new added video track as selected + // video track when there is no selected track. + if (VideoTracks()->SelectedIndex() == -1) { + MOZ_ASSERT(!mSelectedVideoStreamTrack); + videoTrack->SetEnabledInternal(true, dom::MediaTrack::FIRE_NO_EVENTS); + } + } + + // The set of enabled AudioTracks and selected video track might have changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); + AbstractThread::DispatchDirectTask( + NewRunnableMethod("HTMLMediaElement::FirstFrameLoaded", this, + &HTMLMediaElement::FirstFrameLoaded)); +} + +void HTMLMediaElement::NotifyMediaStreamTrackRemoved( + const RefPtr<MediaStreamTrack>& aTrack) { + MOZ_ASSERT(aTrack); + + nsAutoString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("%p, Removing %sTrack with id %s", this, + aTrack->AsAudioStreamTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); + + MOZ_DIAGNOSTIC_ASSERT(AudioTracks() && VideoTracks(), + "Element can't have been unlinked"); + if (dom::MediaTrack* t = AudioTracks()->GetTrackById(id)) { + AudioTracks()->RemoveTrack(t); + } else if (dom::MediaTrack* t = VideoTracks()->GetTrackById(id)) { + VideoTracks()->RemoveTrack(t); + } else { + NS_ASSERTION(aTrack->AsVideoStreamTrack() && !IsVideo(), + "MediaStreamTrack ended but did not exist in track lists. " + "This is only allowed if a video element ends and we are an " + "audio element."); + return; + } +} + +void HTMLMediaElement::ProcessMediaFragmentURI() { + if (!mLoadingSrc) { + mFragmentStart = mFragmentEnd = -1.0; + return; + } + nsMediaFragmentURIParser parser(mLoadingSrc); + + if (mDecoder && parser.HasEndTime()) { + mFragmentEnd = parser.GetEndTime(); + } + + if (parser.HasStartTime()) { + SetCurrentTime(parser.GetStartTime()); + mFragmentStart = parser.GetStartTime(); + } +} + +void HTMLMediaElement::MetadataLoaded(const MediaInfo* aInfo, + UniquePtr<const MetadataTags> aTags) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mDecoder) { + ConstructMediaTracks(aInfo); + } + + SetMediaInfo(*aInfo); + + mIsEncrypted = + aInfo->IsEncrypted() || mPendingEncryptedInitData.IsEncrypted(); + mTags = std::move(aTags); + mLoadedDataFired = false; + ChangeReadyState(HAVE_METADATA); + + // Add output tracks synchronously now to be sure they're available in + // "loadedmetadata" event handlers. + UpdateOutputTrackSources(); + + DispatchAsyncEvent(u"durationchange"_ns); + if (IsVideo() && HasVideo()) { + DispatchAsyncEvent(u"resize"_ns); + Invalidate(ImageSizeChanged::No, Some(mMediaInfo.mVideo.mDisplay), + ForceInvalidate::No); + } + NS_ASSERTION(!HasVideo() || (mMediaInfo.mVideo.mDisplay.width > 0 && + mMediaInfo.mVideo.mDisplay.height > 0), + "Video resolution must be known on 'loadedmetadata'"); + DispatchAsyncEvent(u"loadedmetadata"_ns); + + if (mDecoder && mDecoder->IsTransportSeekable() && + mDecoder->IsMediaSeekable()) { + ProcessMediaFragmentURI(); + mDecoder->SetFragmentEndTime(mFragmentEnd); + } + if (mIsEncrypted) { + // We only support playback of encrypted content via MSE by default. + if (!mMediaSource && Preferences::GetBool("media.eme.mse-only", true)) { + DecodeError( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + "Encrypted content not supported outside of MSE")); + return; + } + + // Dispatch a distinct 'encrypted' event for each initData we have. + for (const auto& initData : mPendingEncryptedInitData.mInitDatas) { + DispatchEncrypted(initData.mInitData, initData.mType); + } + mPendingEncryptedInitData.Reset(); + } + + if (IsVideo() && aInfo->HasVideo()) { + // We are a video element playing video so update the screen wakelock + NotifyOwnerDocumentActivityChanged(); + } + + if (mDefaultPlaybackStartPosition != 0.0) { + SetCurrentTime(mDefaultPlaybackStartPosition); + mDefaultPlaybackStartPosition = 0.0; + } + + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); +} + +void HTMLMediaElement::FirstFrameLoaded() { + LOG(LogLevel::Debug, + ("%p, FirstFrameLoaded() mFirstFrameLoaded=%d mWaitingForKey=%d", this, + mFirstFrameLoaded.Ref(), mWaitingForKey)); + + NS_ASSERTION(!mSuspendedAfterFirstFrame, "Should not have already suspended"); + + if (!mFirstFrameLoaded) { + mFirstFrameLoaded = true; + } + + ChangeDelayLoadStatus(false); + + if (mDecoder && mAllowSuspendAfterFirstFrame && mPaused && + !HasAttr(nsGkAtoms::autoplay) && + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA) { + mSuspendedAfterFirstFrame = true; + mDecoder->Suspend(); + } +} + +void HTMLMediaElement::NetworkError(const MediaResult& aError) { + if (mReadyState == HAVE_NOTHING) { + NoSupportedMediaSourceError(aError.Description()); + } else { + Error(MEDIA_ERR_NETWORK); + } +} + +void HTMLMediaElement::DecodeError(const MediaResult& aError) { + nsAutoString src; + GetCurrentSrc(src); + AutoTArray<nsString, 1> params = {src}; + ReportLoadError("MediaLoadDecodeError", params); + + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreDecodeError(OwnerDoc(), aError, src, __func__); + + if (mIsLoadingFromSourceChildren) { + mErrorSink->ResetError(); + if (mSourceLoadCandidate) { + DispatchAsyncSourceError(mSourceLoadCandidate); + QueueLoadFromSourceTask(); + } else { + NS_WARNING("Should know the source we were loading from!"); + } + } else if (mReadyState == HAVE_NOTHING) { + NoSupportedMediaSourceError(aError.Description()); + } else if (IsCORSSameOrigin()) { + Error(MEDIA_ERR_DECODE, aError.Description()); + } else { + Error(MEDIA_ERR_DECODE, "Failed to decode media"_ns); + } +} + +void HTMLMediaElement::DecodeWarning(const MediaResult& aError) { + nsAutoString src; + GetCurrentSrc(src); + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreDecodeWarning(OwnerDoc(), aError, src, __func__); +} + +bool HTMLMediaElement::HasError() const { return GetError(); } + +void HTMLMediaElement::LoadAborted() { Error(MEDIA_ERR_ABORTED); } + +void HTMLMediaElement::Error(uint16_t aErrorCode, + const nsACString& aErrorDetails) { + mErrorSink->SetError(aErrorCode, aErrorDetails); + ChangeDelayLoadStatus(false); + UpdateAudioChannelPlayingState(); +} + +void HTMLMediaElement::PlaybackEnded() { + // We changed state which can affect AddRemoveSelfReference + AddRemoveSelfReference(); + + NS_ASSERTION(!mDecoder || mDecoder->IsEnded(), + "Decoder fired ended, but not in ended state"); + + // IsPlaybackEnded() became true. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); + + if (mSrcStream) { + LOG(LogLevel::Debug, + ("%p, got duration by reaching the end of the resource", this)); + mSrcStreamPlaybackEnded = true; + DispatchAsyncEvent(u"durationchange"_ns); + } else { + // mediacapture-main: + // Setting the loop attribute has no effect since a MediaStream has no + // defined end and therefore cannot be looped. + if (HasAttr(nsGkAtoms::loop)) { + SetCurrentTime(0); + return; + } + } + + FireTimeUpdate(TimeupdateType::eMandatory); + + if (!mPaused) { + Pause(); + } + + if (mSrcStream) { + // A MediaStream that goes from inactive to active shall be eligible for + // autoplay again according to the mediacapture-main spec. + mCanAutoplayFlag = true; + } + + if (StaticPrefs::media_mediacontrol_stopcontrol_aftermediaends()) { + mMediaControlKeyListener->StopIfNeeded(); + } + DispatchAsyncEvent(u"ended"_ns); +} + +void HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded() { + mSrcStreamReportPlaybackEnded = mSrcStreamPlaybackEnded; +} + +void HTMLMediaElement::SeekStarted() { DispatchAsyncEvent(u"seeking"_ns); } + +void HTMLMediaElement::SeekCompleted() { + mPlayingBeforeSeek = false; + SetPlayedOrSeeked(true); + if (mTextTrackManager) { + mTextTrackManager->DidSeek(); + } + // https://html.spec.whatwg.org/multipage/media.html#seeking:dom-media-seek + // (Step 16) + // TODO (bug 1688131): run these steps in a stable state. + FireTimeUpdate(TimeupdateType::eMandatory); + DispatchAsyncEvent(u"seeked"_ns); + // We changed whether we're seeking so we need to AddRemoveSelfReference + AddRemoveSelfReference(); + if (mCurrentPlayRangeStart == -1.0) { + mCurrentPlayRangeStart = CurrentTime(); + } + + if (mSeekDOMPromise) { + AbstractMainThread()->Dispatch(NS_NewRunnableFunction( + __func__, [promise = std::move(mSeekDOMPromise)] { + promise->MaybeResolveWithUndefined(); + })); + } + MOZ_ASSERT(!mSeekDOMPromise); +} + +void HTMLMediaElement::SeekAborted() { + if (mSeekDOMPromise) { + AbstractMainThread()->Dispatch(NS_NewRunnableFunction( + __func__, [promise = std::move(mSeekDOMPromise)] { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + })); + } + MOZ_ASSERT(!mSeekDOMPromise); +} + +void HTMLMediaElement::NotifySuspendedByCache(bool aSuspendedByCache) { + LOG(LogLevel::Debug, + ("%p, mDownloadSuspendedByCache=%d", this, aSuspendedByCache)); + mDownloadSuspendedByCache = aSuspendedByCache; +} + +void HTMLMediaElement::DownloadSuspended() { + if (mNetworkState == NETWORK_LOADING) { + DispatchAsyncEvent(u"progress"_ns); + } + ChangeNetworkState(NETWORK_IDLE); +} + +void HTMLMediaElement::DownloadResumed() { + ChangeNetworkState(NETWORK_LOADING); +} + +void HTMLMediaElement::CheckProgress(bool aHaveNewProgress) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mNetworkState == NETWORK_LOADING); + + TimeStamp now = TimeStamp::NowLoRes(); + + if (aHaveNewProgress) { + mDataTime = now; + } + + // If this is the first progress, or PROGRESS_MS has passed since the last + // progress event fired and more data has arrived since then, fire a + // progress event. + NS_ASSERTION( + (mProgressTime.IsNull() && !aHaveNewProgress) || !mDataTime.IsNull(), + "null TimeStamp mDataTime should not be used in comparison"); + if (mProgressTime.IsNull() + ? aHaveNewProgress + : (now - mProgressTime >= + TimeDuration::FromMilliseconds(PROGRESS_MS) && + mDataTime > mProgressTime)) { + DispatchAsyncEvent(u"progress"_ns); + // Resolution() ensures that future data will have now > mProgressTime, + // and so will trigger another event. mDataTime is not reset because it + // is still required to detect stalled; it is similarly offset by + // resolution to indicate the new data has not yet arrived. + mProgressTime = now - TimeDuration::Resolution(); + if (mDataTime > mProgressTime) { + mDataTime = mProgressTime; + } + if (!mProgressTimer) { + NS_ASSERTION(aHaveNewProgress, + "timer dispatched when there was no timer"); + // Were stalled. Restart timer. + StartProgressTimer(); + if (!mLoadedDataFired) { + ChangeDelayLoadStatus(true); + } + } + // Download statistics may have been updated, force a recheck of the + // readyState. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); + } + + if (now - mDataTime >= TimeDuration::FromMilliseconds(STALL_MS)) { + if (!mMediaSource) { + DispatchAsyncEvent(u"stalled"_ns); + } else { + ChangeDelayLoadStatus(false); + } + + NS_ASSERTION(mProgressTimer, "detected stalled without timer"); + // Stop timer events, which prevents repeated stalled events until there + // is more progress. + StopProgress(); + } + + AddRemoveSelfReference(); +} + +/* static */ +void HTMLMediaElement::ProgressTimerCallback(nsITimer* aTimer, void* aClosure) { + auto* decoder = static_cast<HTMLMediaElement*>(aClosure); + decoder->CheckProgress(false); +} + +void HTMLMediaElement::StartProgressTimer() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mNetworkState == NETWORK_LOADING); + NS_ASSERTION(!mProgressTimer, "Already started progress timer."); + + NS_NewTimerWithFuncCallback( + getter_AddRefs(mProgressTimer), ProgressTimerCallback, this, PROGRESS_MS, + nsITimer::TYPE_REPEATING_SLACK, "HTMLMediaElement::ProgressTimerCallback", + GetMainThreadSerialEventTarget()); +} + +void HTMLMediaElement::StartProgress() { + // Record the time now for detecting stalled. + mDataTime = TimeStamp::NowLoRes(); + // Reset mProgressTime so that mDataTime is not indicating bytes received + // after the last progress event. + mProgressTime = TimeStamp(); + StartProgressTimer(); +} + +void HTMLMediaElement::StopProgress() { + MOZ_ASSERT(NS_IsMainThread()); + if (!mProgressTimer) { + return; + } + + mProgressTimer->Cancel(); + mProgressTimer = nullptr; +} + +void HTMLMediaElement::DownloadProgressed() { + if (mNetworkState != NETWORK_LOADING) { + return; + } + CheckProgress(true); +} + +bool HTMLMediaElement::ShouldCheckAllowOrigin() { + return mCORSMode != CORS_NONE; +} + +bool HTMLMediaElement::IsCORSSameOrigin() { + bool subsumes; + RefPtr<nsIPrincipal> principal = GetCurrentPrincipal(); + return (NS_SUCCEEDED(NodePrincipal()->Subsumes(principal, &subsumes)) && + subsumes) || + ShouldCheckAllowOrigin(); +} + +void HTMLMediaElement::UpdateReadyStateInternal() { + if (!mDecoder && !mSrcStream) { + // Not initialized - bail out. + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Not initialized", + this)); + return; + } + + if (mDecoder && mReadyState < HAVE_METADATA) { + // aNextFrame might have a next frame because the decoder can advance + // on its own thread before MetadataLoaded gets a chance to run. + // The arrival of more data can't change us out of this readyState. + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Decoder ready state < HAVE_METADATA", + this)); + return; + } + + if (mDecoder) { + // IsPlaybackEnded() might have become false. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); + } + + if (mSrcStream && mReadyState < HAVE_METADATA) { + bool hasAudioTracks = AudioTracks() && !AudioTracks()->IsEmpty(); + bool hasVideoTracks = VideoTracks() && !VideoTracks()->IsEmpty(); + if (!hasAudioTracks && !hasVideoTracks) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Stream with no tracks", + this)); + // Give it one last chance to remove the self reference if needed. + AddRemoveSelfReference(); + return; + } + + if (IsVideo() && hasVideoTracks && !HasVideo()) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Stream waiting for video", + this)); + return; + } + + LOG(LogLevel::Debug, + ("MediaElement %p UpdateReadyStateInternal() Stream has " + "metadata; audioTracks=%d, videoTracks=%d, " + "hasVideoFrame=%d", + this, AudioTracks()->Length(), VideoTracks()->Length(), HasVideo())); + + // We are playing a stream that has video and a video frame is now set. + // This means we have all metadata needed to change ready state. + MediaInfo mediaInfo = mMediaInfo; + if (hasAudioTracks) { + mediaInfo.EnableAudio(); + } + if (hasVideoTracks) { + mediaInfo.EnableVideo(); + if (mSelectedVideoStreamTrack) { + mediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha()); + } + } + MetadataLoaded(&mediaInfo, nullptr); + } + + if (mMediaSource) { + // readyState has changed, assuming it's following the pending mediasource + // operations. Notify the Mediasource that the operations have completed. + mMediaSource->CompletePendingTransactions(); + } + + enum NextFrameStatus nextFrameStatus = NextFrameStatus(); + if (mWaitingForKey == NOT_WAITING_FOR_KEY) { + if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE && mDecoder && + !mDecoder->IsEnded()) { + nextFrameStatus = mDecoder->NextFrameBufferedStatus(); + } + } else if (mWaitingForKey == WAITING_FOR_KEY) { + if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE || + nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) { + // http://w3c.github.io/encrypted-media/#wait-for-key + // Continuing 7.3.4 Queue a "waitingforkey" Event + // 4. Queue a task to fire a simple event named waitingforkey + // at the media element. + // 5. Set the readyState of media element to HAVE_METADATA. + // NOTE: We'll change to HAVE_CURRENT_DATA or HAVE_METADATA + // depending on whether we've loaded the first frame or not + // below. + // 6. Suspend playback. + // Note: Playback will already be stalled, as the next frame is + // unavailable. + mWaitingForKey = WAITING_FOR_KEY_DISPATCHED; + DispatchAsyncEvent(u"waitingforkey"_ns); + } + } else { + MOZ_ASSERT(mWaitingForKey == WAITING_FOR_KEY_DISPATCHED); + if (nextFrameStatus == NEXT_FRAME_AVAILABLE) { + // We have new frames after dispatching "waitingforkey". + // This means we've got the key and can reset mWaitingForKey now. + mWaitingForKey = NOT_WAITING_FOR_KEY; + } + } + + if (nextFrameStatus == MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING) { + LOG(LogLevel::Debug, + ("MediaElement %p UpdateReadyStateInternal() " + "NEXT_FRAME_UNAVAILABLE_SEEKING; Forcing HAVE_METADATA", + this)); + ChangeReadyState(HAVE_METADATA); + return; + } + + if (IsVideo() && VideoTracks() && !VideoTracks()->IsEmpty() && + !IsPlaybackEnded() && GetImageContainer() && + !GetImageContainer()->HasCurrentImage()) { + // Don't advance if we are playing video, but don't have a video frame. + // Also, if video became available after advancing to HAVE_CURRENT_DATA + // while we are still playing, we need to revert to HAVE_METADATA until + // a video frame is available. + LOG(LogLevel::Debug, + ("MediaElement %p UpdateReadyStateInternal() " + "Playing video but no video frame; Forcing HAVE_METADATA", + this)); + ChangeReadyState(HAVE_METADATA); + return; + } + + if (!mFirstFrameLoaded) { + // We haven't yet loaded the first frame, making us unable to determine + // if we have enough valid data at the present stage. + return; + } + + if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) { + // Force HAVE_CURRENT_DATA when buffering. + ChangeReadyState(HAVE_CURRENT_DATA); + return; + } + + // TextTracks must be loaded for the HAVE_ENOUGH_DATA and + // HAVE_FUTURE_DATA. + // So force HAVE_CURRENT_DATA if text tracks not loaded. + if (mTextTrackManager && !mTextTrackManager->IsLoaded()) { + ChangeReadyState(HAVE_CURRENT_DATA); + return; + } + + if (mDownloadSuspendedByCache && mDecoder && !mDecoder->IsEnded()) { + // The decoder has signaled that the download has been suspended by the + // media cache. So move readyState into HAVE_ENOUGH_DATA, in case there's + // script waiting for a "canplaythrough" event; without this forced + // transition, we will never fire the "canplaythrough" event if the + // media cache is too small, and scripts are bound to fail. Don't force + // this transition if the decoder is in ended state; the readyState + // should remain at HAVE_CURRENT_DATA in this case. + // Note that this state transition includes the case where we finished + // downloaded the whole data stream. + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Decoder download suspended by cache", + this)); + ChangeReadyState(HAVE_ENOUGH_DATA); + return; + } + + if (nextFrameStatus != MediaDecoderOwner::NEXT_FRAME_AVAILABLE) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Next frame not available", + this)); + ChangeReadyState(HAVE_CURRENT_DATA); + return; + } + + if (mSrcStream) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Stream HAVE_ENOUGH_DATA", + this)); + ChangeReadyState(HAVE_ENOUGH_DATA); + return; + } + + // Now see if we should set HAVE_ENOUGH_DATA. + // If it's something we don't know the size of, then we can't + // make a real estimate, so we go straight to HAVE_ENOUGH_DATA once + // we've downloaded enough data that our download rate is considered + // reliable. We have to move to HAVE_ENOUGH_DATA at some point or + // autoplay elements for live streams will never play. Otherwise we + // move to HAVE_ENOUGH_DATA if we can play through the entire media + // without stopping to buffer. + if (mDecoder->CanPlayThrough()) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Decoder can play through", + this)); + ChangeReadyState(HAVE_ENOUGH_DATA); + return; + } + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Default; Decoder has future data", + this)); + ChangeReadyState(HAVE_FUTURE_DATA); +} + +static const char* const gReadyStateToString[] = { + "HAVE_NOTHING", "HAVE_METADATA", "HAVE_CURRENT_DATA", "HAVE_FUTURE_DATA", + "HAVE_ENOUGH_DATA"}; + +void HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState) { + if (mReadyState == aState) { + return; + } + + nsMediaReadyState oldState = mReadyState; + mReadyState = aState; + LOG(LogLevel::Debug, + ("%p Ready state changed to %s", this, gReadyStateToString[aState])); + + DDLOG(DDLogCategory::Property, "ready_state", gReadyStateToString[aState]); + + // https://html.spec.whatwg.org/multipage/media.html#text-track-cue-active-flag + // The user agent must synchronously unset cues' active flag whenever the + // media element's readyState is changed back to HAVE_NOTHING. + if (mReadyState == HAVE_NOTHING && mTextTrackManager) { + mTextTrackManager->NotifyReset(); + } + + if (mNetworkState == NETWORK_EMPTY) { + return; + } + + UpdateAudioChannelPlayingState(); + + // Handle raising of "waiting" event during seek (see 4.8.10.9) + // or + // 4.8.12.7 Ready states: + // "If the previous ready state was HAVE_FUTURE_DATA or more, and the new + // ready state is HAVE_CURRENT_DATA or less + // If the media element was potentially playing before its readyState + // attribute changed to a value lower than HAVE_FUTURE_DATA, and the element + // has not ended playback, and playback has not stopped due to errors, + // paused for user interaction, or paused for in-band content, the user agent + // must queue a task to fire a simple event named timeupdate at the element, + // and queue a task to fire a simple event named waiting at the element." + if (mPlayingBeforeSeek && mReadyState < HAVE_FUTURE_DATA) { + DispatchAsyncEvent(u"waiting"_ns); + } else if (oldState >= HAVE_FUTURE_DATA && mReadyState < HAVE_FUTURE_DATA && + !Paused() && !Ended() && !mErrorSink->mError) { + FireTimeUpdate(TimeupdateType::eMandatory); + DispatchAsyncEvent(u"waiting"_ns); + } + + if (oldState < HAVE_CURRENT_DATA && mReadyState >= HAVE_CURRENT_DATA && + !mLoadedDataFired) { + DispatchAsyncEvent(u"loadeddata"_ns); + mLoadedDataFired = true; + } + + if (oldState < HAVE_FUTURE_DATA && mReadyState >= HAVE_FUTURE_DATA) { + DispatchAsyncEvent(u"canplay"_ns); + if (!mPaused) { + if (mDecoder && !mSuspendedByInactiveDocOrDocshell) { + MOZ_ASSERT(AllowedToPlay()); + mDecoder->Play(); + } + NotifyAboutPlaying(); + } + } + + CheckAutoplayDataReady(); + + if (oldState < HAVE_ENOUGH_DATA && mReadyState >= HAVE_ENOUGH_DATA) { + DispatchAsyncEvent(u"canplaythrough"_ns); + } +} + +static const char* const gNetworkStateToString[] = {"EMPTY", "IDLE", "LOADING", + "NO_SOURCE"}; + +void HTMLMediaElement::ChangeNetworkState(nsMediaNetworkState aState) { + if (mNetworkState == aState) { + return; + } + + nsMediaNetworkState oldState = mNetworkState; + mNetworkState = aState; + LOG(LogLevel::Debug, + ("%p Network state changed to %s", this, gNetworkStateToString[aState])); + DDLOG(DDLogCategory::Property, "network_state", + gNetworkStateToString[aState]); + + if (oldState == NETWORK_LOADING) { + // Stop progress notification when exiting NETWORK_LOADING. + StopProgress(); + } + + if (mNetworkState == NETWORK_LOADING) { + // Start progress notification when entering NETWORK_LOADING. + StartProgress(); + } else if (mNetworkState == NETWORK_IDLE && !mErrorSink->mError) { + // Fire 'suspend' event when entering NETWORK_IDLE and no error presented. + DispatchAsyncEvent(u"suspend"_ns); + } + + // According to the resource selection (step2, step9-18), dedicated media + // source failure step (step4) and aborting existing load (step4), set show + // poster flag to true. https://html.spec.whatwg.org/multipage/media.html + if (mNetworkState == NETWORK_NO_SOURCE || mNetworkState == NETWORK_EMPTY) { + mShowPoster = true; + } + + // Changing mNetworkState affects AddRemoveSelfReference(). + AddRemoveSelfReference(); +} + +bool HTMLMediaElement::IsEligibleForAutoplay() { + // We also activate autoplay when playing a media source since the data + // download is controlled by the script and there is no way to evaluate + // MediaDecoder::CanPlayThrough(). + + if (!HasAttr(nsGkAtoms::autoplay)) { + return false; + } + + if (!mCanAutoplayFlag) { + return false; + } + + if (IsEditable()) { + return false; + } + + if (!mPaused) { + return false; + } + + if (mSuspendedByInactiveDocOrDocshell) { + return false; + } + + // Static document is used for print preview and printing, should not be + // autoplay + if (OwnerDoc()->IsStaticDocument()) { + return false; + } + + if (ShouldBeSuspendedByInactiveDocShell()) { + LOG(LogLevel::Debug, ("%p prohibiting autoplay by the docShell", this)); + return false; + } + + if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) { + CreateResumeDelayedMediaPlaybackAgentIfNeeded(); + LOG(LogLevel::Debug, ("%p delay playing from autoplay", this)); + return false; + } + + return mReadyState >= HAVE_ENOUGH_DATA; +} + +void HTMLMediaElement::CheckAutoplayDataReady() { + if (!IsEligibleForAutoplay()) { + return; + } + if (!AllowedToPlay()) { + DispatchEventsWhenPlayWasNotAllowed(); + return; + } + RunAutoplay(); +} + +void HTMLMediaElement::RunAutoplay() { + mAllowedToPlayPromise.ResolveIfExists(true, __func__); + mPaused = false; + // We changed mPaused which can affect AddRemoveSelfReference + AddRemoveSelfReference(); + UpdateSrcMediaStreamPlaying(); + UpdateAudioChannelPlayingState(); + StartMediaControlKeyListenerIfNeeded(); + + if (mDecoder) { + SetPlayedOrSeeked(true); + if (mCurrentPlayRangeStart == -1.0) { + mCurrentPlayRangeStart = CurrentTime(); + } + MOZ_ASSERT(!mSuspendedByInactiveDocOrDocshell); + mDecoder->Play(); + } else if (mSrcStream) { + SetPlayedOrSeeked(true); + } + + // https://html.spec.whatwg.org/multipage/media.html#ready-states:show-poster-flag + if (mShowPoster) { + mShowPoster = false; + if (mTextTrackManager) { + mTextTrackManager->TimeMarchesOn(); + } + } + + // For blocked media, the event would be pending until it is resumed. + DispatchAsyncEvent(u"play"_ns); + + DispatchAsyncEvent(u"playing"_ns); +} + +bool HTMLMediaElement::IsActuallyInvisible() const { + // That means an element is not connected. It probably hasn't connected to a + // document tree, or connects to a disconnected DOM tree. + if (!IsInComposedDoc()) { + return true; + } + + // An element is not in user's view port, which means it's either existing in + // somewhere in the page where user hasn't seen yet, or is being set + // `display:none`. + if (!IsInViewPort()) { + return true; + } + + // Element being used in picture-in-picture mode would be always visible. + if (IsBeingUsedInPictureInPictureMode()) { + return false; + } + + // That check is the page is in the background. + return OwnerDoc()->Hidden(); +} + +bool HTMLMediaElement::IsInViewPort() const { + return mVisibilityState == Visibility::ApproximatelyVisible; +} + +VideoFrameContainer* HTMLMediaElement::GetVideoFrameContainer() { + if (mShuttingDown) { + return nullptr; + } + + if (mVideoFrameContainer) return mVideoFrameContainer; + + // Only video frames need an image container. + if (!IsVideo()) { + return nullptr; + } + + mVideoFrameContainer = new VideoFrameContainer( + this, MakeAndAddRef<ImageContainer>(ImageContainer::ASYNCHRONOUS)); + + return mVideoFrameContainer; +} + +void HTMLMediaElement::PrincipalChanged(MediaStreamTrack* aTrack) { + if (aTrack != mSelectedVideoStreamTrack) { + return; + } + + nsContentUtils::CombineResourcePrincipals(&mSrcStreamVideoPrincipal, + aTrack->GetPrincipal()); + + LOG(LogLevel::Debug, + ("HTMLMediaElement %p video track principal changed to %p (combined " + "into %p). Waiting for it to reach VideoFrameContainer before setting.", + this, aTrack->GetPrincipal(), mSrcStreamVideoPrincipal.get())); + + if (mVideoFrameContainer) { + UpdateSrcStreamVideoPrincipal( + mVideoFrameContainer->GetLastPrincipalHandle()); + } +} + +void HTMLMediaElement::UpdateSrcStreamVideoPrincipal( + const PrincipalHandle& aPrincipalHandle) { + nsTArray<RefPtr<VideoStreamTrack>> videoTracks; + mSrcStream->GetVideoTracks(videoTracks); + + for (const RefPtr<VideoStreamTrack>& track : videoTracks) { + if (PrincipalHandleMatches(aPrincipalHandle, track->GetPrincipal()) && + !track->Ended()) { + // When the PrincipalHandle for the VideoFrameContainer changes to that of + // a live track in mSrcStream we know that a removed track was displayed + // but is no longer so. + LOG(LogLevel::Debug, ("HTMLMediaElement %p VideoFrameContainer's " + "PrincipalHandle matches track %p. That's all we " + "need.", + this, track.get())); + mSrcStreamVideoPrincipal = track->GetPrincipal(); + break; + } + } +} + +void HTMLMediaElement::PrincipalHandleChangedForVideoFrameContainer( + VideoFrameContainer* aContainer, + const PrincipalHandle& aNewPrincipalHandle) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mSrcStream) { + return; + } + + LOG(LogLevel::Debug, ("HTMLMediaElement %p PrincipalHandle changed in " + "VideoFrameContainer.", + this)); + + UpdateSrcStreamVideoPrincipal(aNewPrincipalHandle); +} + +already_AddRefed<nsMediaEventRunner> HTMLMediaElement::GetEventRunner( + const nsAString& aName, EventFlag aFlag) { + RefPtr<nsMediaEventRunner> runner; + if (aName.EqualsLiteral("playing")) { + runner = new nsNotifyAboutPlayingRunner(this, TakePendingPlayPromises()); + } else if (aName.EqualsLiteral("timeupdate")) { + runner = new nsTimeupdateRunner(this, aFlag == EventFlag::eMandatory); + } else { + runner = new nsAsyncEventRunner(aName, this); + } + return runner.forget(); +} + +nsresult HTMLMediaElement::DispatchEvent(const nsAString& aName) { + LOG_EVENT(LogLevel::Debug, ("%p Dispatching event %s", this, + NS_ConvertUTF16toUTF8(aName).get())); + + if (mEventBlocker->ShouldBlockEventDelivery()) { + RefPtr<nsMediaEventRunner> runner = GetEventRunner(aName); + mEventBlocker->PostponeEvent(runner); + return NS_OK; + } + + return nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, aName, + CanBubble::eNo, Cancelable::eNo); +} + +void HTMLMediaElement::DispatchAsyncEvent(const nsAString& aName) { + RefPtr<nsMediaEventRunner> runner = GetEventRunner(aName); + DispatchAsyncEvent(std::move(runner)); +} + +void HTMLMediaElement::DispatchAsyncEvent(RefPtr<nsMediaEventRunner> aRunner) { + NS_ConvertUTF16toUTF8 eventName(aRunner->EventName()); + LOG_EVENT(LogLevel::Debug, ("%p Queuing event %s", this, eventName.get())); + DDLOG(DDLogCategory::Event, "HTMLMediaElement", nsCString(eventName.get())); + if (mEventBlocker->ShouldBlockEventDelivery()) { + mEventBlocker->PostponeEvent(aRunner); + return; + } + GetMainThreadSerialEventTarget()->Dispatch(aRunner.forget()); +} + +bool HTMLMediaElement::IsPotentiallyPlaying() const { + // TODO: + // playback has not stopped due to errors, + // and the element has not paused for user interaction + return !mPaused && + (mReadyState == HAVE_ENOUGH_DATA || mReadyState == HAVE_FUTURE_DATA) && + !IsPlaybackEnded(); +} + +bool HTMLMediaElement::IsPlaybackEnded() const { + // TODO: + // the current playback position is equal to the effective end of the media + // resource. See bug 449157. + if (mDecoder) { + return mReadyState >= HAVE_METADATA && mDecoder->IsEnded(); + } + if (mSrcStream) { + return mReadyState >= HAVE_METADATA && mSrcStreamPlaybackEnded; + } + return false; +} + +already_AddRefed<nsIPrincipal> HTMLMediaElement::GetCurrentPrincipal() { + if (mDecoder) { + return mDecoder->GetCurrentPrincipal(); + } + if (mSrcStream) { + nsTArray<RefPtr<MediaStreamTrack>> tracks; + mSrcStream->GetTracks(tracks); + nsCOMPtr<nsIPrincipal> principal = mSrcStream->GetPrincipal(); + return principal.forget(); + } + return nullptr; +} + +bool HTMLMediaElement::HadCrossOriginRedirects() { + if (mDecoder) { + return mDecoder->HadCrossOriginRedirects(); + } + return false; +} + +bool HTMLMediaElement::ShouldResistFingerprinting(RFPTarget aTarget) const { + return OwnerDoc()->ShouldResistFingerprinting(aTarget); +} + +already_AddRefed<nsIPrincipal> HTMLMediaElement::GetCurrentVideoPrincipal() { + if (mDecoder) { + return mDecoder->GetCurrentPrincipal(); + } + if (mSrcStream) { + nsCOMPtr<nsIPrincipal> principal = mSrcStreamVideoPrincipal; + return principal.forget(); + } + return nullptr; +} + +void HTMLMediaElement::NotifyDecoderPrincipalChanged() { + RefPtr<nsIPrincipal> principal = GetCurrentPrincipal(); + bool isSameOrigin = !principal || IsCORSSameOrigin(); + mDecoder->UpdateSameOriginStatus(isSameOrigin); + + if (isSameOrigin) { + principal = NodePrincipal(); + } + for (const auto& entry : mOutputTrackSources.Values()) { + entry->SetPrincipal(principal); + } + mDecoder->SetOutputTracksPrincipal(principal); +} + +void HTMLMediaElement::Invalidate(ImageSizeChanged aImageSizeChanged, + const Maybe<nsIntSize>& aNewIntrinsicSize, + ForceInvalidate aForceInvalidate) { + nsIFrame* frame = GetPrimaryFrame(); + if (aNewIntrinsicSize) { + UpdateMediaSize(aNewIntrinsicSize.value()); + if (frame) { + nsPresContext* presContext = frame->PresContext(); + PresShell* presShell = presContext->PresShell(); + presShell->FrameNeedsReflow(frame, + IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); + } + } + + RefPtr<ImageContainer> imageContainer = GetImageContainer(); + bool asyncInvalidate = imageContainer && imageContainer->IsAsync() && + aForceInvalidate == ForceInvalidate::No; + if (frame) { + if (aImageSizeChanged == ImageSizeChanged::Yes) { + frame->InvalidateFrame(); + } else { + frame->InvalidateLayer(DisplayItemType::TYPE_VIDEO, nullptr, nullptr, + asyncInvalidate ? nsIFrame::UPDATE_IS_ASYNC : 0); + } + } + + SVGObserverUtils::InvalidateDirectRenderingObservers(this); +} + +void HTMLMediaElement::UpdateMediaSize(const nsIntSize& aSize) { + MOZ_ASSERT(NS_IsMainThread()); + + if (IsVideo() && mReadyState != HAVE_NOTHING && + mMediaInfo.mVideo.mDisplay != aSize) { + DispatchAsyncEvent(u"resize"_ns); + } + + mMediaInfo.mVideo.mDisplay = aSize; + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); +} + +void HTMLMediaElement::SuspendOrResumeElement(bool aSuspendElement) { + LOG(LogLevel::Debug, ("%p SuspendOrResumeElement(suspend=%d) docHidden=%d", + this, aSuspendElement, OwnerDoc()->Hidden())); + + if (aSuspendElement == mSuspendedByInactiveDocOrDocshell) { + return; + } + + mSuspendedByInactiveDocOrDocshell = aSuspendElement; + UpdateSrcMediaStreamPlaying(); + UpdateAudioChannelPlayingState(); + + if (aSuspendElement) { + if (mDecoder) { + mDecoder->Pause(); + mDecoder->Suspend(); + mDecoder->SetDelaySeekMode(true); + } + mEventBlocker->SetBlockEventDelivery(true); + // We won't want to resume media element from the bfcache. + ClearResumeDelayedMediaPlaybackAgentIfNeeded(); + mMediaControlKeyListener->StopIfNeeded(); + } else { + if (mDecoder) { + mDecoder->Resume(); + if (!mPaused && !mDecoder->IsEnded()) { + mDecoder->Play(); + } + mDecoder->SetDelaySeekMode(false); + } + mEventBlocker->SetBlockEventDelivery(false); + // If the media element has been blocked and isn't still allowed to play + // when it comes back from the bfcache, we would notify front end to show + // the blocking icon in order to inform user that the site is still being + // blocked. + if (mHasEverBeenBlockedForAutoplay && !AllowedToPlay()) { + MaybeNotifyAutoplayBlocked(); + } + StartMediaControlKeyListenerIfNeeded(); + } + if (StaticPrefs::media_testing_only_events()) { + auto dispatcher = MakeRefPtr<AsyncEventDispatcher>( + this, u"MozMediaSuspendChanged"_ns, CanBubble::eYes, + ChromeOnlyDispatch::eYes); + dispatcher->PostDOMEvent(); + } +} + +bool HTMLMediaElement::IsBeingDestroyed() { + nsIDocShell* docShell = OwnerDoc()->GetDocShell(); + bool isBeingDestroyed = false; + if (docShell) { + docShell->IsBeingDestroyed(&isBeingDestroyed); + } + return isBeingDestroyed; +} + +bool HTMLMediaElement::ShouldBeSuspendedByInactiveDocShell() const { + BrowsingContext* bc = OwnerDoc()->GetBrowsingContext(); + return bc && !bc->IsActive() && bc->Top()->GetSuspendMediaWhenInactive(); +} + +void HTMLMediaElement::NotifyOwnerDocumentActivityChanged() { + if (mDecoder && !IsBeingDestroyed()) { + NotifyDecoderActivityChanges(); + } + + // We would suspend media when the document is inactive, or its docshell has + // been set to hidden and explicitly wants to suspend media. In those cases, + // the media would be not visible and we don't want them to continue playing. + bool shouldSuspend = + !OwnerDoc()->IsActive() || ShouldBeSuspendedByInactiveDocShell(); + SuspendOrResumeElement(shouldSuspend); + + // If the owning document has become inactive we should shutdown the CDM. + if (!OwnerDoc()->IsCurrentActiveDocument() && mMediaKeys) { + // We don't shutdown MediaKeys here because it also listens for document + // activity and will take care of shutting down itself. + DDUNLINKCHILD(mMediaKeys.get()); + mMediaKeys = nullptr; + if (mDecoder) { + ShutdownDecoder(); + } + } + + AddRemoveSelfReference(); +} + +void HTMLMediaElement::NotifyFullScreenChanged() { + const bool isInFullScreen = IsInFullScreen(); + if (isInFullScreen) { + StartMediaControlKeyListenerIfNeeded(); + if (!mMediaControlKeyListener->IsStarted()) { + MEDIACONTROL_LOG("Failed to start the listener when entering fullscreen"); + } + } + // Updating controller fullscreen state no matter the listener starts or not. + BrowsingContext* bc = OwnerDoc()->GetBrowsingContext(); + if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(bc)) { + updater->NotifyMediaFullScreenState(bc->Id(), isInFullScreen); + } +} + +void HTMLMediaElement::AddRemoveSelfReference() { + // XXX we could release earlier here in many situations if we examined + // which event listeners are attached. Right now we assume there is a + // potential listener for every event. We would also have to keep the + // element alive if it was playing and producing audio output --- right now + // that's covered by the !mPaused check. + Document* ownerDoc = OwnerDoc(); + + // See the comment at the top of this file for the explanation of this + // boolean expression. + bool needSelfReference = + !mShuttingDown && ownerDoc->IsActive() && + (mDelayingLoadEvent || (!mPaused && !Ended()) || + (mDecoder && mDecoder->IsSeeking()) || IsEligibleForAutoplay() || + (mMediaSource ? mProgressTimer : mNetworkState == NETWORK_LOADING)); + + if (needSelfReference != mHasSelfReference) { + mHasSelfReference = needSelfReference; + RefPtr<HTMLMediaElement> self = this; + if (needSelfReference) { + // The shutdown observer will hold a strong reference to us. This + // will do to keep us alive. We need to know about shutdown so that + // we can release our self-reference. + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "dom::HTMLMediaElement::AddSelfReference", + [self]() { self->mShutdownObserver->AddRefMediaElement(); })); + } else { + // Dispatch Release asynchronously so that we don't destroy this object + // inside a call stack of method calls on this object + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "dom::HTMLMediaElement::AddSelfReference", + [self]() { self->mShutdownObserver->ReleaseMediaElement(); })); + } + } +} + +void HTMLMediaElement::NotifyShutdownEvent() { + mShuttingDown = true; + ResetState(); + AddRemoveSelfReference(); +} + +void HTMLMediaElement::DispatchAsyncSourceError(nsIContent* aSourceElement) { + LOG_EVENT(LogLevel::Debug, ("%p Queuing simple source error event", this)); + + nsCOMPtr<nsIRunnable> event = + new nsSourceErrorEventRunner(this, aSourceElement); + GetMainThreadSerialEventTarget()->Dispatch(event.forget()); +} + +void HTMLMediaElement::NotifyAddedSource() { + // If a source element is inserted as a child of a media element + // that has no src attribute and whose networkState has the value + // NETWORK_EMPTY, the user agent must invoke the media element's + // resource selection algorithm. + if (!HasAttr(nsGkAtoms::src) && mNetworkState == NETWORK_EMPTY) { + AssertReadyStateIsNothing(); + QueueSelectResourceTask(); + } + + // A load was paused in the resource selection algorithm, waiting for + // a new source child to be added, resume the resource selection algorithm. + if (mLoadWaitStatus == WAITING_FOR_SOURCE) { + // Rest the flag so we don't queue multiple LoadFromSourceTask() when + // multiple <source> are attached in an event loop. + mLoadWaitStatus = NOT_WAITING; + QueueLoadFromSourceTask(); + } +} + +HTMLSourceElement* HTMLMediaElement::GetNextSource() { + mSourceLoadCandidate = nullptr; + + while (true) { + if (mSourcePointer == nsINode::GetLastChild()) { + return nullptr; // no more children + } + + if (!mSourcePointer) { + mSourcePointer = nsINode::GetFirstChild(); + } else { + mSourcePointer = mSourcePointer->GetNextSibling(); + } + nsIContent* child = mSourcePointer; + + // If child is a <source> element, it is the next candidate. + if (auto* source = HTMLSourceElement::FromNodeOrNull(child)) { + mSourceLoadCandidate = source; + return source; + } + } + MOZ_ASSERT_UNREACHABLE("Execution should not reach here!"); + return nullptr; +} + +void HTMLMediaElement::ChangeDelayLoadStatus(bool aDelay) { + if (mDelayingLoadEvent == aDelay) return; + + mDelayingLoadEvent = aDelay; + + LOG(LogLevel::Debug, ("%p ChangeDelayLoadStatus(%d) doc=0x%p", this, aDelay, + mLoadBlockedDoc.get())); + if (mDecoder) { + mDecoder->SetLoadInBackground(!aDelay); + } + if (aDelay) { + mLoadBlockedDoc = OwnerDoc(); + mLoadBlockedDoc->BlockOnload(); + } else { + // mLoadBlockedDoc might be null due to GC unlinking + if (mLoadBlockedDoc) { + mLoadBlockedDoc->UnblockOnload(false); + mLoadBlockedDoc = nullptr; + } + } + + // We changed mDelayingLoadEvent which can affect AddRemoveSelfReference + AddRemoveSelfReference(); +} + +already_AddRefed<nsILoadGroup> HTMLMediaElement::GetDocumentLoadGroup() { + if (!OwnerDoc()->IsActive()) { + NS_WARNING("Load group requested for media element in inactive document."); + } + return OwnerDoc()->GetDocumentLoadGroup(); +} + +nsresult HTMLMediaElement::CopyInnerTo(Element* aDest) { + nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + if (aDest->OwnerDoc()->IsStaticDocument()) { + HTMLMediaElement* dest = static_cast<HTMLMediaElement*>(aDest); + dest->SetMediaInfo(mMediaInfo); + } + return rv; +} + +already_AddRefed<TimeRanges> HTMLMediaElement::Buffered() const { + media::TimeIntervals buffered = + mDecoder ? mDecoder->GetBuffered() : media::TimeIntervals(); + RefPtr<TimeRanges> ranges = new TimeRanges( + ToSupports(OwnerDoc()), buffered.ToMicrosecondResolution()); + return ranges.forget(); +} + +void HTMLMediaElement::SetRequestHeaders(nsIHttpChannel* aChannel) { + // Send Accept header for video and audio types only (Bug 489071) + SetAcceptHeader(aChannel); + + // Apache doesn't send Content-Length when gzip transfer encoding is used, + // which prevents us from estimating the video length (if explicit + // Content-Duration and a length spec in the container are not present either) + // and from seeking. So, disable the standard "Accept-Encoding: gzip,deflate" + // that we usually send. See bug 614760. + DebugOnly<nsresult> rv = + aChannel->SetRequestHeader("Accept-Encoding"_ns, ""_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Set the Referrer header + // + // FIXME: Shouldn't this use the Element constructor? Though I guess it + // doesn't matter as no HTMLMediaElement supports the referrerinfo attribute. + auto referrerInfo = MakeRefPtr<ReferrerInfo>(*OwnerDoc()); + rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +const TimeStamp& HTMLMediaElement::LastTimeupdateDispatchTime() const { + MOZ_ASSERT(NS_IsMainThread()); + return mLastTimeUpdateDispatchTime; +} + +void HTMLMediaElement::UpdateLastTimeupdateDispatchTime() { + MOZ_ASSERT(NS_IsMainThread()); + mLastTimeUpdateDispatchTime = TimeStamp::Now(); +} + +bool HTMLMediaElement::ShouldQueueTimeupdateAsyncTask( + TimeupdateType aType) const { + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + // That means dispatching `timeupdate` is mandatorily required in the spec. + if (aType == TimeupdateType::eMandatory) { + return true; + } + + // The timeupdate only occurs when the current playback position changes. + // https://html.spec.whatwg.org/multipage/media.html#event-media-timeupdate + if (mLastCurrentTime == CurrentTime()) { + return false; + } + + // Number of milliseconds between timeupdate events as defined by spec. + if (!mQueueTimeUpdateRunnerTime.IsNull() && + TimeStamp::Now() - mQueueTimeUpdateRunnerTime < + TimeDuration::FromMilliseconds(TIMEUPDATE_MS)) { + return false; + } + return true; +} + +void HTMLMediaElement::FireTimeUpdate(TimeupdateType aType) { + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + + if (ShouldQueueTimeupdateAsyncTask(aType)) { + RefPtr<nsMediaEventRunner> runner = + GetEventRunner(u"timeupdate"_ns, aType == TimeupdateType::eMandatory + ? EventFlag::eMandatory + : EventFlag::eNone); + DispatchAsyncEvent(std::move(runner)); + mQueueTimeUpdateRunnerTime = TimeStamp::Now(); + mLastCurrentTime = CurrentTime(); + } + if (mFragmentEnd >= 0.0 && CurrentTime() >= mFragmentEnd) { + Pause(); + mFragmentEnd = -1.0; + mFragmentStart = -1.0; + mDecoder->SetFragmentEndTime(mFragmentEnd); + } + + // Update the cues displaying on the video. + // Here mTextTrackManager can be null if the cycle collector has unlinked + // us before our parent. In that case UnbindFromTree will call us + // when our parent is unlinked. + if (mTextTrackManager) { + mTextTrackManager->TimeMarchesOn(); + } +} + +MediaError* HTMLMediaElement::GetError() const { return mErrorSink->mError; } + +void HTMLMediaElement::GetCurrentSpec(nsCString& aString) { + // If playing a regular URL, an ObjectURL of a Blob/File, return that. + if (mLoadingSrc) { + mLoadingSrc->GetSpec(aString); + } else if (mSrcMediaSource) { + // If playing an ObjectURL, and it's a MediaSource, return the value of the + // `src` attribute. + nsAutoString src; + GetSrc(src); + CopyUTF16toUTF8(src, aString); + } else { + // Playing e.g. a MediaStream via an object URL - return an empty string + aString.Truncate(); + } +} + +double HTMLMediaElement::MozFragmentEnd() { + double duration = Duration(); + + // If there is no end fragment, or the fragment end is greater than the + // duration, return the duration. + return (mFragmentEnd < 0.0 || mFragmentEnd > duration) ? duration + : mFragmentEnd; +} + +void HTMLMediaElement::SetDefaultPlaybackRate(double aDefaultPlaybackRate, + ErrorResult& aRv) { + if (mSrcAttrStream) { + return; + } + + if (aDefaultPlaybackRate < 0) { + aRv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return; + } + + double defaultPlaybackRate = ClampPlaybackRate(aDefaultPlaybackRate); + + if (mDefaultPlaybackRate == defaultPlaybackRate) { + return; + } + + mDefaultPlaybackRate = defaultPlaybackRate; + DispatchAsyncEvent(u"ratechange"_ns); +} + +void HTMLMediaElement::SetPlaybackRate(double aPlaybackRate, ErrorResult& aRv) { + if (mSrcAttrStream) { + return; + } + + // Changing the playback rate of a media that has more than two channels is + // not supported. + if (aPlaybackRate < 0) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return; + } + + if (mPlaybackRate == aPlaybackRate) { + return; + } + + mPlaybackRate = aPlaybackRate; + // Playback rate threshold above which audio is muted. + uint32_t threshold = StaticPrefs::media_audio_playbackrate_muting_threshold(); + if (mPlaybackRate != 0.0 && + (mPlaybackRate > threshold || mPlaybackRate < 1. / threshold)) { + SetMutedInternal(mMuted | MUTED_BY_INVALID_PLAYBACK_RATE); + } else { + SetMutedInternal(mMuted & ~MUTED_BY_INVALID_PLAYBACK_RATE); + } + + if (mDecoder) { + mDecoder->SetPlaybackRate(ClampPlaybackRate(mPlaybackRate)); + } + DispatchAsyncEvent(u"ratechange"_ns); +} + +void HTMLMediaElement::SetPreservesPitch(bool aPreservesPitch) { + mPreservesPitch = aPreservesPitch; + if (mDecoder) { + mDecoder->SetPreservesPitch(mPreservesPitch); + } +} + +ImageContainer* HTMLMediaElement::GetImageContainer() { + VideoFrameContainer* container = GetVideoFrameContainer(); + return container ? container->GetImageContainer() : nullptr; +} + +void HTMLMediaElement::UpdateAudioChannelPlayingState() { + if (mAudioChannelWrapper) { + mAudioChannelWrapper->UpdateAudioChannelPlayingState(); + } +} + +static const char* VisibilityString(Visibility aVisibility) { + switch (aVisibility) { + case Visibility::Untracked: { + return "Untracked"; + } + case Visibility::ApproximatelyNonVisible: { + return "ApproximatelyNonVisible"; + } + case Visibility::ApproximatelyVisible: { + return "ApproximatelyVisible"; + } + } + + return "NAN"; +} + +void HTMLMediaElement::OnVisibilityChange(Visibility aNewVisibility) { + LOG(LogLevel::Debug, + ("OnVisibilityChange(): %s\n", VisibilityString(aNewVisibility))); + + mVisibilityState = aNewVisibility; + if (StaticPrefs::media_test_video_suspend()) { + DispatchAsyncEvent(u"visibilitychanged"_ns); + } + + if (!mDecoder) { + return; + } + NotifyDecoderActivityChanges(); +} + +MediaKeys* HTMLMediaElement::GetMediaKeys() const { return mMediaKeys; } + +bool HTMLMediaElement::ContainsRestrictedContent() const { + return GetMediaKeys() != nullptr; +} + +void HTMLMediaElement::SetCDMProxyFailure(const MediaResult& aResult) { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + + ResetSetMediaKeysTempVariables(); + + mSetMediaKeysDOMPromise->MaybeReject(aResult.Code(), aResult.Message()); +} + +void HTMLMediaElement::RemoveMediaKeys() { + LOG(LogLevel::Debug, ("%s", __func__)); + // 5.2.3 Stop using the CDM instance represented by the mediaKeys attribute + // to decrypt media data and remove the association with the media element. + if (mMediaKeys) { + mMediaKeys->Unbind(); + } + mMediaKeys = nullptr; +} + +bool HTMLMediaElement::TryRemoveMediaKeysAssociation() { + MOZ_ASSERT(mMediaKeys); + LOG(LogLevel::Debug, ("%s", __func__)); + // 5.2.1 If the user agent or CDM do not support removing the association, + // let this object's attaching media keys value be false and reject promise + // with a new DOMException whose name is NotSupportedError. + // 5.2.2 If the association cannot currently be removed, let this object's + // attaching media keys value be false and reject promise with a new + // DOMException whose name is InvalidStateError. + if (mDecoder) { + RefPtr<HTMLMediaElement> self = this; + mDecoder->SetCDMProxy(nullptr) + ->Then( + AbstractMainThread(), __func__, + [self]() { + self->mSetCDMRequest.Complete(); + + self->RemoveMediaKeys(); + if (self->AttachNewMediaKeys()) { + // No incoming MediaKeys object or MediaDecoder is not + // created yet. + self->MakeAssociationWithCDMResolved(); + } + }, + [self](const MediaResult& aResult) { + self->mSetCDMRequest.Complete(); + // 5.2.4 If the preceding step failed, let this object's + // attaching media keys value be false and reject promise with + // a new DOMException whose name is the appropriate error name. + self->SetCDMProxyFailure(aResult); + }) + ->Track(mSetCDMRequest); + return false; + } + + RemoveMediaKeys(); + return true; +} + +bool HTMLMediaElement::DetachExistingMediaKeys() { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + // 5.1 If mediaKeys is not null, CDM instance represented by mediaKeys is + // already in use by another media element, and the user agent is unable + // to use it with this element, let this object's attaching media keys + // value be false and reject promise with a new DOMException whose name + // is QuotaExceededError. + if (mIncomingMediaKeys && mIncomingMediaKeys->IsBoundToMediaElement()) { + SetCDMProxyFailure(MediaResult( + NS_ERROR_DOM_MEDIA_KEY_QUOTA_EXCEEDED_ERR, + "MediaKeys object is already bound to another HTMLMediaElement")); + return false; + } + + // 5.2 If the mediaKeys attribute is not null, run the following steps: + if (mMediaKeys) { + return TryRemoveMediaKeysAssociation(); + } + return true; +} + +void HTMLMediaElement::MakeAssociationWithCDMResolved() { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + + // 5.4 Set the mediaKeys attribute to mediaKeys. + mMediaKeys = mIncomingMediaKeys; +#ifdef MOZ_WMF_CDM + if (mMediaKeys && mMediaKeys->GetCDMProxy()) { + mIsUsingWMFCDM = !!mMediaKeys->GetCDMProxy()->AsWMFCDMProxy(); + } +#endif + // 5.5 Let this object's attaching media keys value be false. + ResetSetMediaKeysTempVariables(); + // 5.6 Resolve promise. + mSetMediaKeysDOMPromise->MaybeResolveWithUndefined(); + mSetMediaKeysDOMPromise = nullptr; +} + +bool HTMLMediaElement::TryMakeAssociationWithCDM(CDMProxy* aProxy) { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(aProxy); + + // 5.3.3 Queue a task to run the "Attempt to Resume Playback If Necessary" + // algorithm on the media element. + // Note: Setting the CDMProxy on the MediaDecoder will unblock playback. + if (mDecoder) { + // CDMProxy is set asynchronously in MediaFormatReader, once it's done, + // HTMLMediaElement should resolve or reject the DOM promise. + RefPtr<HTMLMediaElement> self = this; + mDecoder->SetCDMProxy(aProxy) + ->Then( + AbstractMainThread(), __func__, + [self]() { + self->mSetCDMRequest.Complete(); + self->MakeAssociationWithCDMResolved(); + }, + [self](const MediaResult& aResult) { + self->mSetCDMRequest.Complete(); + self->SetCDMProxyFailure(aResult); + }) + ->Track(mSetCDMRequest); + return false; + } + return true; +} + +bool HTMLMediaElement::AttachNewMediaKeys() { + LOG(LogLevel::Debug, + ("%s incoming MediaKeys(%p)", __func__, mIncomingMediaKeys.get())); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + + // 5.3. If mediaKeys is not null, run the following steps: + if (mIncomingMediaKeys) { + auto* cdmProxy = mIncomingMediaKeys->GetCDMProxy(); + if (!cdmProxy) { + SetCDMProxyFailure(MediaResult( + NS_ERROR_DOM_INVALID_STATE_ERR, + "CDM crashed before binding MediaKeys object to HTMLMediaElement")); + return false; + } + + // 5.3.1 Associate the CDM instance represented by mediaKeys with the + // media element for decrypting media data. + if (NS_FAILED(mIncomingMediaKeys->Bind(this))) { + // 5.3.2 If the preceding step failed, run the following steps: + + // 5.3.2.1 Set the mediaKeys attribute to null. + mMediaKeys = nullptr; + // 5.3.2.2 Let this object's attaching media keys value be false. + // 5.3.2.3 Reject promise with a new DOMException whose name is + // the appropriate error name. + SetCDMProxyFailure( + MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR, + "Failed to bind MediaKeys object to HTMLMediaElement")); + return false; + } + return TryMakeAssociationWithCDM(cdmProxy); + } + return true; +} + +void HTMLMediaElement::ResetSetMediaKeysTempVariables() { + mAttachingMediaKey = false; + mIncomingMediaKeys = nullptr; +} + +already_AddRefed<Promise> HTMLMediaElement::SetMediaKeys( + mozilla::dom::MediaKeys* aMediaKeys, ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p SetMediaKeys(%p) mMediaKeys=%p mDecoder=%p", this, + aMediaKeys, mMediaKeys.get(), mDecoder.get())); + + if (MozAudioCaptured()) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + RefPtr<DetailedPromise> promise = DetailedPromise::Create( + win->AsGlobal(), aRv, "HTMLMediaElement.setMediaKeys"_ns); + if (aRv.Failed()) { + return nullptr; + } + + // 1. If mediaKeys and the mediaKeys attribute are the same object, + // return a resolved promise. + if (mMediaKeys == aMediaKeys) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + // 2. If this object's attaching media keys value is true, return a + // promise rejected with a new DOMException whose name is InvalidStateError. + if (mAttachingMediaKey) { + promise->MaybeRejectWithInvalidStateError( + "A MediaKeys object is in attaching operation."); + return promise.forget(); + } + + // 3. Let this object's attaching media keys value be true. + mAttachingMediaKey = true; + mIncomingMediaKeys = aMediaKeys; + + // 4. Let promise be a new promise. + mSetMediaKeysDOMPromise = promise; + + // 5. Run the following steps in parallel: + + // 5.1 & 5.2 & 5.3 + if (!DetachExistingMediaKeys() || !AttachNewMediaKeys()) { + return promise.forget(); + } + + // 5.4, 5.5, 5.6 + MakeAssociationWithCDMResolved(); + + // 6. Return promise. + return promise.forget(); +} + +EventHandlerNonNull* HTMLMediaElement::GetOnencrypted() { + return EventTarget::GetEventHandler(nsGkAtoms::onencrypted); +} + +void HTMLMediaElement::SetOnencrypted(EventHandlerNonNull* aCallback) { + EventTarget::SetEventHandler(nsGkAtoms::onencrypted, aCallback); +} + +EventHandlerNonNull* HTMLMediaElement::GetOnwaitingforkey() { + return EventTarget::GetEventHandler(nsGkAtoms::onwaitingforkey); +} + +void HTMLMediaElement::SetOnwaitingforkey(EventHandlerNonNull* aCallback) { + EventTarget::SetEventHandler(nsGkAtoms::onwaitingforkey, aCallback); +} + +void HTMLMediaElement::DispatchEncrypted(const nsTArray<uint8_t>& aInitData, + const nsAString& aInitDataType) { + LOG(LogLevel::Debug, ("%p DispatchEncrypted initDataType='%s'", this, + NS_ConvertUTF16toUTF8(aInitDataType).get())); + + if (mReadyState == HAVE_NOTHING) { + // Ready state not HAVE_METADATA (yet), don't dispatch encrypted now. + // Queueing for later dispatch in MetadataLoaded. + mPendingEncryptedInitData.AddInitData(aInitDataType, aInitData); + return; + } + + RefPtr<MediaEncryptedEvent> event; + if (IsCORSSameOrigin()) { + event = MediaEncryptedEvent::Constructor(this, aInitDataType, aInitData); + } else { + event = MediaEncryptedEvent::Constructor(this); + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, event.forget()); + asyncDispatcher->PostDOMEvent(); +} + +bool HTMLMediaElement::IsEventAttributeNameInternal(nsAtom* aName) { + return aName == nsGkAtoms::onencrypted || + nsGenericHTMLElement::IsEventAttributeNameInternal(aName); +} + +void HTMLMediaElement::NotifyWaitingForKey() { + LOG(LogLevel::Debug, ("%p, NotifyWaitingForKey()", this)); + + // http://w3c.github.io/encrypted-media/#wait-for-key + // 7.3.4 Queue a "waitingforkey" Event + // 1. Let the media element be the specified HTMLMediaElement object. + // 2. If the media element's waiting for key value is true, abort these steps. + if (mWaitingForKey == NOT_WAITING_FOR_KEY) { + // 3. Set the media element's waiting for key value to true. + // Note: algorithm continues in UpdateReadyStateInternal() when all decoded + // data enqueued in the MDSM is consumed. + mWaitingForKey = WAITING_FOR_KEY; + // mWaitingForKey changed outside of UpdateReadyStateInternal. This may + // affect mReadyState. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); + } +} + +AudioTrackList* HTMLMediaElement::AudioTracks() { return mAudioTrackList; } + +VideoTrackList* HTMLMediaElement::VideoTracks() { return mVideoTrackList; } + +TextTrackList* HTMLMediaElement::GetTextTracks() { + return GetOrCreateTextTrackManager()->GetTextTracks(); +} + +already_AddRefed<TextTrack> HTMLMediaElement::AddTextTrack( + TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage) { + return GetOrCreateTextTrackManager()->AddTextTrack( + aKind, aLabel, aLanguage, TextTrackMode::Hidden, + TextTrackReadyState::Loaded, TextTrackSource::AddTextTrack); +} + +void HTMLMediaElement::PopulatePendingTextTrackList() { + if (mTextTrackManager) { + mTextTrackManager->PopulatePendingList(); + } +} + +TextTrackManager* HTMLMediaElement::GetOrCreateTextTrackManager() { + if (!mTextTrackManager) { + mTextTrackManager = new TextTrackManager(this); + mTextTrackManager->AddListeners(); + } + return mTextTrackManager; +} + +MediaDecoderOwner::NextFrameStatus HTMLMediaElement::NextFrameStatus() { + if (mDecoder) { + return mDecoder->NextFrameStatus(); + } + if (mSrcStream) { + AutoTArray<RefPtr<MediaTrack>, 4> tracks; + GetAllEnabledMediaTracks(tracks); + if (!tracks.IsEmpty() && !mSrcStreamPlaybackEnded) { + return NEXT_FRAME_AVAILABLE; + } + return NEXT_FRAME_UNAVAILABLE; + } + return NEXT_FRAME_UNINITIALIZED; +} + +void HTMLMediaElement::SetDecoder(MediaDecoder* aDecoder) { + MOZ_ASSERT(aDecoder); // Use ShutdownDecoder() to clear. + if (mDecoder) { + ShutdownDecoder(); + } + mDecoder = aDecoder; + DDLINKCHILD("decoder", mDecoder.get()); + if (mDecoder && mForcedHidden) { + mDecoder->SetForcedHidden(mForcedHidden); + } +} + +float HTMLMediaElement::ComputedVolume() const { + return mMuted ? 0.0f + : mAudioChannelWrapper ? mAudioChannelWrapper->GetEffectiveVolume() + : static_cast<float>(mVolume); +} + +bool HTMLMediaElement::ComputedMuted() const { + return (mMuted & MUTED_BY_AUDIO_CHANNEL); +} + +bool HTMLMediaElement::IsSuspendedByInactiveDocOrDocShell() const { + return mSuspendedByInactiveDocOrDocshell; +} + +bool HTMLMediaElement::IsCurrentlyPlaying() const { + // We have playable data, but we still need to check whether data is "real" + // current data. + return mReadyState >= HAVE_CURRENT_DATA && !IsPlaybackEnded(); +} + +void HTMLMediaElement::SetAudibleState(bool aAudible) { + if (mIsAudioTrackAudible != aAudible) { + mIsAudioTrackAudible = aAudible; + NotifyAudioPlaybackChanged( + AudioChannelService::AudibleChangedReasons::eDataAudibleChanged); + } +} + +void HTMLMediaElement::NotifyAudioPlaybackChanged( + AudibleChangedReasons aReason) { + if (mAudioChannelWrapper) { + mAudioChannelWrapper->NotifyAudioPlaybackChanged(aReason); + } + // We would start the listener after media becomes audible. + const bool isAudible = IsAudible(); + if (isAudible && !mMediaControlKeyListener->IsStarted()) { + StartMediaControlKeyListenerIfNeeded(); + } + mMediaControlKeyListener->UpdateMediaAudibleState(isAudible); + // only request wake lock for audible media. + UpdateWakeLock(); +} + +void HTMLMediaElement::SetMediaInfo(const MediaInfo& aInfo) { + const bool oldHasAudio = mMediaInfo.HasAudio(); + mMediaInfo = aInfo; + if ((aInfo.HasAudio() != oldHasAudio) && mResumeDelayedPlaybackAgent) { + mResumeDelayedPlaybackAgent->UpdateAudibleState(this, IsAudible()); + } + nsILoadContext* loadContext = OwnerDoc()->GetLoadContext(); + if (HasAudio() && loadContext && !loadContext->UsePrivateBrowsing()) { + mTitleChangeObserver->Subscribe(); + UpdateStreamName(); + } else { + mTitleChangeObserver->Unsubscribe(); + } + if (mAudioChannelWrapper) { + mAudioChannelWrapper->AudioCaptureTrackChangeIfNeeded(); + } + UpdateWakeLock(); +} + +MediaInfo HTMLMediaElement::GetMediaInfo() const { return mMediaInfo; } + +FrameStatistics* HTMLMediaElement::GetFrameStatistics() const { + return mDecoder ? &(mDecoder->GetFrameStatistics()) : nullptr; +} + +void HTMLMediaElement::DispatchAsyncTestingEvent(const nsAString& aName) { + if (!StaticPrefs::media_testing_only_events()) { + return; + } + DispatchAsyncEvent(aName); +} + +void HTMLMediaElement::AudioCaptureTrackChange(bool aCapture) { + // No need to capture a silent media element. + if (!HasAudio()) { + return; + } + + if (aCapture && !mStreamWindowCapturer) { + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return; + } + + MediaTrackGraph* mtg = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, window, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + MediaTrackGraph::DEFAULT_OUTPUT_DEVICE); + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED, + StreamCaptureType::CAPTURE_AUDIO, mtg); + mStreamWindowCapturer = + MakeUnique<MediaStreamWindowCapturer>(stream, window->WindowID()); + } else if (!aCapture && mStreamWindowCapturer) { + for (size_t i = 0; i < mOutputStreams.Length(); i++) { + if (mOutputStreams[i].mStream == mStreamWindowCapturer->mStream) { + // We own this MediaStream, it is not exposed to JS. + AutoTArray<RefPtr<MediaStreamTrack>, 2> tracks; + mStreamWindowCapturer->mStream->GetTracks(tracks); + for (auto& track : tracks) { + track->Stop(); + } + mOutputStreams.RemoveElementAt(i); + break; + } + } + mStreamWindowCapturer = nullptr; + if (mOutputStreams.IsEmpty()) { + mTracksCaptured = nullptr; + } + } +} + +void HTMLMediaElement::NotifyCueDisplayStatesChanged() { + if (!mTextTrackManager) { + return; + } + + mTextTrackManager->DispatchUpdateCueDisplay(); +} + +void HTMLMediaElement::LogVisibility(CallerAPI aAPI) { + const bool isVisible = mVisibilityState == Visibility::ApproximatelyVisible; + + LOG(LogLevel::Debug, ("%p visibility = %u, API: '%d' and 'All'", this, + isVisible, static_cast<int>(aAPI))); + + if (!isVisible) { + LOG(LogLevel::Debug, ("%p inTree = %u, API: '%d' and 'All'", this, + IsInComposedDoc(), static_cast<int>(aAPI))); + } +} + +void HTMLMediaElement::UpdateCustomPolicyAfterPlayed() { + if (mAudioChannelWrapper) { + mAudioChannelWrapper->NotifyPlayStateChanged(); + } +} + +AbstractThread* HTMLMediaElement::AbstractMainThread() const { + return AbstractThread::MainThread(); +} + +nsTArray<RefPtr<PlayPromise>> HTMLMediaElement::TakePendingPlayPromises() { + return std::move(mPendingPlayPromises); +} + +void HTMLMediaElement::NotifyAboutPlaying() { + // Stick to the DispatchAsyncEvent() call path for now because we want to + // trigger some telemetry-related codes in the DispatchAsyncEvent() method. + DispatchAsyncEvent(u"playing"_ns); +} + +already_AddRefed<PlayPromise> HTMLMediaElement::CreatePlayPromise( + ErrorResult& aRv) const { + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<PlayPromise> promise = PlayPromise::Create(win->AsGlobal(), aRv); + LOG(LogLevel::Debug, ("%p created PlayPromise %p", this, promise.get())); + + return promise.forget(); +} + +already_AddRefed<Promise> HTMLMediaElement::CreateDOMPromise( + ErrorResult& aRv) const { + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return Promise::Create(win->AsGlobal(), aRv); +} + +void HTMLMediaElement::AsyncResolvePendingPlayPromises() { + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIRunnable> event = new nsResolveOrRejectPendingPlayPromisesRunner( + this, TakePendingPlayPromises()); + + GetMainThreadSerialEventTarget()->Dispatch(event.forget()); +} + +void HTMLMediaElement::AsyncRejectPendingPlayPromises(nsresult aError) { + if (!mPaused) { + mPaused = true; + DispatchAsyncEvent(u"pause"_ns); + } + + if (mShuttingDown) { + return; + } + + if (aError == NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR) { + DispatchEventsWhenPlayWasNotAllowed(); + } + + nsCOMPtr<nsIRunnable> event = new nsResolveOrRejectPendingPlayPromisesRunner( + this, TakePendingPlayPromises(), aError); + + GetMainThreadSerialEventTarget()->Dispatch(event.forget()); +} + +void HTMLMediaElement::GetEMEInfo(dom::EMEDebugInfo& aInfo) { + MOZ_ASSERT(NS_IsMainThread(), + "MediaKeys expects to be interacted with on main thread!"); + if (!mMediaKeys) { + return; + } + mMediaKeys->GetKeySystem(aInfo.mKeySystem); + mMediaKeys->GetSessionsInfo(aInfo.mSessionsInfo); +} + +void HTMLMediaElement::NotifyDecoderActivityChanges() const { + if (mDecoder) { + mDecoder->NotifyOwnerActivityChanged(IsActuallyInvisible(), + IsInComposedDoc()); + } +} + +Document* HTMLMediaElement::GetDocument() const { return OwnerDoc(); } + +bool HTMLMediaElement::IsAudible() const { + // No audio track. + if (!HasAudio()) { + return false; + } + + // Muted or the volume should not be ~0 + if (mMuted || (std::fabs(Volume()) <= 1e-7)) { + return false; + } + + return mIsAudioTrackAudible; +} + +Maybe<nsAutoString> HTMLMediaElement::GetKeySystem() const { + if (!mMediaKeys) { + return Nothing(); + } + nsAutoString keySystem; + mMediaKeys->GetKeySystem(keySystem); + return Some(keySystem); +} + +void HTMLMediaElement::ConstructMediaTracks(const MediaInfo* aInfo) { + if (!aInfo) { + return; + } + + AudioTrackList* audioList = AudioTracks(); + if (audioList && aInfo->HasAudio()) { + const TrackInfo& info = aInfo->mAudio; + RefPtr<AudioTrack> track = MediaTrackList::CreateAudioTrack( + audioList->GetOwnerGlobal(), info.mId, info.mKind, info.mLabel, + info.mLanguage, info.mEnabled); + + audioList->AddTrack(track); + } + + VideoTrackList* videoList = VideoTracks(); + if (videoList && aInfo->HasVideo()) { + const TrackInfo& info = aInfo->mVideo; + RefPtr<VideoTrack> track = MediaTrackList::CreateVideoTrack( + videoList->GetOwnerGlobal(), info.mId, info.mKind, info.mLabel, + info.mLanguage); + + videoList->AddTrack(track); + track->SetEnabledInternal(info.mEnabled, MediaTrack::FIRE_NO_EVENTS); + } +} + +void HTMLMediaElement::RemoveMediaTracks() { + if (mAudioTrackList) { + mAudioTrackList->RemoveTracks(); + } + if (mVideoTrackList) { + mVideoTrackList->RemoveTracks(); + } +} + +class MediaElementGMPCrashHelper : public GMPCrashHelper { + public: + explicit MediaElementGMPCrashHelper(HTMLMediaElement* aElement) + : mElement(aElement) { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + } + already_AddRefed<nsPIDOMWindowInner> GetPluginCrashedEventTarget() override { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + if (!mElement) { + return nullptr; + } + return do_AddRef(mElement->OwnerDoc()->GetInnerWindow()); + } + + private: + WeakPtr<HTMLMediaElement> mElement; +}; + +already_AddRefed<GMPCrashHelper> HTMLMediaElement::CreateGMPCrashHelper() { + return MakeAndAddRef<MediaElementGMPCrashHelper>(this); +} + +void HTMLMediaElement::MarkAsTainted() { + mHasSuspendTaint = true; + + if (mDecoder) { + mDecoder->SetSuspendTaint(true); + } +} + +bool HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj) { + return nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::debugger) || + nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::tabs); +} + +already_AddRefed<Promise> HTMLMediaElement::SetSinkId(const nsAString& aSinkId, + ErrorResult& aRv) { + LOG(LogLevel::Info, + ("%p, setSinkId(%s)", this, NS_ConvertUTF16toUTF8(aSinkId).get())); + nsCOMPtr<nsPIDOMWindowInner> win = OwnerDoc()->GetInnerWindow(); + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!FeaturePolicyUtils::IsFeatureAllowed(win->GetExtantDoc(), + u"speaker-selection"_ns)) { + promise->MaybeRejectWithNotAllowedError( + "Document's Permissions Policy does not allow setSinkId()"); + } + + if (mSink.first.Equals(aSinkId)) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + RefPtr<MediaDevices> mediaDevices = win->Navigator()->GetMediaDevices(aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsString sinkId(aSinkId); + mediaDevices->GetSinkDevice(sinkId) + ->Then( + AbstractMainThread(), __func__, + [self = RefPtr<HTMLMediaElement>(this), + this](RefPtr<AudioDeviceInfo>&& aInfo) { + // Sink found switch output device. + MOZ_ASSERT(aInfo); + if (mDecoder) { + RefPtr<SinkInfoPromise> p = mDecoder->SetSink(aInfo)->Then( + AbstractMainThread(), __func__, + [aInfo](const GenericPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + return SinkInfoPromise::CreateAndResolve(aInfo, __func__); + } + return SinkInfoPromise::CreateAndReject( + aValue.RejectValue(), __func__); + }); + return p; + } + if (mSrcStream) { + MOZ_ASSERT(mMediaStreamRenderer); + RefPtr<SinkInfoPromise> p = + mMediaStreamRenderer->SetAudioOutputDevice(aInfo)->Then( + AbstractMainThread(), __func__, + [aInfo]( + const GenericPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + return SinkInfoPromise::CreateAndResolve(aInfo, + __func__); + } + return SinkInfoPromise::CreateAndReject( + aValue.RejectValue(), __func__); + }); + return p; + } + // No media attached to the element save it for later. + return SinkInfoPromise::CreateAndResolve(aInfo, __func__); + }, + [](nsresult res) { + // Promise is rejected, sink not found. + return SinkInfoPromise::CreateAndReject(res, __func__); + }) + ->Then(AbstractMainThread(), __func__, + [promise, self = RefPtr<HTMLMediaElement>(this), this, + sinkId](const SinkInfoPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + LOG(LogLevel::Info, ("%p, set sinkid=%s", this, + NS_ConvertUTF16toUTF8(sinkId).get())); + mSink = std::pair(sinkId, aValue.ResolveValue()); + promise->MaybeResolveWithUndefined(); + } else { + switch (aValue.RejectValue()) { + case NS_ERROR_ABORT: + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + break; + case NS_ERROR_NOT_AVAILABLE: { + promise->MaybeRejectWithNotFoundError( + "The object can not be found here."); + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Invalid error."); + } + } + }); + + aRv = NS_OK; + return promise.forget(); +} + +void HTMLMediaElement::NotifyTextTrackModeChanged() { + if (mPendingTextTrackChanged) { + return; + } + mPendingTextTrackChanged = true; + AbstractMainThread()->Dispatch( + NS_NewRunnableFunction("HTMLMediaElement::NotifyTextTrackModeChanged", + [this, self = RefPtr<HTMLMediaElement>(this)]() { + mPendingTextTrackChanged = false; + if (!mTextTrackManager) { + return; + } + GetTextTracks()->CreateAndDispatchChangeEvent(); + // https://html.spec.whatwg.org/multipage/media.html#text-track-model:show-poster-flag + if (!mShowPoster) { + mTextTrackManager->TimeMarchesOn(); + } + })); +} + +void HTMLMediaElement::CreateResumeDelayedMediaPlaybackAgentIfNeeded() { + if (mResumeDelayedPlaybackAgent) { + return; + } + mResumeDelayedPlaybackAgent = + MediaPlaybackDelayPolicy::CreateResumeDelayedPlaybackAgent(this, + IsAudible()); + if (!mResumeDelayedPlaybackAgent) { + LOG(LogLevel::Debug, + ("%p Failed to create a delayed playback agant", this)); + return; + } + mResumeDelayedPlaybackAgent->GetResumePromise() + ->Then( + AbstractMainThread(), __func__, + [self = RefPtr<HTMLMediaElement>(this)]() { + LOG(LogLevel::Debug, ("%p Resume delayed Play() call", self.get())); + self->mResumePlaybackRequest.Complete(); + self->mResumeDelayedPlaybackAgent = nullptr; + IgnoredErrorResult dummy; + RefPtr<Promise> toBeIgnored = self->Play(dummy); + }, + [self = RefPtr<HTMLMediaElement>(this)]() { + LOG(LogLevel::Debug, + ("%p Can not resume delayed Play() call", self.get())); + self->mResumePlaybackRequest.Complete(); + self->mResumeDelayedPlaybackAgent = nullptr; + }) + ->Track(mResumePlaybackRequest); +} + +void HTMLMediaElement::ClearResumeDelayedMediaPlaybackAgentIfNeeded() { + if (mResumeDelayedPlaybackAgent) { + mResumePlaybackRequest.DisconnectIfExists(); + mResumeDelayedPlaybackAgent = nullptr; + } +} + +void HTMLMediaElement::NotifyMediaControlPlaybackStateChanged() { + if (!mMediaControlKeyListener->IsStarted()) { + return; + } + if (mPaused) { + mMediaControlKeyListener->NotifyMediaStoppedPlaying(); + } else { + mMediaControlKeyListener->NotifyMediaStartedPlaying(); + } +} + +bool HTMLMediaElement::IsInFullScreen() const { + return State().HasState(ElementState::FULLSCREEN); +} + +bool HTMLMediaElement::IsPlayable() const { + return (mDecoder || mSrcStream) && !HasError(); +} + +bool HTMLMediaElement::ShouldStartMediaControlKeyListener() const { + if (!IsPlayable()) { + MEDIACONTROL_LOG("Not start listener because media is not playable"); + return false; + } + + if (mSrcStream) { + MEDIACONTROL_LOG("Not listening because media is real-time"); + return false; + } + + if (IsBeingUsedInPictureInPictureMode()) { + MEDIACONTROL_LOG("Start listener because of being used in PiP mode"); + return true; + } + + if (IsInFullScreen()) { + MEDIACONTROL_LOG("Start listener because of being used in fullscreen"); + return true; + } + + // In order to filter out notification-ish sound, we use this pref to set the + // eligible media duration to prevent showing media control for those short + // sound. + if (Duration() < + StaticPrefs::media_mediacontrol_eligible_media_duration_s()) { + MEDIACONTROL_LOG("Not listening because media's duration %f is too short.", + Duration()); + return false; + } + + // This includes cases such like `video is muted`, `video has zero volume`, + // `video's audio track is still inaudible` and `tab is muted by audio channel + // (tab sound indicator)`, all these cases would make media inaudible. + // `ComputedVolume()` would return the final volume applied the affection made + // by audio channel, which is used to detect if the tab is muted by audio + // channel. + if (!IsAudible() || ComputedVolume() == 0.0f) { + MEDIACONTROL_LOG("Not listening because media is inaudible"); + return false; + } + return true; +} + +void HTMLMediaElement::StartMediaControlKeyListenerIfNeeded() { + if (!ShouldStartMediaControlKeyListener()) { + return; + } + mMediaControlKeyListener->Start(); +} + +void HTMLMediaElement::UpdateStreamName() { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoString aTitle; + OwnerDoc()->GetTitle(aTitle); + + if (mDecoder) { + mDecoder->SetStreamName(aTitle); + } +} + +void HTMLMediaElement::SetSecondaryMediaStreamRenderer( + VideoFrameContainer* aContainer, + FirstFrameVideoOutput* aFirstFrameOutput /* = nullptr */) { + MOZ_ASSERT(mSrcStream); + MOZ_ASSERT(mMediaStreamRenderer); + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Shutdown(); + mSecondaryMediaStreamRenderer = nullptr; + } + if (aContainer) { + mSecondaryMediaStreamRenderer = MakeAndAddRef<MediaStreamRenderer>( + AbstractMainThread(), aContainer, aFirstFrameOutput, this); + if (mSrcStreamIsPlaying) { + mSecondaryMediaStreamRenderer->Start(); + } + if (mSelectedVideoStreamTrack) { + mSecondaryMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack); + } + } +} + +void HTMLMediaElement::UpdateMediaControlAfterPictureInPictureModeChanged() { + if (IsBeingUsedInPictureInPictureMode()) { + // When media enters PIP mode, we should ensure that the listener has been + // started because we always want to control PIP video. + StartMediaControlKeyListenerIfNeeded(); + if (!mMediaControlKeyListener->IsStarted()) { + MEDIACONTROL_LOG("Failed to start listener when entering PIP mode"); + } + // Updating controller PIP state no matter the listener starts or not. + mMediaControlKeyListener->SetPictureInPictureModeEnabled(true); + } else { + mMediaControlKeyListener->SetPictureInPictureModeEnabled(false); + } +} + +bool HTMLMediaElement::IsBeingUsedInPictureInPictureMode() const { + if (!IsVideo()) { + return false; + } + return static_cast<const HTMLVideoElement*>(this)->IsCloningElementVisually(); +} + +void HTMLMediaElement::NodeInfoChanged(Document* aOldDoc) { + if (mMediaSource) { + OwnerDoc()->AddMediaElementWithMSE(); + aOldDoc->RemoveMediaElementWithMSE(); + } + + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); +} + +#ifdef MOZ_WMF_CDM +bool HTMLMediaElement::IsUsingWMFCDM() const { return mIsUsingWMFCDM; }; +#endif + +} // namespace mozilla::dom + +#undef LOG +#undef LOG_EVENT |