HTMLMediaElement.cpp   [plain text]


/*
 * Copyright (C) 2007-2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "HTMLMediaElement.h"

#if ENABLE(VIDEO)

#include "ApplicationCacheHost.h"
#include "ApplicationCacheResource.h"
#include "Attribute.h"
#include "Blob.h"
#include "CSSPropertyNames.h"
#include "CSSValueKeywords.h"
#include "ChromeClient.h"
#include "CommonVM.h"
#include "ContentSecurityPolicy.h"
#include "ContentType.h"
#include "CookieJar.h"
#include "DiagnosticLoggingClient.h"
#include "DiagnosticLoggingKeys.h"
#include "Document.h"
#include "DocumentLoader.h"
#include "ElementChildIterator.h"
#include "EventNames.h"
#include "FrameLoader.h"
#include "FrameLoaderClient.h"
#include "FrameView.h"
#include "HTMLParserIdioms.h"
#include "HTMLSourceElement.h"
#include "HTMLVideoElement.h"
#include "JSDOMError.h"
#include "JSDOMPromiseDeferred.h"
#include "JSHTMLMediaElement.h"
#include "Language.h"
#include "Logging.h"
#include "MIMETypeRegistry.h"
#include "MainFrame.h"
#include "MediaController.h"
#include "MediaControls.h"
#include "MediaDocument.h"
#include "MediaError.h"
#include "MediaFragmentURIParser.h"
#include "MediaList.h"
#include "MediaPlayer.h"
#include "MediaQueryEvaluator.h"
#include "MediaResourceLoader.h"
#include "NetworkingContext.h"
#include "NoEventDispatchAssertion.h"
#include "Page.h"
#include "PageGroup.h"
#include "PlatformMediaSessionManager.h"
#include "ProgressTracker.h"
#include "RenderLayerCompositor.h"
#include "RenderTheme.h"
#include "RenderVideo.h"
#include "RenderView.h"
#include "ResourceLoadInfo.h"
#include "ScriptController.h"
#include "ScriptSourceCode.h"
#include "SecurityOriginData.h"
#include "SecurityPolicy.h"
#include "SessionID.h"
#include "Settings.h"
#include "ShadowRoot.h"
#include "SleepDisabler.h"
#include "TimeRanges.h"
#include "UserContentController.h"
#include "UserGestureIndicator.h"
#include <limits>
#include <runtime/Uint8Array.h>
#include <wtf/Algorithms.h>
#include <wtf/CurrentTime.h>
#include <wtf/MathExtras.h>
#include <wtf/MemoryPressureHandler.h>
#include <wtf/Ref.h>
#include <wtf/text/CString.h>

#if ENABLE(VIDEO_TRACK)
#include "AudioTrackList.h"
#include "HTMLTrackElement.h"
#include "InbandGenericTextTrack.h"
#include "InbandTextTrackPrivate.h"
#include "InbandWebVTTTextTrack.h"
#include "RuntimeEnabledFeatures.h"
#include "TextTrackCueList.h"
#include "TextTrackList.h"
#include "VideoTrackList.h"
#endif

#if ENABLE(WEB_AUDIO)
#include "AudioSourceProvider.h"
#include "MediaElementAudioSourceNode.h"
#endif

#if PLATFORM(IOS)
#include "RuntimeApplicationChecks.h"
#include "WebVideoFullscreenInterfaceAVKit.h"
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
#include "WebKitPlaybackTargetAvailabilityEvent.h"
#endif

#if ENABLE(MEDIA_SESSION)
#include "MediaSession.h"
#endif

#if ENABLE(MEDIA_SOURCE)
#include "DOMWindow.h"
#include "MediaSource.h"
#include "VideoPlaybackQuality.h"
#endif

#if ENABLE(MEDIA_STREAM)
#include "DOMURL.h"
#include "MediaStream.h"
#include "MediaStreamRegistry.h"
#endif

#if ENABLE(LEGACY_ENCRYPTED_MEDIA)
#include "WebKitMediaKeyNeededEvent.h"
#include "WebKitMediaKeys.h"
#endif

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
#include "JSMediaControlsHost.h"
#include "MediaControlsHost.h"
#include <bindings/ScriptObject.h>
#endif

#if ENABLE(ENCRYPTED_MEDIA)
#include "NotImplemented.h"
#endif

#if PLATFORM(IOS) || (PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE))
#include "WebVideoFullscreenModel.h"
#endif

#define RELEASE_LOG_IF_ALLOWED(fmt, ...) RELEASE_LOG_IF(document().page() && document().page()->isAlwaysOnLoggingAllowed(), Media, "%p - HTMLMediaElement::" fmt, this, ##__VA_ARGS__)

namespace WebCore {

static const Seconds SeekRepeatDelay { 100_ms };
static const double SeekTime = 0.2;
static const Seconds ScanRepeatDelay { 1.5_s };
static const double ScanMaximumRate = 8;
static const double AutoplayInterferenceTimeThreshold = 10;

static const Seconds hideMediaControlsAfterEndedDelay { 6_s };

static void setFlags(unsigned& value, unsigned flags)
{
    value |= flags;
}

static void clearFlags(unsigned& value, unsigned flags)
{
    value &= ~flags;
}
    
#if !LOG_DISABLED
static String urlForLoggingMedia(const URL& url)
{
    static const unsigned maximumURLLengthForLogging = 128;

    if (url.string().length() < maximumURLLengthForLogging)
        return url.string();
    return url.string().substring(0, maximumURLLengthForLogging) + "...";
}

static const char* boolString(bool val)
{
    return val ? "true" : "false";
}

static String actionName(HTMLMediaElementEnums::DelayedActionType action)
{
    StringBuilder actionBuilder;

#define ACTION(_actionType) \
    if (action & (HTMLMediaElementEnums::_actionType)) { \
        if (!actionBuilder.isEmpty()) \
        actionBuilder.appendLiteral(", "); \
        actionBuilder.append(#_actionType); \
    } \

    ACTION(ConfigureTextTracks);
    ACTION(TextTrackChangesNotification);
    ACTION(ConfigureTextTrackDisplay);
    ACTION(CheckPlaybackTargetCompatablity);
    ACTION(CheckMediaState);
    ACTION(MediaEngineUpdated);

    return actionBuilder.toString();

#undef ACTION
}

#endif

#ifndef LOG_MEDIA_EVENTS
// Default to not logging events because so many are generated they can overwhelm the rest of 
// the logging.
#define LOG_MEDIA_EVENTS 0
#endif

#ifndef LOG_CACHED_TIME_WARNINGS
// Default to not logging warnings about excessive drift in the cached media time because it adds a
// fair amount of overhead and logging.
#define LOG_CACHED_TIME_WARNINGS 0
#endif

#if ENABLE(MEDIA_SOURCE)
// URL protocol used to signal that the media source API is being used.
static const char* mediaSourceBlobProtocol = "blob";
#endif

#if ENABLE(MEDIA_STREAM)
// URL protocol used to signal that the media stream API is being used.
static const char* mediaStreamBlobProtocol = "blob";
#endif

using namespace HTMLNames;

typedef HashMap<Document*, HashSet<HTMLMediaElement*>> DocumentElementSetMap;
static DocumentElementSetMap& documentToElementSetMap()
{
    static NeverDestroyed<DocumentElementSetMap> map;
    return map;
}

static void addElementToDocumentMap(HTMLMediaElement& element, Document& document)
{
    DocumentElementSetMap& map = documentToElementSetMap();
    HashSet<HTMLMediaElement*> set = map.take(&document);
    set.add(&element);
    map.add(&document, set);
}

static void removeElementFromDocumentMap(HTMLMediaElement& element, Document& document)
{
    DocumentElementSetMap& map = documentToElementSetMap();
    HashSet<HTMLMediaElement*> set = map.take(&document);
    set.remove(&element);
    if (!set.isEmpty())
        map.add(&document, set);
}

#if ENABLE(VIDEO_TRACK)

class TrackDisplayUpdateScope {
public:
    TrackDisplayUpdateScope(HTMLMediaElement& element)
        : m_element(element)
    {
        m_element.beginIgnoringTrackDisplayUpdateRequests();
    }
    ~TrackDisplayUpdateScope()
    {
        m_element.endIgnoringTrackDisplayUpdateRequests();
    }
    
private:
    HTMLMediaElement& m_element;
};

#endif

struct HTMLMediaElement::TrackGroup {
    enum GroupKind { CaptionsAndSubtitles, Description, Chapter, Metadata, Other };

    TrackGroup(GroupKind kind)
        : kind(kind)
    {
    }

    Vector<RefPtr<TextTrack>> tracks;
    RefPtr<TextTrack> visibleTrack;
    RefPtr<TextTrack> defaultTrack;
    GroupKind kind;
    bool hasSrcLang { false };
};

HashSet<HTMLMediaElement*>& HTMLMediaElement::allMediaElements()
{
    static NeverDestroyed<HashSet<HTMLMediaElement*>> elements;
    return elements;
}

#if ENABLE(MEDIA_SESSION)
typedef HashMap<uint64_t, HTMLMediaElement*> IDToElementMap;

static IDToElementMap& elementIDsToElements()
{
    static NeverDestroyed<IDToElementMap> map;
    return map;
}

HTMLMediaElement* HTMLMediaElement::elementWithID(uint64_t id)
{
    if (id == HTMLMediaElementInvalidID)
        return nullptr;
    
    return elementIDsToElements().get(id);
}

static uint64_t nextElementID()
{
    static uint64_t elementID = 0;
    return ++elementID;
}
#endif

struct MediaElementSessionInfo {
    const MediaElementSession* session;
    MediaElementSession::PlaybackControlsPurpose purpose;

    double timeOfLastUserInteraction;
    bool canShowControlsManager : 1;
    bool isVisibleInViewportOrFullscreen : 1;
    bool isLargeEnoughForMainContent : 1;
    bool isPlayingAudio : 1;
};

static MediaElementSessionInfo mediaElementSessionInfoForSession(const MediaElementSession& session, MediaElementSession::PlaybackControlsPurpose purpose)
{
    const HTMLMediaElement& element = session.element();
    return {
        &session,
        purpose,
        session.mostRecentUserInteractionTime(),
        session.canShowControlsManager(purpose),
        element.isFullscreen() || element.isVisibleInViewport(),
        session.isLargeEnoughForMainContent(MediaSessionMainContentPurpose::MediaControls),
        element.isPlaying() && element.hasAudio() && !element.muted()
    };
}

static bool preferMediaControlsForCandidateSessionOverOtherCandidateSession(const MediaElementSessionInfo& session, const MediaElementSessionInfo& otherSession)
{
    MediaElementSession::PlaybackControlsPurpose purpose = session.purpose;
    ASSERT(purpose == otherSession.purpose);

    // For the controls manager, prioritize visible media over offscreen media.
    if (purpose == MediaElementSession::PlaybackControlsPurpose::ControlsManager && session.isVisibleInViewportOrFullscreen != otherSession.isVisibleInViewportOrFullscreen)
        return session.isVisibleInViewportOrFullscreen;

    // For Now Playing, prioritize elements that would normally satisfy main content.
    if (purpose == MediaElementSession::PlaybackControlsPurpose::NowPlaying && session.isLargeEnoughForMainContent != otherSession.isLargeEnoughForMainContent)
        return session.isLargeEnoughForMainContent;

    // As a tiebreaker, prioritize elements that the user recently interacted with.
    return session.timeOfLastUserInteraction > otherSession.timeOfLastUserInteraction;
}

static bool mediaSessionMayBeConfusedWithMainContent(const MediaElementSessionInfo& session, MediaElementSession::PlaybackControlsPurpose purpose)
{
    if (purpose == MediaElementSession::PlaybackControlsPurpose::NowPlaying)
        return session.isPlayingAudio;

    if (!session.isVisibleInViewportOrFullscreen)
        return false;

    if (!session.isLargeEnoughForMainContent)
        return false;

    // Even if this video is not a candidate, if it is visible to the user and large enough
    // to be main content, it poses a risk for being confused with main content.
    return true;
}

HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& document, bool createdByParser)
    : HTMLElement(tagName, document)
    , ActiveDOMObject(&document)
    , m_weakFactory(this)
    , m_pendingActionTimer(*this, &HTMLMediaElement::pendingActionTimerFired)
    , m_progressEventTimer(*this, &HTMLMediaElement::progressEventTimerFired)
    , m_playbackProgressTimer(*this, &HTMLMediaElement::playbackProgressTimerFired)
    , m_scanTimer(*this, &HTMLMediaElement::scanTimerFired)
    , m_playbackControlsManagerBehaviorRestrictionsTimer(*this, &HTMLMediaElement::playbackControlsManagerBehaviorRestrictionsTimerFired)
    , m_seekToPlaybackPositionEndedTimer(*this, &HTMLMediaElement::seekToPlaybackPositionEndedTimerFired)
    , m_asyncEventQueue(*this)
    , m_lastTimeUpdateEventMovieTime(MediaTime::positiveInfiniteTime())
    , m_firstTimePlaying(true)
    , m_playing(false)
    , m_isWaitingUntilMediaCanStart(false)
    , m_shouldDelayLoadEvent(false)
    , m_haveFiredLoadedData(false)
    , m_inActiveDocument(true)
    , m_autoplaying(true)
    , m_muted(false)
    , m_explicitlyMuted(false)
    , m_initiallyMuted(false)
    , m_paused(true)
    , m_seeking(false)
    , m_seekRequested(false)
    , m_sentStalledEvent(false)
    , m_sentEndEvent(false)
    , m_pausedInternal(false)
    , m_closedCaptionsVisible(false)
    , m_webkitLegacyClosedCaptionOverride(false)
    , m_completelyLoaded(false)
    , m_havePreparedToPlay(false)
    , m_parsingInProgress(createdByParser)
    , m_elementIsHidden(document.hidden())
    , m_creatingControls(false)
    , m_receivedLayoutSizeChanged(false)
    , m_hasEverNotifiedAboutPlaying(false)
    , m_hasEverHadAudio(false)
    , m_hasEverHadVideo(false)
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    , m_mediaControlsDependOnPageScaleFactor(false)
    , m_haveSetUpCaptionContainer(false)
#endif
    , m_isScrubbingRemotely(false)
#if ENABLE(VIDEO_TRACK)
    , m_tracksAreReady(true)
    , m_haveVisibleTextTrack(false)
    , m_processingPreferenceChange(false)
#endif
{
    allMediaElements().add(this);

    LOG(Media, "HTMLMediaElement::HTMLMediaElement(%p)", this);
    setHasCustomStyleResolveCallbacks();
}

void HTMLMediaElement::finishInitialization()
{
    m_mediaSession = std::make_unique<MediaElementSession>(*this);

    m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureForFullscreen);
    m_mediaSession->addBehaviorRestriction(MediaElementSession::RequirePageConsentToLoadMedia);
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureToAutoplayToExternalDevice);
#endif
    m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureToControlControlsManager);
    m_mediaSession->addBehaviorRestriction(MediaElementSession::RequirePlaybackToControlControlsManager);

    auto& document = this->document();
    auto* page = document.page();

    if (document.settings().invisibleAutoplayNotPermitted())
        m_mediaSession->addBehaviorRestriction(MediaElementSession::InvisibleAutoplayNotPermitted);

    if (document.ownerElement() || !document.isMediaDocument()) {
        const auto& topDocument = document.topDocument();
        const bool isProcessingUserGesture = processingUserGestureForMedia();
        const bool shouldAudioPlaybackRequireUserGesture = topDocument.audioPlaybackRequiresUserGesture() && !isProcessingUserGesture;
        const bool shouldVideoPlaybackRequireUserGesture = topDocument.videoPlaybackRequiresUserGesture() && !isProcessingUserGesture;

        if (shouldVideoPlaybackRequireUserGesture) {
            m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureForVideoRateChange);
            if (document.settings().requiresUserGestureToLoadVideo())
                m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureForLoad);
        }

        if (page && page->isLowPowerModeEnabled())
            m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureForVideoDueToLowPowerMode);

        if (shouldAudioPlaybackRequireUserGesture)
            m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureForAudioRateChange);

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
        if (shouldVideoPlaybackRequireUserGesture || shouldAudioPlaybackRequireUserGesture)
            m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureToShowPlaybackTargetPicker);
#endif

        if (!document.settings().mediaDataLoadsAutomatically())
            m_mediaSession->addBehaviorRestriction(MediaElementSession::AutoPreloadingNotPermitted);

        if (document.settings().mainContentUserGestureOverrideEnabled())
            m_mediaSession->addBehaviorRestriction(MediaElementSession::OverrideUserGestureRequirementForMainContent);
    }

#if PLATFORM(IOS)
    if (!document.settings().videoPlaybackRequiresUserGesture() && !document.settings().audioPlaybackRequiresUserGesture()) {
        // Relax RequireUserGestureForFullscreen when videoPlaybackRequiresUserGesture and audioPlaybackRequiresUserGesture is not set:
        m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequireUserGestureForFullscreen);
    }
#endif

#if ENABLE(VIDEO_TRACK)
    if (page)
        m_captionDisplayMode = page->group().captionPreferences().captionDisplayMode();
#endif

#if ENABLE(MEDIA_SESSION)
    m_elementID = nextElementID();
    elementIDsToElements().add(m_elementID, this);

    setSessionInternal(document.defaultMediaSession());
#endif

    registerWithDocument(document);

#if USE(AUDIO_SESSION) && PLATFORM(MAC)
    AudioSession::sharedSession().addMutedStateObserver(this);
#endif

    mediaSession().clientWillBeginAutoplaying();
}

HTMLMediaElement::~HTMLMediaElement()
{
    LOG(Media, "HTMLMediaElement::~HTMLMediaElement(%p)", this);

    beginIgnoringTrackDisplayUpdateRequests();
    allMediaElements().remove(this);

    m_asyncEventQueue.close();

    setShouldDelayLoadEvent(false);
    unregisterWithDocument(document());

#if USE(AUDIO_SESSION) && PLATFORM(MAC)
    AudioSession::sharedSession().removeMutedStateObserver(this);
#endif

#if ENABLE(VIDEO_TRACK)
    if (m_audioTracks)
        m_audioTracks->clearElement();
    if (m_textTracks)
        m_textTracks->clearElement();
    if (m_videoTracks)
        m_videoTracks->clearElement();
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent)) {
        m_hasPlaybackTargetAvailabilityListeners = false;
        m_mediaSession->setHasPlaybackTargetAvailabilityListeners(*this, false);
        updateMediaState();
    }
#endif

    if (m_mediaController) {
        m_mediaController->removeMediaElement(*this);
        m_mediaController = nullptr;
    }

#if ENABLE(MEDIA_SOURCE)
    detachMediaSource();
#endif

#if ENABLE(LEGACY_ENCRYPTED_MEDIA)
    webkitSetMediaKeys(nullptr);
#endif

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (m_isolatedWorld)
        m_isolatedWorld->clearWrappers();
#endif

#if ENABLE(MEDIA_SESSION)
    if (m_session) {
        m_session->removeMediaElement(*this);
        m_session = nullptr;
    }

    elementIDsToElements().remove(m_elementID);
#endif

    m_seekTaskQueue.close();
    m_promiseTaskQueue.close();
    m_pauseAfterDetachedTaskQueue.close();
    m_updatePlaybackControlsManagerQueue.close();
    m_playbackControlsManagerBehaviorRestrictionsQueue.close();
    m_resourceSelectionTaskQueue.close();
    m_visibilityChangeTaskQueue.close();

    m_completelyLoaded = true;

    if (m_player) {
        m_player->invalidate();
        m_player = nullptr;
    }

    m_mediaSession = nullptr;
    updatePlaybackControlsManager();
}

static bool needsAutoplayPlayPauseEventsQuirk(const Document& document)
{
    auto* page = document.page();
    if (!page || !page->settings().needsSiteSpecificQuirks())
        return false;

    auto* loader = document.loader();
    return loader && loader->allowedAutoplayQuirks().contains(AutoplayQuirk::SynthesizedPauseEvents);
}

HTMLMediaElement* HTMLMediaElement::bestMediaElementForShowingPlaybackControlsManager(MediaElementSession::PlaybackControlsPurpose purpose)
{
    auto allSessions = PlatformMediaSessionManager::sharedManager().currentSessionsMatching([] (const PlatformMediaSession& session) {
        return is<MediaElementSession>(session);
    });

    Vector<MediaElementSessionInfo> candidateSessions;
    bool atLeastOneNonCandidateMayBeConfusedForMainContent = false;
    for (auto& session : allSessions) {
        auto mediaElementSessionInfo = mediaElementSessionInfoForSession(downcast<MediaElementSession>(*session), purpose);
        if (mediaElementSessionInfo.canShowControlsManager)
            candidateSessions.append(mediaElementSessionInfo);
        else if (mediaSessionMayBeConfusedWithMainContent(mediaElementSessionInfo, purpose))
            atLeastOneNonCandidateMayBeConfusedForMainContent = true;
    }

    if (!candidateSessions.size())
        return nullptr;

    std::sort(candidateSessions.begin(), candidateSessions.end(), preferMediaControlsForCandidateSessionOverOtherCandidateSession);
    auto strongestSessionCandidate = candidateSessions.first();
    if (!strongestSessionCandidate.isVisibleInViewportOrFullscreen && !strongestSessionCandidate.isPlayingAudio && atLeastOneNonCandidateMayBeConfusedForMainContent)
        return nullptr;

    return &strongestSessionCandidate.session->element();
}

void HTMLMediaElement::registerWithDocument(Document& document)
{
    m_mediaSession->registerWithDocument(document);

    if (m_isWaitingUntilMediaCanStart)
        document.addMediaCanStartListener(this);

#if !PLATFORM(IOS)
    document.registerForMediaVolumeCallbacks(this);
    document.registerForPrivateBrowsingStateChangedCallbacks(this);
#endif

    document.registerForVisibilityStateChangedCallbacks(this);

#if ENABLE(VIDEO_TRACK)
    if (m_requireCaptionPreferencesChangedCallbacks)
        document.registerForCaptionPreferencesChangedCallbacks(this);
#endif

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (m_mediaControlsDependOnPageScaleFactor)
        document.registerForPageScaleFactorChangedCallbacks(this);
    document.registerForUserInterfaceLayoutDirectionChangedCallbacks(*this);
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    document.registerForDocumentSuspensionCallbacks(this);
#endif

    document.registerForAllowsMediaDocumentInlinePlaybackChangedCallbacks(*this);

    document.addAudioProducer(this);
    addElementToDocumentMap(*this, document);

#if ENABLE(MEDIA_STREAM)
    document.registerForMediaStreamStateChangeCallbacks(*this);
#endif
}

void HTMLMediaElement::unregisterWithDocument(Document& document)
{
    m_mediaSession->unregisterWithDocument(document);

    if (m_isWaitingUntilMediaCanStart)
        document.removeMediaCanStartListener(this);

#if !PLATFORM(IOS)
    document.unregisterForMediaVolumeCallbacks(this);
    document.unregisterForPrivateBrowsingStateChangedCallbacks(this);
#endif

    document.unregisterForVisibilityStateChangedCallbacks(this);

#if ENABLE(VIDEO_TRACK)
    if (m_requireCaptionPreferencesChangedCallbacks)
        document.unregisterForCaptionPreferencesChangedCallbacks(this);
#endif

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (m_mediaControlsDependOnPageScaleFactor)
        document.unregisterForPageScaleFactorChangedCallbacks(this);
    document.unregisterForUserInterfaceLayoutDirectionChangedCallbacks(*this);
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    document.unregisterForDocumentSuspensionCallbacks(this);
#endif

    document.unregisterForAllowsMediaDocumentInlinePlaybackChangedCallbacks(*this);

    document.removeAudioProducer(this);
    removeElementFromDocumentMap(*this, document);

#if ENABLE(MEDIA_STREAM)
    document.unregisterForMediaStreamStateChangeCallbacks(*this);
#endif

}

void HTMLMediaElement::didMoveToNewDocument(Document& oldDocument, Document& newDocument)
{
    ASSERT_WITH_SECURITY_IMPLICATION(&document() == &newDocument);
    if (m_shouldDelayLoadEvent) {
        oldDocument.decrementLoadEventDelayCount();
        newDocument.incrementLoadEventDelayCount();
    }

    unregisterWithDocument(oldDocument);
    registerWithDocument(newDocument);

    HTMLElement::didMoveToNewDocument(oldDocument, newDocument);
    updateShouldAutoplay();
}

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
void HTMLMediaElement::prepareForDocumentSuspension()
{
    m_mediaSession->unregisterWithDocument(document());
}

void HTMLMediaElement::resumeFromDocumentSuspension()
{
    m_mediaSession->registerWithDocument(document());
    updateShouldAutoplay();
}
#endif

bool HTMLMediaElement::supportsFocus() const
{
    if (document().isMediaDocument())
        return false;

    // If no controls specified, we should still be able to focus the element if it has tabIndex.
    return controls() ||  HTMLElement::supportsFocus();
}

bool HTMLMediaElement::isMouseFocusable() const
{
    return false;
}

void HTMLMediaElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
{
    if (name == srcAttr) {
        // https://html.spec.whatwg.org/multipage/embedded-content.html#location-of-the-media-resource
        // Location of the Media Resource
        // 12 February 2017

        // If a src attribute of a media element is set or changed, the user
        // agent must invoke the media element's media element load algorithm.
#if PLATFORM(IOS)
        // Note, unless the restriction on requiring user action has been removed,
        // do not begin downloading data on iOS.
        if (!value.isNull() && m_mediaSession->dataLoadingPermitted(*this))
#else
        if (!value.isNull())
#endif
            prepareForLoad();
    } else if (name == controlsAttr)
        configureMediaControls();
    else if (name == loopAttr)
        updateSleepDisabling();
    else if (name == preloadAttr) {
        if (equalLettersIgnoringASCIICase(value, "none"))
            m_preload = MediaPlayer::None;
        else if (equalLettersIgnoringASCIICase(value, "metadata"))
            m_preload = MediaPlayer::MetaData;
        else {
            // The spec does not define an "invalid value default" but "auto" is suggested as the
            // "missing value default", so use it for everything except "none" and "metadata"
            m_preload = MediaPlayer::Auto;
        }

        // The attribute must be ignored if the autoplay attribute is present
        if (!autoplay() && !m_havePreparedToPlay && m_player)
            m_player->setPreload(m_mediaSession->effectivePreloadForElement(*this));

    } else if (name == mediagroupAttr)
        setMediaGroup(value);
    else if (name == autoplayAttr) {
        if (processingUserGestureForMedia())
            removeBehaviorsRestrictionsAfterFirstUserGesture();
    }
    else
        HTMLElement::parseAttribute(name, value);
}

void HTMLMediaElement::finishParsingChildren()
{
    HTMLElement::finishParsingChildren();
    m_parsingInProgress = false;
    
#if ENABLE(VIDEO_TRACK)
    if (descendantsOfType<HTMLTrackElement>(*this).first())
        scheduleDelayedAction(ConfigureTextTracks);
#endif
}

bool HTMLMediaElement::rendererIsNeeded(const RenderStyle& style)
{
    return controls() && HTMLElement::rendererIsNeeded(style);
}

RenderPtr<RenderElement> HTMLMediaElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition&)
{
    return createRenderer<RenderMedia>(*this, WTFMove(style));
}

bool HTMLMediaElement::childShouldCreateRenderer(const Node& child) const
{
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    return hasShadowRootParent(child) && HTMLElement::childShouldCreateRenderer(child);
#else
    if (!hasMediaControls())
        return false;
    // <media> doesn't allow its content, including shadow subtree, to
    // be rendered. So this should return false for most of the children.
    // One exception is a shadow tree built for rendering controls which should be visible.
    // So we let them go here by comparing its subtree root with one of the controls.
    return &mediaControls()->treeScope() == &child.treeScope()
        && hasShadowRootParent(child)
        && HTMLElement::childShouldCreateRenderer(child);
#endif
}

Node::InsertionNotificationRequest HTMLMediaElement::insertedInto(ContainerNode& insertionPoint)
{
    LOG(Media, "HTMLMediaElement::insertedInto(%p)", this);

    HTMLElement::insertedInto(insertionPoint);
    if (insertionPoint.isConnected()) {
        m_inActiveDocument = true;

#if PLATFORM(IOS)
        if (m_networkState == NETWORK_EMPTY && !attributeWithoutSynchronization(srcAttr).isEmpty() && m_mediaSession->dataLoadingPermitted(*this))
#else
        if (m_networkState == NETWORK_EMPTY && !attributeWithoutSynchronization(srcAttr).isEmpty())
#endif
            prepareForLoad();
    }

    if (!m_explicitlyMuted) {
        m_explicitlyMuted = true;
        m_muted = hasAttributeWithoutSynchronization(mutedAttr);
        m_mediaSession->canProduceAudioChanged();
    }

    return InsertionShouldCallFinishedInsertingSubtree;
}

void HTMLMediaElement::finishedInsertingSubtree()
{
    configureMediaControls();
}

void HTMLMediaElement::pauseAfterDetachedTask()
{
    // If we were re-inserted into an active document, no need to pause.
    if (m_inActiveDocument)
        return;

    if (hasMediaControls())
        mediaControls()->hide();
    if (m_networkState > NETWORK_EMPTY)
        pause();
    if (m_videoFullscreenMode != VideoFullscreenModeNone)
        exitFullscreen();

    if (!m_player)
        return;

    size_t extraMemoryCost = m_player->extraMemoryCost();
    if (extraMemoryCost > m_reportedExtraMemoryCost) {
        JSC::VM& vm = commonVM();
        JSC::JSLockHolder lock(vm);

        size_t extraMemoryCostDelta = extraMemoryCost - m_reportedExtraMemoryCost;
        m_reportedExtraMemoryCost = extraMemoryCost;
        // FIXME: Adopt reportExtraMemoryVisited, and switch to reportExtraMemoryAllocated.
        // https://bugs.webkit.org/show_bug.cgi?id=142595
        vm.heap.deprecatedReportExtraMemory(extraMemoryCostDelta);
    }
}

void HTMLMediaElement::removedFrom(ContainerNode& insertionPoint)
{
    LOG(Media, "HTMLMediaElement::removedFrom(%p)", this);

    m_inActiveDocument = false;
    if (insertionPoint.isConnected()) {
        // Pause asynchronously to let the operation that removed us finish, in case we get inserted back into a document.
        m_pauseAfterDetachedTaskQueue.enqueueTask(std::bind(&HTMLMediaElement::pauseAfterDetachedTask, this));
    }

    HTMLElement::removedFrom(insertionPoint);
}

void HTMLMediaElement::willAttachRenderers()
{
    ASSERT(!renderer());
}

inline void HTMLMediaElement::updateRenderer()
{
    if (auto* renderer = this->renderer())
        renderer->updateFromElement();
}

void HTMLMediaElement::didAttachRenderers()
{
    if (auto* renderer = this->renderer()) {
        renderer->updateFromElement();
        if (m_mediaSession && m_mediaSession->wantsToObserveViewportVisibilityForAutoplay())
            renderer->registerForVisibleInViewportCallback();
    }
    updateShouldAutoplay();
}

void HTMLMediaElement::willDetachRenderers()
{
    if (auto* renderer = this->renderer())
        renderer->unregisterForVisibleInViewportCallback();
}

void HTMLMediaElement::didDetachRenderers()
{
    updateShouldAutoplay();
}

void HTMLMediaElement::didRecalcStyle(Style::Change)
{
    updateRenderer();
}

void HTMLMediaElement::scheduleDelayedAction(DelayedActionType actionType)
{
    LOG(Media, "HTMLMediaElement::scheduleDelayedAction(%p) - setting %s flag", this, actionName(actionType).utf8().data());

#if ENABLE(VIDEO_TRACK)
    if (actionType & ConfigureTextTracks)
        setFlags(m_pendingActionFlags, ConfigureTextTracks);
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (actionType & CheckPlaybackTargetCompatablity)
        setFlags(m_pendingActionFlags, CheckPlaybackTargetCompatablity);
#endif

    if (actionType & CheckMediaState)
        setFlags(m_pendingActionFlags, CheckMediaState);

    if (actionType & MediaEngineUpdated)
        setFlags(m_pendingActionFlags, MediaEngineUpdated);

    if (actionType & UpdatePlayState)
        setFlags(m_pendingActionFlags, UpdatePlayState);

    m_pendingActionTimer.startOneShot(0_s);
}

void HTMLMediaElement::scheduleNextSourceChild()
{
    // Schedule the timer to try the next <source> element WITHOUT resetting state ala prepareForLoad.
    m_resourceSelectionTaskQueue.enqueueTask([this] {
        loadNextSourceChild();
    });
}

void HTMLMediaElement::mediaPlayerActiveSourceBuffersChanged(const MediaPlayer*)
{
    m_hasEverHadAudio |= hasAudio();
    m_hasEverHadVideo |= hasVideo();
}

void HTMLMediaElement::scheduleEvent(const AtomicString& eventName)
{
#if LOG_MEDIA_EVENTS
    LOG(Media, "HTMLMediaElement::scheduleEvent(%p) - scheduling '%s'", this, eventName.string().ascii().data());
#endif
    RefPtr<Event> event = Event::create(eventName, false, true);
    
    // Don't set the event target, the event queue will set it in GenericEventQueue::timerFired and setting it here
    // will trigger an ASSERT if this element has been marked for deletion.

    m_asyncEventQueue.enqueueEvent(WTFMove(event));
}

void HTMLMediaElement::scheduleResolvePendingPlayPromises()
{
    m_promiseTaskQueue.enqueueTask(std::bind(&HTMLMediaElement::resolvePendingPlayPromises, this));
}

void HTMLMediaElement::rejectPendingPlayPromises(DOMError& error)
{
    Vector<DOMPromiseDeferred<void>> pendingPlayPromises = WTFMove(m_pendingPlayPromises);

    for (auto& promise : pendingPlayPromises)
        promise.rejectType<IDLInterface<DOMError>>(error);
}

void HTMLMediaElement::resolvePendingPlayPromises()
{
    Vector<DOMPromiseDeferred<void>> pendingPlayPromises = WTFMove(m_pendingPlayPromises);

    for (auto& promise : pendingPlayPromises)
        promise.resolve();
}

void HTMLMediaElement::scheduleNotifyAboutPlaying()
{
    m_promiseTaskQueue.enqueueTask(std::bind(&HTMLMediaElement::notifyAboutPlaying, this));
}

void HTMLMediaElement::notifyAboutPlaying()
{
    Ref<HTMLMediaElement> protectedThis(*this); // The 'playing' event can make arbitrary DOM mutations.
    m_playbackStartedTime = currentMediaTime().toDouble();
    dispatchEvent(Event::create(eventNames().playingEvent, false, true));
    resolvePendingPlayPromises();

    m_hasEverNotifiedAboutPlaying = true;
    scheduleUpdatePlaybackControlsManager();
}

bool HTMLMediaElement::hasEverNotifiedAboutPlaying() const
{
    return m_hasEverNotifiedAboutPlaying;
}

void HTMLMediaElement::pendingActionTimerFired()
{
    Ref<HTMLMediaElement> protectedThis(*this); // loadNextSourceChild may fire 'beforeload', which can make arbitrary DOM mutations.
    PendingActionFlags pendingActions = m_pendingActionFlags;
    m_pendingActionFlags = 0;

#if ENABLE(VIDEO_TRACK)
    if (pendingActions & ConfigureTextTracks)
        configureTextTracks();
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (pendingActions & CheckPlaybackTargetCompatablity && m_isPlayingToWirelessTarget && !m_player->canPlayToWirelessPlaybackTarget()) {
        LOG(Media, "HTMLMediaElement::pendingActionTimerFired(%p) - calling setShouldPlayToPlaybackTarget(false)", this);
        m_failedToPlayToWirelessTarget = true;
        m_player->setShouldPlayToPlaybackTarget(false);
    }

    if (pendingActions & CheckMediaState)
        updateMediaState();
#endif

    if (pendingActions & MediaEngineUpdated)
        mediaEngineWasUpdated();

    if (pendingActions & UpdatePlayState)
        updatePlayState();
}

MediaError* HTMLMediaElement::error() const
{
    return m_error.get();
}

void HTMLMediaElement::setSrcObject(MediaProvider&& mediaProvider)
{
    // FIXME: Setting the srcObject attribute may cause other changes to the media element's internal state:
    // Specifically, if srcObject is specified, the UA must use it as the source of media, even if the src
    // attribute is also set or children are present. If the value of srcObject is replaced or set to null
    // the UA must re-run the media element load algorithm.
    //
    // https://bugs.webkit.org/show_bug.cgi?id=124896


    // https://www.w3.org/TR/html51/semantics-embedded-content.html#dom-htmlmediaelement-srcobject
    // 4.7.14.2. Location of the media resource
    // srcObject: On setting, it must set the element’s assigned media provider object to the new
    // value, and then invoke the element’s media element load algorithm.
    m_mediaProvider = WTFMove(mediaProvider);
    prepareForLoad();
}

void HTMLMediaElement::setCrossOrigin(const AtomicString& value)
{
    setAttributeWithoutSynchronization(crossoriginAttr, value);
}

String HTMLMediaElement::crossOrigin() const
{
    return parseCORSSettingsAttribute(attributeWithoutSynchronization(crossoriginAttr));
}

HTMLMediaElement::NetworkState HTMLMediaElement::networkState() const
{
    return m_networkState;
}

String HTMLMediaElement::canPlayType(const String& mimeType) const
{
    MediaEngineSupportParameters parameters;
    ContentType contentType(mimeType);
    parameters.type = contentType;
    parameters.contentTypesRequiringHardwareSupport = mediaContentTypesRequiringHardwareSupport();
    MediaPlayer::SupportsType support = MediaPlayer::supportsType(parameters, this);
    String canPlay;

    // 4.8.10.3
    switch (support)
    {
        case MediaPlayer::IsNotSupported:
            canPlay = emptyString();
            break;
        case MediaPlayer::MayBeSupported:
            canPlay = ASCIILiteral("maybe");
            break;
        case MediaPlayer::IsSupported:
            canPlay = ASCIILiteral("probably");
            break;
    }
    
    LOG(Media, "HTMLMediaElement::canPlayType(%p) - [%s] -> %s", this, mimeType.utf8().data(), canPlay.utf8().data());

    return canPlay;
}

double HTMLMediaElement::getStartDate() const
{
    if (!m_player)
        return std::numeric_limits<double>::quiet_NaN();
    return m_player->getStartDate().toDouble();
}

void HTMLMediaElement::load()
{
    Ref<HTMLMediaElement> protectedThis(*this); // prepareForLoad may result in a 'beforeload' event, which can make arbitrary DOM mutations.
    
    LOG(Media, "HTMLMediaElement::load(%p)", this);
    
    if (!m_mediaSession->dataLoadingPermitted(*this))
        return;
    if (processingUserGestureForMedia())
        removeBehaviorsRestrictionsAfterFirstUserGesture();

    prepareForLoad();
    m_resourceSelectionTaskQueue.enqueueTask([this] {
        prepareToPlay();
    });
}

void HTMLMediaElement::prepareForLoad()
{
    // https://html.spec.whatwg.org/multipage/embedded-content.html#media-element-load-algorithm
    // The Media Element Load Algorithm
    // 12 February 2017

    LOG(Media, "HTMLMediaElement::prepareForLoad(%p)", this);

    // 1 - Abort any already-running instance of the resource selection algorithm for this element.
    // Perform the cleanup required for the resource load algorithm to run.
    stopPeriodicTimers();
    m_pendingActionTimer.stop();
    m_resourceSelectionTaskQueue.cancelAllTasks();
    // FIXME: Figure out appropriate place to reset LoadTextTrackResource if necessary and set m_pendingActionFlags to 0 here.
    m_sentEndEvent = false;
    m_sentStalledEvent = false;
    m_haveFiredLoadedData = false;
    m_completelyLoaded = false;
    m_havePreparedToPlay = false;
    m_displayMode = Unknown;
    m_currentSrc = URL();

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    m_failedToPlayToWirelessTarget = false;
#endif

    m_loadState = WaitingForSource;
    m_currentSourceNode = nullptr;

    createMediaPlayer();

    // 2 - Let pending tasks be a list of all tasks from the media element's media element event task source in one of the task queues.
    // 3 - For each task in pending tasks that would resolve pending play promises or reject pending play promises, immediately resolve or reject those promises in the order the corresponding tasks were queued.
    // 4 - Remove each task in pending tasks from its task queue
    cancelPendingEventsAndCallbacks();

    // 5 - If the media element's networkState is set to NETWORK_LOADING or NETWORK_IDLE, queue
    // a task to fire a simple event named abort at the media element.
    if (m_networkState == NETWORK_LOADING || m_networkState == NETWORK_IDLE)
        scheduleEvent(eventNames().abortEvent);

    // 6 - If the media element's networkState is not set to NETWORK_EMPTY, then run these substeps
    if (m_networkState != NETWORK_EMPTY) {
        // 6.1 - Queue a task to fire a simple event named emptied at the media element.
        scheduleEvent(eventNames().emptiedEvent);

        // 6.2 - If a fetching process is in progress for the media element, the user agent should stop it.
        m_networkState = NETWORK_EMPTY;

        // 6.3 - If the media element’s assigned media provider object is a MediaSource object, then detach it.
#if ENABLE(MEDIA_SOURCE)
        detachMediaSource();
#endif

        // 6.4 - Forget the media element's media-resource-specific tracks.
        forgetResourceSpecificTracks();

        // 6.5 - If readyState is not set to HAVE_NOTHING, then set it to that state.
        m_readyState = HAVE_NOTHING;
        m_readyStateMaximum = HAVE_NOTHING;

        // 6.6 - If the paused attribute is false, then set it to true.
        m_paused = true;

        // 6.7 - If seeking is true, set it to false.
        clearSeeking();

        // 6.8 - Set the current playback position to 0.
        //       Set the official playback position to 0.
        //       If this changed the official playback position, then queue a task to fire a simple event named timeupdate at the media element.
        m_lastSeekTime = MediaTime::zeroTime();
        m_playedTimeRanges = TimeRanges::create();
        // FIXME: Add support for firing this event. e.g., scheduleEvent(eventNames().timeUpdateEvent);

        // 4.9 - Set the initial playback position to 0.
        // FIXME: Make this less subtle. The position only becomes 0 because of the createMediaPlayer() call
        // above.
        refreshCachedTime();

        invalidateCachedTime();

        // 4.10 - Set the timeline offset to Not-a-Number (NaN).
        // 4.11 - Update the duration attribute to Not-a-Number (NaN).

        updateMediaController();
#if ENABLE(VIDEO_TRACK)
        updateActiveTextTrackCues(MediaTime::zeroTime());
#endif
    }

    // 7 - Set the playbackRate attribute to the value of the defaultPlaybackRate attribute.
    setPlaybackRate(defaultPlaybackRate());

    // 8 - Set the error attribute to null and the autoplaying flag to true.
    m_error = nullptr;
    m_autoplaying = true;
    mediaSession().clientWillBeginAutoplaying();

    // 9 - Invoke the media element's resource selection algorithm.
    selectMediaResource();

    // 10 - Note: Playback of any previously playing media resource for this element stops.

    configureMediaControls();
}

void HTMLMediaElement::selectMediaResource()
{
    // https://www.w3.org/TR/2016/REC-html51-20161101/semantics-embedded-content.html#resource-selection-algorithm
    // The Resource Selection Algorithm

    // 1. Set the element’s networkState attribute to the NETWORK_NO_SOURCE value.
    m_networkState = NETWORK_NO_SOURCE;

    // 2. Set the element’s show poster flag to true.
    setDisplayMode(Poster);

    // 3. Set the media element’s delaying-the-load-event flag to true (this delays the load event).
    setShouldDelayLoadEvent(true);

    // 4. in parallel await a stable state, allowing the task that invoked this algorithm to continue.
    if (m_resourceSelectionTaskQueue.hasPendingTasks())
        return;

    if (!m_mediaSession->pageAllowsDataLoading(*this)) {
        LOG(Media, "HTMLMediaElement::selectMediaResource(%p) - not allowed to load in background, waiting", this);
        setShouldDelayLoadEvent(false);
        if (m_isWaitingUntilMediaCanStart)
            return;
        m_isWaitingUntilMediaCanStart = true;
        document().addMediaCanStartListener(this);
        return;
    }

    // Once the page has allowed an element to load media, it is free to load at will. This allows a
    // playlist that starts in a foreground tab to continue automatically if the tab is subsequently
    // put into the background.
    m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequirePageConsentToLoadMedia);


    m_resourceSelectionTaskQueue.enqueueTask([this]  {
        // 5. If the media element’s blocked-on-parser flag is false, then populate the list of pending text tracks.
#if ENABLE(VIDEO_TRACK)
        if (hasMediaControls())
            mediaControls()->changedClosedCaptionsVisibility();

        // HTMLMediaElement::textTracksAreReady will need "... the text tracks whose mode was not in the
        // disabled state when the element's resource selection algorithm last started".
        // FIXME: Update this to match "populate the list of pending text tracks" step.
        m_textTracksWhenResourceSelectionBegan.clear();
        if (m_textTracks) {
            for (unsigned i = 0; i < m_textTracks->length(); ++i) {
                TextTrack* track = m_textTracks->item(i);
                if (track->mode() != TextTrack::Mode::Disabled)
                    m_textTracksWhenResourceSelectionBegan.append(track);
            }
        }
#endif

        enum Mode { None, Object, Attribute, Children };
        Mode mode = None;

        if (m_mediaProvider) {
            // 6. If the media element has an assigned media provider object, then let mode be object.
            mode = Object;
        } else if (hasAttributeWithoutSynchronization(srcAttr)) {
            //    Otherwise, if the media element has no assigned media provider object but has a src attribute, then let mode be attribute.
            mode = Attribute;
            ASSERT(m_player);
            if (!m_player) {
                RELEASE_LOG_ERROR(Media, "HTMLMediaElement::selectMediaResource(%p) - has srcAttr but m_player is not created", this);
                return;
            }
        } else if (auto firstSource = childrenOfType<HTMLSourceElement>(*this).first()) {
            //    Otherwise, if the media element does not have an assigned media provider object and does not have a src attribute,
            //    but does have a source element child, then let mode be children and let candidate be the first such source element
            //    child in tree order.
            mode = Children;
            m_nextChildNodeToConsider = firstSource;
            m_currentSourceNode = nullptr;
        } else {
            //  Otherwise the media element has no assigned media provider object and has neither a src attribute nor a source
            //  element child: set the networkState to NETWORK_EMPTY, and abort these steps; the synchronous section ends.
            m_loadState = WaitingForSource;
            setShouldDelayLoadEvent(false);
            m_networkState = NETWORK_EMPTY;

            LOG(Media, "HTMLMediaElement::selectMediaResource(%p) - nothing to load", this);
            return;
        }

        // 7. Set the media element’s networkState to NETWORK_LOADING.
        m_networkState = NETWORK_LOADING;

        // 8. Queue a task to fire a simple event named loadstart at the media element.
        scheduleEvent(eventNames().loadstartEvent);

        // 9. Run the appropriate steps from the following list:
        // ↳ If mode is object
        if (mode == Object) {
            // 1. Set the currentSrc attribute to the empty string.
            m_currentSrc = URL();

            // 2. End the synchronous section, continuing the remaining steps in parallel.
            // 3. Run the resource fetch algorithm with the assigned media provider object.
            WTF::visit(WTF::makeVisitor(
#if ENABLE(MEDIA_STREAM)
                [this](RefPtr<MediaStream> stream) { m_mediaStreamSrcObject = stream; },
#endif
#if ENABLE(MEDIA_SOURCE)
                [this](RefPtr<MediaSource> source) { m_mediaSource = source; },
#endif
                [this](RefPtr<Blob> blob) { m_blob = blob; }
            ), m_mediaProvider.value());

            ContentType contentType;
            loadResource(URL(), contentType, String());
            LOG(Media, "HTMLMediaElement::selectMediaResource(%p) - using 'srcObject' property", this);

            //    If that algorithm returns without aborting this one, then the load failed.
            // 4. Failed with media provider: Reaching this step indicates that the media resource
            //    failed to load. Queue a task to run the dedicated media source failure steps.
            // 5. Wait for the task queued by the previous step to have executed.
            // 6. Abort these steps. The element won’t attempt to load another resource until this
            //    algorithm is triggered again.
            return;
        }

        // ↳ If mode is attribute
        if (mode == Attribute) {
            m_loadState = LoadingFromSrcAttr;

            // 1. If the src attribute’s value is the empty string, then end the synchronous section,
            //    and jump down to the failed with attribute step below.
            // 2. Let absolute URL be the absolute URL that would have resulted from parsing the URL
            //    specified by the src attribute’s value relative to the media element when the src
            //    attribute was last changed.
            URL absoluteURL = getNonEmptyURLAttribute(srcAttr);
            if (absoluteURL.isEmpty()) {
                mediaLoadingFailed(MediaPlayer::FormatError);
                LOG(Media, "HTMLMediaElement::selectMediaResource(%p) -  empty 'src'", this);
                return;
            }

            if (!isSafeToLoadURL(absoluteURL, Complain) || !dispatchBeforeLoadEvent(absoluteURL.string())) {
                mediaLoadingFailed(MediaPlayer::FormatError);
                return;
            }

            // 3. If absolute URL was obtained successfully, set the currentSrc attribute to absolute URL.
            m_currentSrc = absoluteURL;

            // 4. End the synchronous section, continuing the remaining steps in parallel.
            // 5. If absolute URL was obtained successfully, run the resource fetch algorithm with absolute
            //    URL. If that algorithm returns without aborting this one, then the load failed.

            // No type or key system information is available when the url comes
            // from the 'src' attribute so MediaPlayer
            // will have to pick a media engine based on the file extension.
            ContentType contentType;
            loadResource(absoluteURL, contentType, String());
            LOG(Media, "HTMLMediaElement::selectMediaResource(%p) - using 'src' attribute url", this);

            // 6. Failed with attribute: Reaching this step indicates that the media resource failed to load
            //    or that the given URL could not be resolved. Queue a task to run the dedicated media source failure steps.
            // 7. Wait for the task queued by the previous step to have executed.
            // 8. Abort these steps. The element won’t attempt to load another resource until this algorithm is triggered again.
            return;
        }

        // ↳ Otherwise (mode is children)
        // (Ctd. in loadNextSourceChild())
        loadNextSourceChild();
    });
}

void HTMLMediaElement::loadNextSourceChild()
{
    ContentType contentType;
    String keySystem;
    URL mediaURL = selectNextSourceChild(&contentType, &keySystem, Complain);
    if (!mediaURL.isValid()) {
        waitForSourceChange();
        return;
    }

    // Recreate the media player for the new url
    createMediaPlayer();

    m_loadState = LoadingFromSourceElement;
    loadResource(mediaURL, contentType, keySystem);
}

void HTMLMediaElement::loadResource(const URL& initialURL, ContentType& contentType, const String& keySystem)
{
    ASSERT(initialURL.isEmpty() || isSafeToLoadURL(initialURL, Complain));

    LOG(Media, "HTMLMediaElement::loadResource(%p) - %s, %s, %s", this, urlForLoggingMedia(initialURL).utf8().data(), contentType.raw().utf8().data(), keySystem.utf8().data());

    Frame* frame = document().frame();
    if (!frame) {
        mediaLoadingFailed(MediaPlayer::FormatError);
        return;
    }

    Page* page = frame->page();
    if (!page) {
        mediaLoadingFailed(MediaPlayer::FormatError);
        return;
    }

    URL url = initialURL;
    if (!url.isEmpty() && !frame->loader().willLoadMediaElementURL(url)) {
        mediaLoadingFailed(MediaPlayer::FormatError);
        return;
    }

#if ENABLE(CONTENT_EXTENSIONS)
    if (auto* documentLoader = frame->loader().documentLoader()) {
        if (page->userContentProvider().processContentExtensionRulesForLoad(url, ResourceType::Media, *documentLoader).blockedLoad) {
            mediaLoadingFailed(MediaPlayer::FormatError);
            return;
        }
    }
#endif

    // The resource fetch algorithm 
    m_networkState = NETWORK_LOADING;

    // If the URL should be loaded from the application cache, pass the URL of the cached file to the media engine.
    ApplicationCacheResource* resource = nullptr;
    if (!url.isEmpty() && frame->loader().documentLoader()->applicationCacheHost().shouldLoadResourceFromApplicationCache(ResourceRequest(url), resource)) {
        // Resources that are not present in the manifest will always fail to load (at least, after the
        // cache has been primed the first time), making the testing of offline applications simpler.
        if (!resource || resource->path().isEmpty()) {
            mediaLoadingFailed(MediaPlayer::NetworkError);
            return;
        }
    }

    // Log that we started loading a media element.
    page->diagnosticLoggingClient().logDiagnosticMessage(isVideo() ? DiagnosticLoggingKeys::videoKey() : DiagnosticLoggingKeys::audioKey(), DiagnosticLoggingKeys::loadingKey(), ShouldSample::No);

    m_firstTimePlaying = true;

    // Set m_currentSrc *before* changing to the cache URL, the fact that we are loading from the app
    // cache is an internal detail not exposed through the media element API.
    m_currentSrc = url;

    if (resource) {
        url = ApplicationCacheHost::createFileURL(resource->path());
        LOG(Media, "HTMLMediaElement::loadResource(%p) - will load from app cache -> %s", this, urlForLoggingMedia(url).utf8().data());
    }

    LOG(Media, "HTMLMediaElement::loadResource(%p) - m_currentSrc -> %s", this, urlForLoggingMedia(m_currentSrc).utf8().data());

    startProgressEventTimer();

    bool privateMode = document().page() && document().page()->usesEphemeralSession();
    m_player->setPrivateBrowsingMode(privateMode);

    // Reset display mode to force a recalculation of what to show because we are resetting the player.
    setDisplayMode(Unknown);

    if (!autoplay() && !m_havePreparedToPlay)
        m_player->setPreload(m_mediaSession->effectivePreloadForElement(*this));
    m_player->setPreservesPitch(m_webkitPreservesPitch);

    if (!m_explicitlyMuted) {
        m_explicitlyMuted = true;
        m_muted = hasAttributeWithoutSynchronization(mutedAttr);
        m_mediaSession->canProduceAudioChanged();
    }

    updateVolume();

    bool loadAttempted = false;
#if ENABLE(MEDIA_SOURCE)
    if (!m_mediaSource && url.protocolIs(mediaSourceBlobProtocol))
        m_mediaSource = MediaSource::lookup(url.string());

    if (m_mediaSource) {
        loadAttempted = true;
        if (!m_mediaSource->attachToElement(*this) || !m_player->load(url, contentType, m_mediaSource.get())) {
            // Forget our reference to the MediaSource, so we leave it alone
            // while processing remainder of load failure.
            m_mediaSource = nullptr;
            mediaLoadingFailed(MediaPlayer::FormatError);
        }
    }
#endif

#if ENABLE(MEDIA_STREAM)
    if (!loadAttempted) {
        if (!m_mediaStreamSrcObject && url.protocolIs(mediaStreamBlobProtocol))
            m_mediaStreamSrcObject = MediaStreamRegistry::shared().lookUp(url);

        if (m_mediaStreamSrcObject) {
            loadAttempted = true;
            if (!m_player->load(m_mediaStreamSrcObject->privateStream()))
                mediaLoadingFailed(MediaPlayer::FormatError);
        }
    }
#endif

    if (!loadAttempted && m_blob) {
        loadAttempted = true;
        if (!m_player->load(m_blob->url(), contentType, keySystem))
            mediaLoadingFailed(MediaPlayer::FormatError);
    }

    if (!loadAttempted && !m_player->load(url, contentType, keySystem))
        mediaLoadingFailed(MediaPlayer::FormatError);

    // If there is no poster to display, allow the media engine to render video frames as soon as
    // they are available.
    updateDisplayState();

    updateRenderer();
}

#if ENABLE(VIDEO_TRACK)

static bool trackIndexCompare(TextTrack* a, TextTrack* b)
{
    return a->trackIndex() - b->trackIndex() < 0;
}

static bool eventTimeCueCompare(const std::pair<MediaTime, TextTrackCue*>& a, const std::pair<MediaTime, TextTrackCue*>& b)
{
    // 12 - Sort the tasks in events in ascending time order (tasks with earlier
    // times first).
    if (a.first != b.first)
        return a.first - b.first < MediaTime::zeroTime();

    // If the cues belong to different text tracks, it doesn't make sense to
    // compare the two tracks by the relative cue order, so return the relative
    // track order.
    if (a.second->track() != b.second->track())
        return trackIndexCompare(a.second->track(), b.second->track());

    // 12 - Further sort tasks in events that have the same time by the
    // relative text track cue order of the text track cues associated
    // with these tasks.
    return a.second->isOrderedBefore(b.second);
}

static bool compareCueInterval(const CueInterval& one, const CueInterval& two)
{
    return one.data()->isOrderedBefore(two.data());
}

void HTMLMediaElement::updateActiveTextTrackCues(const MediaTime& movieTime)
{
    // 4.8.10.8 Playing the media resource

    //  If the current playback position changes while the steps are running,
    //  then the user agent must wait for the steps to complete, and then must
    //  immediately rerun the steps.
    if (ignoreTrackDisplayUpdateRequests())
        return;

    LOG(Media, "HTMLMediaElement::updateActiveTextTrackCues(%p)", this);

    // 1 - Let current cues be a list of cues, initialized to contain all the
    // cues of all the hidden, showing, or showing by default text tracks of the
    // media element (not the disabled ones) whose start times are less than or
    // equal to the current playback position and whose end times are greater
    // than the current playback position.
    CueList currentCues;

    // The user agent must synchronously unset [the text track cue active] flag
    // whenever ... the media element's readyState is changed back to HAVE_NOTHING.
    if (m_readyState != HAVE_NOTHING && m_player) {
        currentCues = m_cueTree.allOverlaps(m_cueTree.createInterval(movieTime, movieTime));
        if (currentCues.size() > 1)
            std::sort(currentCues.begin(), currentCues.end(), &compareCueInterval);
    }

    CueList previousCues;
    CueList missedCues;

    // 2 - Let other cues be a list of cues, initialized to contain all the cues
    // of hidden, showing, and showing by default text tracks of the media
    // element that are not present in current cues.
    previousCues = m_currentlyActiveCues;

    // 3 - Let last time be the current playback position at the time this
    // algorithm was last run for this media element, if this is not the first
    // time it has run.
    MediaTime lastTime = m_lastTextTrackUpdateTime;

    // 4 - If the current playback position has, since the last time this
    // algorithm was run, only changed through its usual monotonic increase
    // during normal playback, then let missed cues be the list of cues in other
    // cues whose start times are greater than or equal to last time and whose
    // end times are less than or equal to the current playback position.
    // Otherwise, let missed cues be an empty list.
    if (lastTime >= MediaTime::zeroTime() && m_lastSeekTime < movieTime) {
        for (auto& cue : m_cueTree.allOverlaps(m_cueTree.createInterval(lastTime, movieTime))) {
            // Consider cues that may have been missed since the last seek time.
            if (cue.low() > std::max(m_lastSeekTime, lastTime) && cue.high() < movieTime)
                missedCues.append(cue);
        }
    }

    m_lastTextTrackUpdateTime = movieTime;

    // 5 - If the time was reached through the usual monotonic increase of the
    // current playback position during normal playback, and if the user agent
    // has not fired a timeupdate event at the element in the past 15 to 250ms
    // and is not still running event handlers for such an event, then the user
    // agent must queue a task to fire a simple event named timeupdate at the
    // element. (In the other cases, such as explicit seeks, relevant events get
    // fired as part of the overall process of changing the current playback
    // position.)
    if (!m_paused && m_lastSeekTime <= lastTime)
        scheduleTimeupdateEvent(false);

    // Explicitly cache vector sizes, as their content is constant from here.
    size_t currentCuesSize = currentCues.size();
    size_t missedCuesSize = missedCues.size();
    size_t previousCuesSize = previousCues.size();

    // 6 - If all of the cues in current cues have their text track cue active
    // flag set, none of the cues in other cues have their text track cue active
    // flag set, and missed cues is empty, then abort these steps.
    bool activeSetChanged = missedCuesSize;

    for (size_t i = 0; !activeSetChanged && i < previousCuesSize; ++i)
        if (!currentCues.contains(previousCues[i]) && previousCues[i].data()->isActive())
            activeSetChanged = true;

    for (size_t i = 0; i < currentCuesSize; ++i) {
        TextTrackCue* cue = currentCues[i].data();

        if (cue->isRenderable())
            toVTTCue(cue)->updateDisplayTree(movieTime);

        if (!cue->isActive())
            activeSetChanged = true;
    }

    if (!activeSetChanged)
        return;

    // 7 - If the time was reached through the usual monotonic increase of the
    // current playback position during normal playback, and there are cues in
    // other cues that have their text track cue pause-on-exi flag set and that
    // either have their text track cue active flag set or are also in missed
    // cues, then immediately pause the media element.
    for (size_t i = 0; !m_paused && i < previousCuesSize; ++i) {
        if (previousCues[i].data()->pauseOnExit()
            && previousCues[i].data()->isActive()
            && !currentCues.contains(previousCues[i]))
            pause();
    }

    for (size_t i = 0; !m_paused && i < missedCuesSize; ++i) {
        if (missedCues[i].data()->pauseOnExit())
            pause();
    }

    // 8 - Let events be a list of tasks, initially empty. Each task in this
    // list will be associated with a text track, a text track cue, and a time,
    // which are used to sort the list before the tasks are queued.
    Vector<std::pair<MediaTime, TextTrackCue*>> eventTasks;

    // 8 - Let affected tracks be a list of text tracks, initially empty.
    Vector<TextTrack*> affectedTracks;

    for (size_t i = 0; i < missedCuesSize; ++i) {
        // 9 - For each text track cue in missed cues, prepare an event named enter
        // for the TextTrackCue object with the text track cue start time.
        eventTasks.append({ missedCues[i].data()->startMediaTime(), missedCues[i].data() });

        // 10 - For each text track [...] in missed cues, prepare an event
        // named exit for the TextTrackCue object with the  with the later of
        // the text track cue end time and the text track cue start time.

        // Note: An explicit task is added only if the cue is NOT a zero or
        // negative length cue. Otherwise, the need for an exit event is
        // checked when these tasks are actually queued below. This doesn't
        // affect sorting events before dispatch either, because the exit
        // event has the same time as the enter event.
        if (missedCues[i].data()->startMediaTime() < missedCues[i].data()->endMediaTime())
            eventTasks.append({ missedCues[i].data()->endMediaTime(), missedCues[i].data() });
    }

    for (size_t i = 0; i < previousCuesSize; ++i) {
        // 10 - For each text track cue in other cues that has its text
        // track cue active flag set prepare an event named exit for the
        // TextTrackCue object with the text track cue end time.
        if (!currentCues.contains(previousCues[i]))
            eventTasks.append({ previousCues[i].data()->endMediaTime(), previousCues[i].data() });
    }

    for (size_t i = 0; i < currentCuesSize; ++i) {
        // 11 - For each text track cue in current cues that does not have its
        // text track cue active flag set, prepare an event named enter for the
        // TextTrackCue object with the text track cue start time.
        if (!previousCues.contains(currentCues[i]))
            eventTasks.append({ currentCues[i].data()->startMediaTime(), currentCues[i].data() });
    }

    // 12 - Sort the tasks in events in ascending time order (tasks with earlier
    // times first).
    std::sort(eventTasks.begin(), eventTasks.end(), eventTimeCueCompare);

    for (auto& eventTask : eventTasks) {
        if (!affectedTracks.contains(eventTask.second->track()))
            affectedTracks.append(eventTask.second->track());

        // 13 - Queue each task in events, in list order.

        // Each event in eventTasks may be either an enterEvent or an exitEvent,
        // depending on the time that is associated with the event. This
        // correctly identifies the type of the event, if the startTime is
        // less than the endTime in the cue.
        if (eventTask.second->startTime() >= eventTask.second->endTime()) {
            auto enterEvent = Event::create(eventNames().enterEvent, false, false);
            enterEvent->setTarget(eventTask.second);
            m_asyncEventQueue.enqueueEvent(WTFMove(enterEvent));

            auto exitEvent = Event::create(eventNames().exitEvent, false, false);
            exitEvent->setTarget(eventTask.second);
            m_asyncEventQueue.enqueueEvent(WTFMove(exitEvent));
        } else {
            RefPtr<Event> event;
            if (eventTask.first == eventTask.second->startMediaTime())
                event = Event::create(eventNames().enterEvent, false, false);
            else
                event = Event::create(eventNames().exitEvent, false, false);
            event->setTarget(eventTask.second);
            m_asyncEventQueue.enqueueEvent(WTFMove(event));
        }
    }

    // 14 - Sort affected tracks in the same order as the text tracks appear in
    // the media element's list of text tracks, and remove duplicates.
    std::sort(affectedTracks.begin(), affectedTracks.end(), trackIndexCompare);

    // 15 - For each text track in affected tracks, in the list order, queue a
    // task to fire a simple event named cuechange at the TextTrack object, and, ...
    for (auto& affectedTrack : affectedTracks) {
        auto event = Event::create(eventNames().cuechangeEvent, false, false);
        event->setTarget(affectedTrack);
        m_asyncEventQueue.enqueueEvent(WTFMove(event));

        // ... if the text track has a corresponding track element, to then fire a
        // simple event named cuechange at the track element as well.
        if (is<LoadableTextTrack>(*affectedTrack)) {
            auto event = Event::create(eventNames().cuechangeEvent, false, false);
            auto* trackElement = downcast<LoadableTextTrack>(*affectedTrack).trackElement();
            ASSERT(trackElement);
            event->setTarget(trackElement);
            m_asyncEventQueue.enqueueEvent(WTFMove(event));
        }
    }

    // 16 - Set the text track cue active flag of all the cues in the current
    // cues, and unset the text track cue active flag of all the cues in the
    // other cues.
    for (size_t i = 0; i < currentCuesSize; ++i)
        currentCues[i].data()->setIsActive(true);

    for (size_t i = 0; i < previousCuesSize; ++i)
        if (!currentCues.contains(previousCues[i]))
            previousCues[i].data()->setIsActive(false);

    // Update the current active cues.
    m_currentlyActiveCues = currentCues;

    if (activeSetChanged)
        updateTextTrackDisplay();
}

bool HTMLMediaElement::textTracksAreReady() const
{
    // 4.8.10.12.1 Text track model
    // ...
    // The text tracks of a media element are ready if all the text tracks whose mode was not 
    // in the disabled state when the element's resource selection algorithm last started now
    // have a text track readiness state of loaded or failed to load.
    for (unsigned i = 0; i < m_textTracksWhenResourceSelectionBegan.size(); ++i) {
        if (m_textTracksWhenResourceSelectionBegan[i]->readinessState() == TextTrack::Loading
            || m_textTracksWhenResourceSelectionBegan[i]->readinessState() == TextTrack::NotLoaded)
            return false;
    }

    return true;
}

void HTMLMediaElement::textTrackReadyStateChanged(TextTrack* track)
{
    if (m_player && m_textTracksWhenResourceSelectionBegan.contains(track)) {
        if (track->readinessState() != TextTrack::Loading)
            setReadyState(m_player->readyState());
    } else {
        // The track readiness state might have changed as a result of the user
        // clicking the captions button. In this case, a check whether all the
        // resources have failed loading should be done in order to hide the CC button.
        if (hasMediaControls() && track->readinessState() == TextTrack::FailedToLoad)
            mediaControls()->refreshClosedCaptionsButtonVisibility();
    }
}

void HTMLMediaElement::audioTrackEnabledChanged(AudioTrack& track)
{
    if (m_audioTracks && m_audioTracks->contains(track))
        m_audioTracks->scheduleChangeEvent();
    if (processingUserGestureForMedia())
        removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);
}

void HTMLMediaElement::textTrackModeChanged(TextTrack& track)
{
    bool trackIsLoaded = true;
    if (track.trackType() == TextTrack::TrackElement) {
        trackIsLoaded = false;
        for (auto& trackElement : childrenOfType<HTMLTrackElement>(*this)) {
            if (&trackElement.track() == &track) {
                if (trackElement.readyState() == HTMLTrackElement::LOADING || trackElement.readyState() == HTMLTrackElement::LOADED)
                    trackIsLoaded = true;
                break;
            }
        }
    }

    // If this is the first added track, create the list of text tracks.
    if (!m_textTracks)
        m_textTracks = TextTrackList::create(this, ActiveDOMObject::scriptExecutionContext());
    
    // Mark this track as "configured" so configureTextTracks won't change the mode again.
    track.setHasBeenConfigured(true);
    
    if (track.mode() != TextTrack::Mode::Disabled && trackIsLoaded)
        textTrackAddCues(track, *track.cues());

    configureTextTrackDisplay(AssumeTextTrackVisibilityChanged);

    if (m_textTracks && m_textTracks->contains(track))
        m_textTracks->scheduleChangeEvent();

#if ENABLE(AVF_CAPTIONS)
    if (track.trackType() == TextTrack::TrackElement && m_player)
        m_player->notifyTrackModeChanged();
#endif
}

void HTMLMediaElement::videoTrackSelectedChanged(VideoTrack& track)
{
    if (m_videoTracks && m_videoTracks->contains(track))
        m_videoTracks->scheduleChangeEvent();
}

void HTMLMediaElement::textTrackKindChanged(TextTrack& track)
{
    if (track.kind() != TextTrack::Kind::Captions && track.kind() != TextTrack::Kind::Subtitles && track.mode() == TextTrack::Mode::Showing)
        track.setMode(TextTrack::Mode::Hidden);
}

void HTMLMediaElement::beginIgnoringTrackDisplayUpdateRequests()
{
    ++m_ignoreTrackDisplayUpdate;
}

void HTMLMediaElement::endIgnoringTrackDisplayUpdateRequests()
{
    ASSERT(m_ignoreTrackDisplayUpdate);
    --m_ignoreTrackDisplayUpdate;
    if (!m_ignoreTrackDisplayUpdate && m_inActiveDocument)
        updateActiveTextTrackCues(currentMediaTime());
}

void HTMLMediaElement::textTrackAddCues(TextTrack& track, const TextTrackCueList& cues)
{
    if (track.mode() == TextTrack::Mode::Disabled)
        return;

    TrackDisplayUpdateScope scope { *this };
    for (unsigned i = 0; i < cues.length(); ++i)
        textTrackAddCue(track, *cues.item(i));
}

void HTMLMediaElement::textTrackRemoveCues(TextTrack&, const TextTrackCueList& cues)
{
    TrackDisplayUpdateScope scope { *this };
    for (unsigned i = 0; i < cues.length(); ++i) {
        auto& cue = *cues.item(i);
        textTrackRemoveCue(*cue.track(), cue);
    }
}

void HTMLMediaElement::textTrackAddCue(TextTrack& track, TextTrackCue& cue)
{
    if (track.mode() == TextTrack::Mode::Disabled)
        return;

    // Negative duration cues need be treated in the interval tree as
    // zero-length cues.
    MediaTime endTime = std::max(cue.startMediaTime(), cue.endMediaTime());

    CueInterval interval = m_cueTree.createInterval(cue.startMediaTime(), endTime, &cue);
    if (!m_cueTree.contains(interval))
        m_cueTree.add(interval);
    updateActiveTextTrackCues(currentMediaTime());
}

void HTMLMediaElement::textTrackRemoveCue(TextTrack&, TextTrackCue& cue)
{
    // Negative duration cues need to be treated in the interval tree as
    // zero-length cues.
    MediaTime endTime = std::max(cue.startMediaTime(), cue.endMediaTime());

    CueInterval interval = m_cueTree.createInterval(cue.startMediaTime(), endTime, &cue);
    m_cueTree.remove(interval);

    // Since the cue will be removed from the media element and likely the
    // TextTrack might also be destructed, notifying the region of the cue
    // removal shouldn't be done.
    if (cue.isRenderable())
        toVTTCue(&cue)->notifyRegionWhenRemovingDisplayTree(false);

    size_t index = m_currentlyActiveCues.find(interval);
    if (index != notFound) {
        cue.setIsActive(false);
        m_currentlyActiveCues.remove(index);
    }

    if (cue.isRenderable())
        toVTTCue(&cue)->removeDisplayTree();
    updateActiveTextTrackCues(currentMediaTime());

    if (cue.isRenderable())
        toVTTCue(&cue)->notifyRegionWhenRemovingDisplayTree(true);
}

#endif

static inline bool isAllowedToLoadMediaURL(HTMLMediaElement& element, const URL& url, bool isInUserAgentShadowTree)
{
    // Elements in user agent show tree should load whatever the embedding document policy is.
    if (isInUserAgentShadowTree)
        return true;

    ASSERT(element.document().contentSecurityPolicy());
    return element.document().contentSecurityPolicy()->allowMediaFromSource(url);
}

bool HTMLMediaElement::isSafeToLoadURL(const URL& url, InvalidURLAction actionIfInvalid)
{
    if (!url.isValid()) {
        LOG(Media, "HTMLMediaElement::isSafeToLoadURL(%p) - %s -> FALSE because url is invalid", this, urlForLoggingMedia(url).utf8().data());
        return false;
    }

    Frame* frame = document().frame();
    if (!frame || !document().securityOrigin().canDisplay(url)) {
        if (actionIfInvalid == Complain)
            FrameLoader::reportLocalLoadFailed(frame, url.stringCenterEllipsizedToLength());
        LOG(Media, "HTMLMediaElement::isSafeToLoadURL(%p) - %s -> FALSE rejected by SecurityOrigin", this, urlForLoggingMedia(url).utf8().data());
        return false;
    }

    if (!isAllowedToLoadMediaURL(*this, url, isInUserAgentShadowTree())) {
        LOG(Media, "HTMLMediaElement::isSafeToLoadURL(%p) - %s -> rejected by Content Security Policy", this, urlForLoggingMedia(url).utf8().data());
        return false;
    }

    return true;
}

void HTMLMediaElement::startProgressEventTimer()
{
    if (m_progressEventTimer.isActive())
        return;

    m_previousProgressTime = monotonicallyIncreasingTime();
    // 350ms is not magic, it is in the spec!
    m_progressEventTimer.startRepeating(350_ms);
}

void HTMLMediaElement::waitForSourceChange()
{
    LOG(Media, "HTMLMediaElement::waitForSourceChange(%p)", this);

    stopPeriodicTimers();
    m_loadState = WaitingForSource;

    // 6.17 - Waiting: Set the element's networkState attribute to the NETWORK_NO_SOURCE value
    m_networkState = NETWORK_NO_SOURCE;

    // 6.18 - Set the element's delaying-the-load-event flag to false. This stops delaying the load event.
    setShouldDelayLoadEvent(false);

    updateDisplayState();
    updateRenderer();
}

void HTMLMediaElement::noneSupported()
{
    LOG(Media, "HTMLMediaElement::noneSupported(%p)", this);

    stopPeriodicTimers();
    m_loadState = WaitingForSource;
    m_currentSourceNode = nullptr;

    // 4.8.10.5 
    // 6 - Reaching this step indicates that the media resource failed to load or that the given 
    // URL could not be resolved. In one atomic operation, run the following steps:

    // 6.1 - Set the error attribute to a new MediaError object whose code attribute is set to
    // MEDIA_ERR_SRC_NOT_SUPPORTED.
    m_error = MediaError::create(MediaError::MEDIA_ERR_SRC_NOT_SUPPORTED);

    // 6.2 - Forget the media element's media-resource-specific text tracks.
    forgetResourceSpecificTracks();

    // 6.3 - Set the element's networkState attribute to the NETWORK_NO_SOURCE value.
    m_networkState = NETWORK_NO_SOURCE;

    // 7 - Queue a task to fire a simple event named error at the media element.
    scheduleEvent(eventNames().errorEvent);

    rejectPendingPlayPromises(DOMError::create("NotSupportedError", "The operation is not supported."));

#if ENABLE(MEDIA_SOURCE)
    detachMediaSource();
#endif

    // 8 - Set the element's delaying-the-load-event flag to false. This stops delaying the load event.
    setShouldDelayLoadEvent(false);

    // 9 - Abort these steps. Until the load() method is invoked or the src attribute is changed, 
    // the element won't attempt to load another resource.

    updateDisplayState();
    updateRenderer();
}

void HTMLMediaElement::mediaLoadingFailedFatally(MediaPlayer::NetworkState error)
{
    LOG(Media, "HTMLMediaElement::mediaLoadingFailedFatally(%p) - error = %d", this, static_cast<int>(error));

    // 1 - The user agent should cancel the fetching process.
    stopPeriodicTimers();
    m_loadState = WaitingForSource;

    // 2 - Set the error attribute to a new MediaError object whose code attribute is 
    // set to MEDIA_ERR_NETWORK/MEDIA_ERR_DECODE.
    if (error == MediaPlayer::NetworkError)
        m_error = MediaError::create(MediaError::MEDIA_ERR_NETWORK);
    else if (error == MediaPlayer::DecodeError)
        m_error = MediaError::create(MediaError::MEDIA_ERR_DECODE);
    else
        ASSERT_NOT_REACHED();

    // 3 - Queue a task to fire a simple event named error at the media element.
    scheduleEvent(eventNames().errorEvent);

#if ENABLE(MEDIA_SOURCE)
    detachMediaSource();
#endif

    // 4 - Set the element's networkState attribute to the NETWORK_EMPTY value and queue a
    // task to fire a simple event called emptied at the element.
    m_networkState = NETWORK_EMPTY;
    scheduleEvent(eventNames().emptiedEvent);

    // 5 - Set the element's delaying-the-load-event flag to false. This stops delaying the load event.
    setShouldDelayLoadEvent(false);

    // 6 - Abort the overall resource selection algorithm.
    m_currentSourceNode = nullptr;

#if PLATFORM(COCOA)
    if (is<MediaDocument>(document()))
        downcast<MediaDocument>(document()).mediaElementSawUnsupportedTracks();
#endif
}

void HTMLMediaElement::cancelPendingEventsAndCallbacks()
{
    LOG(Media, "HTMLMediaElement::cancelPendingEventsAndCallbacks(%p)", this);
    m_asyncEventQueue.cancelAllEvents();

    for (auto& source : childrenOfType<HTMLSourceElement>(*this))
        source.cancelPendingErrorEvent();

    rejectPendingPlayPromises(DOMError::create("AbortError", "The operation was aborted."));
}

void HTMLMediaElement::mediaPlayerNetworkStateChanged(MediaPlayer*)
{
    beginProcessingMediaPlayerCallback();
    setNetworkState(m_player->networkState());
    endProcessingMediaPlayerCallback();
}

static void logMediaLoadRequest(Page* page, const String& mediaEngine, const String& errorMessage, bool succeeded)
{
    if (!page)
        return;

    DiagnosticLoggingClient& diagnosticLoggingClient = page->diagnosticLoggingClient();
    if (!succeeded) {
        diagnosticLoggingClient.logDiagnosticMessageWithResult(DiagnosticLoggingKeys::mediaLoadingFailedKey(), errorMessage, DiagnosticLoggingResultFail, ShouldSample::No);
        return;
    }

    diagnosticLoggingClient.logDiagnosticMessage(DiagnosticLoggingKeys::mediaLoadedKey(), mediaEngine, ShouldSample::No);

    if (!page->hasSeenAnyMediaEngine())
        diagnosticLoggingClient.logDiagnosticMessage(DiagnosticLoggingKeys::pageContainsAtLeastOneMediaEngineKey(), emptyString(), ShouldSample::No);

    if (!page->hasSeenMediaEngine(mediaEngine))
        diagnosticLoggingClient.logDiagnosticMessage(DiagnosticLoggingKeys::pageContainsMediaEngineKey(), mediaEngine, ShouldSample::No);

    page->sawMediaEngine(mediaEngine);
}

static String stringForNetworkState(MediaPlayer::NetworkState state)
{
    switch (state) {
    case MediaPlayer::Empty: return ASCIILiteral("Empty");
    case MediaPlayer::Idle: return ASCIILiteral("Idle");
    case MediaPlayer::Loading: return ASCIILiteral("Loading");
    case MediaPlayer::Loaded: return ASCIILiteral("Loaded");
    case MediaPlayer::FormatError: return ASCIILiteral("FormatError");
    case MediaPlayer::NetworkError: return ASCIILiteral("NetworkError");
    case MediaPlayer::DecodeError: return ASCIILiteral("DecodeError");
    default: return emptyString();
    }
}

void HTMLMediaElement::mediaLoadingFailed(MediaPlayer::NetworkState error)
{
    stopPeriodicTimers();
    
    // If we failed while trying to load a <source> element, the movie was never parsed, and there are more
    // <source> children, schedule the next one
    if (m_readyState < HAVE_METADATA && m_loadState == LoadingFromSourceElement) {
        
        // resource selection algorithm
        // Step 9.Otherwise.9 - Failed with elements: Queue a task, using the DOM manipulation task source, to fire a simple event named error at the candidate element.
        if (m_currentSourceNode)
            m_currentSourceNode->scheduleErrorEvent();
        else
            LOG(Media, "HTMLMediaElement::setNetworkState(%p) - error event not sent, <source> was removed", this);
        
        // 9.Otherwise.10 - Asynchronously await a stable state. The synchronous section consists of all the remaining steps of this algorithm until the algorithm says the synchronous section has ended.
        
        // 9.Otherwise.11 - Forget the media element's media-resource-specific tracks.
        forgetResourceSpecificTracks();

        if (havePotentialSourceChild()) {
            LOG(Media, "HTMLMediaElement::setNetworkState(%p) - scheduling next <source>", this);
            scheduleNextSourceChild();
        } else {
            LOG(Media, "HTMLMediaElement::setNetworkState(%p) - no more <source> elements, waiting", this);
            waitForSourceChange();
        }
        
        return;
    }
    
    if ((error == MediaPlayer::NetworkError && m_readyState >= HAVE_METADATA) || error == MediaPlayer::DecodeError)
        mediaLoadingFailedFatally(error);
    else if ((error == MediaPlayer::FormatError || error == MediaPlayer::NetworkError) && m_loadState == LoadingFromSrcAttr)
        noneSupported();
    
    updateDisplayState();
    if (hasMediaControls()) {
        mediaControls()->reset();
        mediaControls()->reportedError();
    }

    logMediaLoadRequest(document().page(), String(), stringForNetworkState(error), false);

    m_mediaSession->clientCharacteristicsChanged();
}

void HTMLMediaElement::setNetworkState(MediaPlayer::NetworkState state)
{
    LOG(Media, "HTMLMediaElement::setNetworkState(%p) - new state = %d, current state = %d", this, static_cast<int>(state), static_cast<int>(m_networkState));

    if (state == MediaPlayer::Empty) {
        // Just update the cached state and leave, we can't do anything.
        m_networkState = NETWORK_EMPTY;
        return;
    }

    if (state == MediaPlayer::FormatError || state == MediaPlayer::NetworkError || state == MediaPlayer::DecodeError) {
        mediaLoadingFailed(state);
        return;
    }

    if (state == MediaPlayer::Idle) {
        if (m_networkState > NETWORK_IDLE) {
            changeNetworkStateFromLoadingToIdle();
            setShouldDelayLoadEvent(false);
        } else {
            m_networkState = NETWORK_IDLE;
        }
    }

    if (state == MediaPlayer::Loading) {
        if (m_networkState < NETWORK_LOADING || m_networkState == NETWORK_NO_SOURCE)
            startProgressEventTimer();
        m_networkState = NETWORK_LOADING;
    }

    if (state == MediaPlayer::Loaded) {
        if (m_networkState != NETWORK_IDLE)
            changeNetworkStateFromLoadingToIdle();
        m_completelyLoaded = true;
    }

    if (hasMediaControls())
        mediaControls()->updateStatusDisplay();
}

void HTMLMediaElement::changeNetworkStateFromLoadingToIdle()
{
    m_progressEventTimer.stop();
    if (hasMediaControls() && m_player->didLoadingProgress())
        mediaControls()->bufferingProgressed();

    // Schedule one last progress event so we guarantee that at least one is fired
    // for files that load very quickly.
    scheduleEvent(eventNames().progressEvent);
    scheduleEvent(eventNames().suspendEvent);
    m_networkState = NETWORK_IDLE;
}

void HTMLMediaElement::mediaPlayerReadyStateChanged(MediaPlayer*)
{
    beginProcessingMediaPlayerCallback();

    setReadyState(m_player->readyState());

    endProcessingMediaPlayerCallback();
}

SuccessOr<MediaPlaybackDenialReason> HTMLMediaElement::canTransitionFromAutoplayToPlay() const
{
    if (isAutoplaying()
        && mediaSession().autoplayPermitted()
        && paused()
        && autoplay()
        && !pausedForUserInteraction()
        && !document().isSandboxed(SandboxAutomaticFeatures)
        && m_readyState == HAVE_ENOUGH_DATA)
        return mediaSession().playbackPermitted(*this);

    RELEASE_LOG(Media, "HTMLMediaElement::canTransitionFromAutoplayToPlay - page consent required");
    return MediaPlaybackDenialReason::PageConsentRequired;
}

void HTMLMediaElement::dispatchPlayPauseEventsIfNeedsQuirks()
{
    auto& document = this->document();
    if (!needsAutoplayPlayPauseEventsQuirk(document) && !needsAutoplayPlayPauseEventsQuirk(document.topDocument()))
        return;

    scheduleEvent(eventNames().playingEvent);
    scheduleEvent(eventNames().pauseEvent);
}

void HTMLMediaElement::setReadyState(MediaPlayer::ReadyState state)
{
    LOG(Media, "HTMLMediaElement::setReadyState(%p) - new state = %d, current state = %d,", this, static_cast<int>(state), static_cast<int>(m_readyState));

    // Set "wasPotentiallyPlaying" BEFORE updating m_readyState, potentiallyPlaying() uses it
    bool wasPotentiallyPlaying = potentiallyPlaying();

    ReadyState oldState = m_readyState;
    ReadyState newState = static_cast<ReadyState>(state);

#if ENABLE(VIDEO_TRACK)
    bool tracksAreReady = textTracksAreReady();

    if (newState == oldState && m_tracksAreReady == tracksAreReady)
        return;

    m_tracksAreReady = tracksAreReady;
#else
    if (newState == oldState)
        return;
    bool tracksAreReady = true;
#endif
    
    if (tracksAreReady)
        m_readyState = newState;
    else {
        // If a media file has text tracks the readyState may not progress beyond HAVE_FUTURE_DATA until
        // the text tracks are ready, regardless of the state of the media file.
        if (newState <= HAVE_METADATA)
            m_readyState = newState;
        else
            m_readyState = HAVE_CURRENT_DATA;
    }
    
    if (oldState > m_readyStateMaximum)
        m_readyStateMaximum = oldState;

    if (m_networkState == NETWORK_EMPTY)
        return;

    if (m_seeking) {
        // 4.8.10.9, step 11
        if (wasPotentiallyPlaying && m_readyState < HAVE_FUTURE_DATA)
            scheduleEvent(eventNames().waitingEvent);

        // 4.8.10.10 step 14 & 15.
        if (m_seekRequested && !m_player->seeking() && m_readyState >= HAVE_CURRENT_DATA)
            finishSeek();
    } else {
        if (wasPotentiallyPlaying && m_readyState < HAVE_FUTURE_DATA) {
            // 4.8.10.8
            invalidateCachedTime();
            scheduleTimeupdateEvent(false);
            scheduleEvent(eventNames().waitingEvent);
        }
    }

    if (m_readyState >= HAVE_METADATA && oldState < HAVE_METADATA) {
        prepareMediaFragmentURI();
        scheduleEvent(eventNames().durationchangeEvent);
        scheduleResizeEvent();
        scheduleEvent(eventNames().loadedmetadataEvent);
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
        if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent))
            enqueuePlaybackTargetAvailabilityChangedEvent();
#endif
        m_initiallyMuted = m_volume < 0.05 || muted();

        if (hasMediaControls())
            mediaControls()->loadedMetadata();
        updateRenderer();

        if (is<MediaDocument>(document()))
            downcast<MediaDocument>(document()).mediaElementNaturalSizeChanged(expandedIntSize(m_player->naturalSize()));

        logMediaLoadRequest(document().page(), m_player->engineDescription(), String(), true);

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
        updateMediaState(UpdateState::Asynchronously);
#endif

        m_mediaSession->clientCharacteristicsChanged();
    }

    bool shouldUpdateDisplayState = false;

    if (m_readyState >= HAVE_CURRENT_DATA && oldState < HAVE_CURRENT_DATA && !m_haveFiredLoadedData) {
        m_haveFiredLoadedData = true;
        shouldUpdateDisplayState = true;
        scheduleEvent(eventNames().loadeddataEvent);
        setShouldDelayLoadEvent(false);
        applyMediaFragmentURI();
    }

    bool isPotentiallyPlaying = potentiallyPlaying();
    if (m_readyState == HAVE_FUTURE_DATA && oldState <= HAVE_CURRENT_DATA && tracksAreReady) {
        scheduleEvent(eventNames().canplayEvent);
        if (isPotentiallyPlaying)
            scheduleNotifyAboutPlaying();
        shouldUpdateDisplayState = true;
    }

    if (m_readyState == HAVE_ENOUGH_DATA && oldState < HAVE_ENOUGH_DATA && tracksAreReady) {
        if (oldState <= HAVE_CURRENT_DATA)
            scheduleEvent(eventNames().canplayEvent);

        scheduleEvent(eventNames().canplaythroughEvent);

        if (isPotentiallyPlaying && oldState <= HAVE_CURRENT_DATA)
            scheduleNotifyAboutPlaying();

        auto success = canTransitionFromAutoplayToPlay();
        if (success) {
            m_paused = false;
            invalidateCachedTime();
            setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Started);
            m_playbackStartedTime = currentMediaTime().toDouble();
            scheduleEvent(eventNames().playEvent);
            scheduleNotifyAboutPlaying();
        } else if (success.value() == MediaPlaybackDenialReason::UserGestureRequired)
            setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);

        shouldUpdateDisplayState = true;
    }

    // If we transition to the Future Data state and we're about to begin playing, ensure playback is actually permitted first,
    // honoring any playback denial reasons such as the requirement of a user gesture.
    if (m_readyState == HAVE_FUTURE_DATA && oldState < HAVE_FUTURE_DATA && potentiallyPlaying() && !m_mediaSession->playbackPermitted(*this)) {
        pauseInternal();
        setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);
    }

    if (shouldUpdateDisplayState) {
        updateDisplayState();
        if (hasMediaControls()) {
            mediaControls()->refreshClosedCaptionsButtonVisibility();
            mediaControls()->updateStatusDisplay();
        }
    }

    updatePlayState();
    updateMediaController();
#if ENABLE(VIDEO_TRACK)
    updateActiveTextTrackCues(currentMediaTime());
#endif
}

#if ENABLE(LEGACY_ENCRYPTED_MEDIA)
RefPtr<ArrayBuffer> HTMLMediaElement::mediaPlayerCachedKeyForKeyId(const String& keyId) const
{
    return m_webKitMediaKeys ? m_webKitMediaKeys->cachedKeyForKeyId(keyId) : nullptr;
}

bool HTMLMediaElement::mediaPlayerKeyNeeded(MediaPlayer*, Uint8Array* initData)
{
    if (!RuntimeEnabledFeatures::sharedFeatures().legacyEncryptedMediaAPIEnabled())
        return false;

    if (!hasEventListeners("webkitneedkey")) {
        m_error = MediaError::create(MediaError::MEDIA_ERR_ENCRYPTED);
        scheduleEvent(eventNames().errorEvent);
        return false;
    }

    auto event = WebKitMediaKeyNeededEvent::create(eventNames().webkitneedkeyEvent, initData);
    event->setTarget(this);
    m_asyncEventQueue.enqueueEvent(WTFMove(event));

    return true;
}

String HTMLMediaElement::mediaPlayerMediaKeysStorageDirectory() const
{
    auto* page = document().page();
    if (!page || page->usesEphemeralSession())
        return emptyString();

    String storageDirectory = document().settings().mediaKeysStorageDirectory();
    if (storageDirectory.isEmpty())
        return emptyString();

    return pathByAppendingComponent(storageDirectory, SecurityOriginData::fromSecurityOrigin(document().securityOrigin()).databaseIdentifier());
}

void HTMLMediaElement::webkitSetMediaKeys(WebKitMediaKeys* mediaKeys)
{
    if (!RuntimeEnabledFeatures::sharedFeatures().legacyEncryptedMediaAPIEnabled())
        return;

    if (m_webKitMediaKeys == mediaKeys)
        return;

    if (m_webKitMediaKeys)
        m_webKitMediaKeys->setMediaElement(nullptr);
    m_webKitMediaKeys = mediaKeys;
    if (m_webKitMediaKeys)
        m_webKitMediaKeys->setMediaElement(this);
}

void HTMLMediaElement::keyAdded()
{
    if (!RuntimeEnabledFeatures::sharedFeatures().legacyEncryptedMediaAPIEnabled())
        return;

    if (m_player)
        m_player->keyAdded();
}

#endif

#if ENABLE(ENCRYPTED_MEDIA)

MediaKeys* HTMLMediaElement::mediaKeys() const
{
    return nullptr;
}

void HTMLMediaElement::setMediaKeys(MediaKeys*, Ref<DeferredPromise>&&)
{
    notImplemented();
}

#endif // ENABLE(ENCRYPTED_MEDIA)

void HTMLMediaElement::progressEventTimerFired()
{
    ASSERT(m_player);
    if (m_networkState != NETWORK_LOADING)
        return;

    double time = monotonicallyIncreasingTime();
    double timedelta = time - m_previousProgressTime;

    if (m_player->didLoadingProgress()) {
        scheduleEvent(eventNames().progressEvent);
        m_previousProgressTime = time;
        m_sentStalledEvent = false;
        updateRenderer();
        if (hasMediaControls())
            mediaControls()->bufferingProgressed();
    } else if (timedelta > 3.0 && !m_sentStalledEvent) {
        scheduleEvent(eventNames().stalledEvent);
        m_sentStalledEvent = true;
        setShouldDelayLoadEvent(false);
    }
}

void HTMLMediaElement::rewind(double timeDelta)
{
    LOG(Media, "HTMLMediaElement::rewind(%p) - %f", this, timeDelta);
    setCurrentTime(std::max(currentMediaTime() - MediaTime::createWithDouble(timeDelta), minTimeSeekable()));
}

void HTMLMediaElement::returnToRealtime()
{
    LOG(Media, "HTMLMediaElement::returnToRealtime(%p)", this);
    setCurrentTime(maxTimeSeekable());
}

void HTMLMediaElement::addPlayedRange(const MediaTime& start, const MediaTime& end)
{
    LOG(Media, "HTMLMediaElement::addPlayedRange(%p) - [%s, %s]", this, toString(start).utf8().data(), toString(end).utf8().data());
    if (!m_playedTimeRanges)
        m_playedTimeRanges = TimeRanges::create();
    m_playedTimeRanges->ranges().add(start, end);
}  

bool HTMLMediaElement::supportsScanning() const
{
    return m_player ? m_player->supportsScanning() : false;
}

void HTMLMediaElement::prepareToPlay()
{
    LOG(Media, "HTMLMediaElement::prepareToPlay(%p)", this);
    if (m_havePreparedToPlay)
        return;
    m_havePreparedToPlay = true;
    if (m_player)
        m_player->prepareToPlay();
}

void HTMLMediaElement::fastSeek(double time)
{
    fastSeek(MediaTime::createWithDouble(time));
}

void HTMLMediaElement::fastSeek(const MediaTime& time)
{
    LOG(Media, "HTMLMediaElement::fastSeek(%p) - %s", this, toString(time).utf8().data());
    // 4.7.10.9 Seeking
    // 9. If the approximate-for-speed flag is set, adjust the new playback position to a value that will
    // allow for playback to resume promptly. If new playback position before this step is before current
    // playback position, then the adjusted new playback position must also be before the current playback
    // position. Similarly, if the new playback position before this step is after current playback position,
    // then the adjusted new playback position must also be after the current playback position.
    refreshCachedTime();
    MediaTime delta = time - currentMediaTime();
    MediaTime negativeTolerance = delta >= MediaTime::zeroTime() ? delta : MediaTime::positiveInfiniteTime();
    MediaTime positiveTolerance = delta < MediaTime::zeroTime() ? -delta : MediaTime::positiveInfiniteTime();

    seekWithTolerance(time, negativeTolerance, positiveTolerance, true);
}

void HTMLMediaElement::seek(const MediaTime& time)
{
    LOG(Media, "HTMLMediaElement::seek(%p) - %s", this, toString(time).utf8().data());
    seekWithTolerance(time, MediaTime::zeroTime(), MediaTime::zeroTime(), true);
}

void HTMLMediaElement::seekInternal(const MediaTime& time)
{
    LOG(Media, "HTMLMediaElement::seekInternal(%p) - %s", this, toString(time).utf8().data());
    seekWithTolerance(time, MediaTime::zeroTime(), MediaTime::zeroTime(), false);
}

void HTMLMediaElement::seekWithTolerance(const MediaTime& inTime, const MediaTime& negativeTolerance, const MediaTime& positiveTolerance, bool fromDOM)
{
    // 4.8.10.9 Seeking
    MediaTime time = inTime;

    // 1 - Set the media element's show poster flag to false.
    setDisplayMode(Video);

    // 2 - If the media element's readyState is HAVE_NOTHING, abort these steps.
    if (m_readyState == HAVE_NOTHING || !m_player)
        return;

    // If the media engine has been told to postpone loading data, let it go ahead now.
    if (m_preload < MediaPlayer::Auto && m_readyState < HAVE_FUTURE_DATA)
        prepareToPlay();

    // Get the current time before setting m_seeking, m_lastSeekTime is returned once it is set.
    refreshCachedTime();
    MediaTime now = currentMediaTime();

    // 3 - If the element's seeking IDL attribute is true, then another instance of this algorithm is
    // already running. Abort that other instance of the algorithm without waiting for the step that
    // it is running to complete.
    if (m_seekTaskQueue.hasPendingTasks()) {
        LOG(Media, "HTMLMediaElement::seekWithTolerance(%p) - cancelling pending seeks", this);
        m_seekTaskQueue.cancelAllTasks();
        if (m_pendingSeek) {
            now = m_pendingSeek->now;
            m_pendingSeek = nullptr;
        }
        m_pendingSeekType = NoSeek;
    }

    // 4 - Set the seeking IDL attribute to true.
    // The flag will be cleared when the engine tells us the time has actually changed.
    m_seeking = true;
    if (m_playing) {
        if (m_lastSeekTime < now)
            addPlayedRange(m_lastSeekTime, now);
    }
    m_lastSeekTime = time;

    // 5 - If the seek was in response to a DOM method call or setting of an IDL attribute, then continue
    // the script. The remainder of these steps must be run asynchronously.
    m_pendingSeek = std::make_unique<PendingSeek>(now, time, negativeTolerance, positiveTolerance);
    if (fromDOM) {
        LOG(Media, "HTMLMediaElement::seekWithTolerance(%p) - enqueuing seek from %s to %s", this, toString(now).utf8().data(), toString(time).utf8().data());
        m_seekTaskQueue.enqueueTask(std::bind(&HTMLMediaElement::seekTask, this));
    } else
        seekTask();

    if (processingUserGestureForMedia())
        m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequireUserGestureToControlControlsManager);
}

void HTMLMediaElement::seekTask()
{
    LOG(Media, "HTMLMediaElement::seekTask(%p)", this);

    if (!m_player) {
        clearSeeking();
        return;
    }

    ASSERT(m_pendingSeek);
    MediaTime now = m_pendingSeek->now;
    MediaTime time = m_pendingSeek->targetTime;
    MediaTime negativeTolerance = m_pendingSeek->negativeTolerance;
    MediaTime positiveTolerance = m_pendingSeek->positiveTolerance;
    m_pendingSeek = nullptr;

    ASSERT(negativeTolerance >= MediaTime::zeroTime());
    
    // 6 - If the new playback position is later than the end of the media resource, then let it be the end 
    // of the media resource instead.
    time = std::min(time, durationMediaTime());

    // 7 - If the new playback position is less than the earliest possible position, let it be that position instead.
    MediaTime earliestTime = m_player->startTime();
    time = std::max(time, earliestTime);

    // Ask the media engine for the time value in the movie's time scale before comparing with current time. This
    // is necessary because if the seek time is not equal to currentTime but the delta is less than the movie's
    // time scale, we will ask the media engine to "seek" to the current movie time, which may be a noop and
    // not generate a timechanged callback. This means m_seeking will never be cleared and we will never 
    // fire a 'seeked' event.
#if !LOG_DISABLED
    MediaTime mediaTime = m_player->mediaTimeForTimeValue(time);
    if (time != mediaTime)
        LOG(Media, "HTMLMediaElement::seekTask(%p) - %s - media timeline equivalent is %s", this, toString(time).utf8().data(), toString(mediaTime).utf8().data());
#endif
    time = m_player->mediaTimeForTimeValue(time);

    // 8 - If the (possibly now changed) new playback position is not in one of the ranges given in the
    // seekable attribute, then let it be the position in one of the ranges given in the seekable attribute 
    // that is the nearest to the new playback position. ... If there are no ranges given in the seekable
    // attribute then set the seeking IDL attribute to false and abort these steps.
    RefPtr<TimeRanges> seekableRanges = seekable();
    bool noSeekRequired = !seekableRanges->length();

    // Short circuit seeking to the current time by just firing the events if no seek is required.
    // Don't skip calling the media engine if 1) we are in poster mode (because a seek should always cancel
    // poster display), or 2) if there is a pending fast seek, or 3) if this seek is not an exact seek
    SeekType thisSeekType = (negativeTolerance == MediaTime::zeroTime() && positiveTolerance == MediaTime::zeroTime()) ? Precise : Fast;
    if (!noSeekRequired && time == now && thisSeekType == Precise && m_pendingSeekType != Fast && displayMode() != Poster)
        noSeekRequired = true;

#if ENABLE(MEDIA_SOURCE)
    // Always notify the media engine of a seek if the source is not closed. This ensures that the source is
    // always in a flushed state when the 'seeking' event fires.
    if (m_mediaSource && !m_mediaSource->isClosed())
        noSeekRequired = false;
#endif

    if (noSeekRequired) {
        LOG(Media, "HTMLMediaElement::seekTask(%p) - seek to %s ignored", this, toString(time).utf8().data());
        if (time == now) {
            scheduleEvent(eventNames().seekingEvent);
            scheduleTimeupdateEvent(false);
            scheduleEvent(eventNames().seekedEvent);
        }
        clearSeeking();
        return;
    }
    time = seekableRanges->ranges().nearest(time);

    m_sentEndEvent = false;
    m_lastSeekTime = time;
    m_pendingSeekType = thisSeekType;
    m_seeking = true;

    // 10 - Queue a task to fire a simple event named seeking at the element.
    scheduleEvent(eventNames().seekingEvent);

    // 11 - Set the current playback position to the given new playback position
    m_seekRequested = true;
    m_player->seekWithTolerance(time, negativeTolerance, positiveTolerance);

    // 12 - Wait until the user agent has established whether or not the media data for the new playback
    // position is available, and, if it is, until it has decoded enough data to play back that position.
    // 13 - Await a stable state. The synchronous section consists of all the remaining steps of this algorithm.
}

void HTMLMediaElement::clearSeeking()
{
    m_seeking = false;
    m_seekRequested = false;
    m_pendingSeekType = NoSeek;
    invalidateCachedTime();
}

void HTMLMediaElement::finishSeek()
{
    // 4.8.10.9 Seeking
    // 14 - Set the seeking IDL attribute to false.
    clearSeeking();

    LOG(Media, "HTMLMediaElement::finishSeek(%p) - current time = %s", this, toString(currentMediaTime()).utf8().data());

    // 15 - Run the time maches on steps.
    // Handled by mediaPlayerTimeChanged().

    // 16 - Queue a task to fire a simple event named timeupdate at the element.
    scheduleEvent(eventNames().timeupdateEvent);

    // 17 - Queue a task to fire a simple event named seeked at the element.
    scheduleEvent(eventNames().seekedEvent);

#if ENABLE(MEDIA_SOURCE)
    if (m_mediaSource)
        m_mediaSource->monitorSourceBuffers();
#endif
}

HTMLMediaElement::ReadyState HTMLMediaElement::readyState() const
{
    return m_readyState;
}

MediaPlayer::MovieLoadType HTMLMediaElement::movieLoadType() const
{
    return m_player ? m_player->movieLoadType() : MediaPlayer::Unknown;
}

bool HTMLMediaElement::hasAudio() const
{
    return m_player ? m_player->hasAudio() : false;
}

bool HTMLMediaElement::seeking() const
{
    return m_seeking;
}

void HTMLMediaElement::refreshCachedTime() const
{
    if (!m_player)
        return;

    m_cachedTime = m_player->currentTime();
    if (!m_cachedTime) {
        // Do not use m_cachedTime until the media engine returns a non-zero value because we can't 
        // estimate current time until playback actually begins. 
        invalidateCachedTime(); 
        return; 
    } 

    m_clockTimeAtLastCachedTimeUpdate = monotonicallyIncreasingTime();
}

void HTMLMediaElement::invalidateCachedTime() const
{
    m_cachedTime = MediaTime::invalidTime();
    if (!m_player || !m_player->maximumDurationToCacheMediaTime())
        return;

#if !LOG_DISABLED
    if (m_cachedTime.isValid())
        LOG(Media, "HTMLMediaElement::invalidateCachedTime(%p)", this);
#endif

    // Don't try to cache movie time when playback first starts as the time reported by the engine
    // sometimes fluctuates for a short amount of time, so the cached time will be off if we take it
    // too early.
    static const double minimumTimePlayingBeforeCacheSnapshot = 0.5;

    m_minimumClockTimeToUpdateCachedTime = monotonicallyIncreasingTime() + minimumTimePlayingBeforeCacheSnapshot;
}

// playback state
double HTMLMediaElement::currentTime() const
{
    return currentMediaTime().toDouble();
}

MediaTime HTMLMediaElement::currentMediaTime() const
{
#if LOG_CACHED_TIME_WARNINGS
    static const MediaTime minCachedDeltaForWarning = MediaTime::create(1, 100);
#endif

    if (!m_player)
        return MediaTime::zeroTime();

    if (m_seeking) {
        LOG(Media, "HTMLMediaElement::currentTime(%p) - seeking, returning %s", this, toString(m_lastSeekTime).utf8().data());
        return m_lastSeekTime;
    }

    if (m_cachedTime.isValid() && m_paused) {
#if LOG_CACHED_TIME_WARNINGS
        MediaTime delta = m_cachedTime - m_player->currentTime();
        if (delta > minCachedDeltaForWarning)
            LOG(Media, "HTMLMediaElement::currentTime(%p) - WARNING, cached time is %s seconds off of media time when paused", this, toString(delta).utf8().data());
#endif
        return m_cachedTime;
    }

    // Is it too soon use a cached time?
    double now = monotonicallyIncreasingTime();
    double maximumDurationToCacheMediaTime = m_player->maximumDurationToCacheMediaTime();

    if (maximumDurationToCacheMediaTime && m_cachedTime.isValid() && !m_paused && now > m_minimumClockTimeToUpdateCachedTime) {
        double clockDelta = now - m_clockTimeAtLastCachedTimeUpdate;

        // Not too soon, use the cached time only if it hasn't expired.
        if (clockDelta < maximumDurationToCacheMediaTime) {
            MediaTime adjustedCacheTime = m_cachedTime + MediaTime::createWithDouble(effectivePlaybackRate() * clockDelta);

#if LOG_CACHED_TIME_WARNINGS
            MediaTime delta = adjustedCacheTime - m_player->currentTime();
            if (delta > minCachedDeltaForWarning)
                LOG(Media, "HTMLMediaElement::currentTime(%p) - WARNING, cached time is %f seconds off of media time when playing", this, delta);
#endif
            return adjustedCacheTime;
        }
    }

#if LOG_CACHED_TIME_WARNINGS
    if (maximumDurationToCacheMediaTime && now > m_minimumClockTimeToUpdateCachedTime && m_cachedTime != MediaPlayer::invalidTime()) {
        double clockDelta = now - m_clockTimeAtLastCachedTimeUpdate;
        MediaTime delta = m_cachedTime + MediaTime::createWithDouble(effectivePlaybackRate() * clockDelta) - m_player->currentTime();
        LOG(Media, "HTMLMediaElement::currentTime(%p) - cached time was %s seconds off of media time when it expired", this, toString(delta).utf8().data());
    }
#endif

    refreshCachedTime();

    if (m_cachedTime.isInvalid())
        return MediaTime::zeroTime();
    
    return m_cachedTime;
}

void HTMLMediaElement::setCurrentTime(double time)
{
    setCurrentTime(MediaTime::createWithDouble(time));
}

void HTMLMediaElement::setCurrentTime(const MediaTime& time)
{
    if (m_mediaController)
        return;

    seekInternal(time);
}

ExceptionOr<void> HTMLMediaElement::setCurrentTimeForBindings(double time)
{
    if (m_mediaController)
        return Exception { INVALID_STATE_ERR };
    seek(MediaTime::createWithDouble(time));
    return { };
}

double HTMLMediaElement::duration() const
{
    return durationMediaTime().toDouble();
}

MediaTime HTMLMediaElement::durationMediaTime() const
{
    if (m_player && m_readyState >= HAVE_METADATA)
        return m_player->duration();

    return MediaTime::invalidTime();
}

bool HTMLMediaElement::paused() const
{
    // As of this writing, JavaScript garbage collection calls this function directly. In the past
    // we had problems where this was called on an object after a bad cast. The assertion below
    // made our regression test detect the problem, so we should keep it because of that. But note
    // that the value of the assertion relies on the compiler not being smart enough to know that
    // isHTMLUnknownElement is guaranteed to return false for an HTMLMediaElement.
    ASSERT(!isHTMLUnknownElement());

    return m_paused;
}

double HTMLMediaElement::defaultPlaybackRate() const
{
#if ENABLE(MEDIA_STREAM)
    // http://w3c.github.io/mediacapture-main/#mediastreams-in-media-elements
    // "defaultPlaybackRate" - On setting: ignored. On getting: return 1.0
    // A MediaStream is not seekable. Therefore, this attribute must always have the
    // value 1.0 and any attempt to alter it must be ignored. Note that this also means
    // that the ratechange event will not fire.
    if (m_mediaStreamSrcObject)
        return 1;
#endif

    return m_defaultPlaybackRate;
}

void HTMLMediaElement::setDefaultPlaybackRate(double rate)
{
#if ENABLE(MEDIA_STREAM)
    // http://w3c.github.io/mediacapture-main/#mediastreams-in-media-elements
    // "defaultPlaybackRate" - On setting: ignored. On getting: return 1.0
    // A MediaStream is not seekable. Therefore, this attribute must always have the
    // value 1.0 and any attempt to alter it must be ignored. Note that this also means
    // that the ratechange event will not fire.
    if (m_mediaStreamSrcObject)
        return;
#endif

    if (m_defaultPlaybackRate != rate) {
        LOG(Media, "HTMLMediaElement::setDefaultPlaybackRate(%p) - %f", this, rate);
        m_defaultPlaybackRate = rate;
        scheduleEvent(eventNames().ratechangeEvent);
    }
}

double HTMLMediaElement::effectivePlaybackRate() const
{
    return m_mediaController ? m_mediaController->playbackRate() : m_reportedPlaybackRate;
}

double HTMLMediaElement::requestedPlaybackRate() const
{
    return m_mediaController ? m_mediaController->playbackRate() : m_requestedPlaybackRate;
}

double HTMLMediaElement::playbackRate() const
{
#if ENABLE(MEDIA_STREAM)
    // http://w3c.github.io/mediacapture-main/#mediastreams-in-media-elements
    // "playbackRate" - A MediaStream is not seekable. Therefore, this attribute must always
    // have the value 1.0 and any attempt to alter it must be ignored. Note that this also
    // means that the ratechange event will not fire.
    if (m_mediaStreamSrcObject)
        return 1;
#endif

    return m_requestedPlaybackRate;
}

void HTMLMediaElement::setPlaybackRate(double rate)
{
    LOG(Media, "HTMLMediaElement::setPlaybackRate(%p) - %f", this, rate);

#if ENABLE(MEDIA_STREAM)
    // http://w3c.github.io/mediacapture-main/#mediastreams-in-media-elements
    // "playbackRate" - A MediaStream is not seekable. Therefore, this attribute must always
    // have the value 1.0 and any attempt to alter it must be ignored. Note that this also
    // means that the ratechange event will not fire.
    if (m_mediaStreamSrcObject)
        return;
#endif

    if (m_player && potentiallyPlaying() && m_player->rate() != rate && !m_mediaController)
        m_player->setRate(rate);

    if (m_requestedPlaybackRate != rate) {
        m_reportedPlaybackRate = m_requestedPlaybackRate = rate;
        invalidateCachedTime();
        scheduleEvent(eventNames().ratechangeEvent);
    }
}

void HTMLMediaElement::updatePlaybackRate()
{
    double requestedRate = requestedPlaybackRate();
    if (m_player && potentiallyPlaying() && m_player->rate() != requestedRate)
        m_player->setRate(requestedRate);
}

bool HTMLMediaElement::webkitPreservesPitch() const
{
    return m_webkitPreservesPitch;
}

void HTMLMediaElement::setWebkitPreservesPitch(bool preservesPitch)
{
    LOG(Media, "HTMLMediaElement::setWebkitPreservesPitch(%p) - %s", this, boolString(preservesPitch));

    m_webkitPreservesPitch = preservesPitch;
    
    if (!m_player)
        return;

    m_player->setPreservesPitch(preservesPitch);
}

bool HTMLMediaElement::ended() const
{
#if ENABLE(MEDIA_STREAM)
    // http://w3c.github.io/mediacapture-main/#mediastreams-in-media-elements
    // When the MediaStream state moves from the active to the inactive state, the User Agent
    // must raise an ended event on the HTMLMediaElement and set its ended attribute to true.
    if (m_mediaStreamSrcObject && m_player && m_player->ended())
        return true;
#endif

    // 4.8.10.8 Playing the media resource
    // The ended attribute must return true if the media element has ended 
    // playback and the direction of playback is forwards, and false otherwise.
    return endedPlayback() && requestedPlaybackRate() > 0;
}

bool HTMLMediaElement::autoplay() const
{
    return hasAttributeWithoutSynchronization(autoplayAttr);
}

String HTMLMediaElement::preload() const
{
#if ENABLE(MEDIA_STREAM)
    // http://w3c.github.io/mediacapture-main/#mediastreams-in-media-elements
    // "preload" - On getting: none. On setting: ignored.
    if (m_mediaStreamSrcObject)
        return ASCIILiteral("none");
#endif

    switch (m_preload) {
    case MediaPlayer::None:
        return ASCIILiteral("none");
    case MediaPlayer::MetaData:
        return ASCIILiteral("metadata");
    case MediaPlayer::Auto:
        return ASCIILiteral("auto");
    }

    ASSERT_NOT_REACHED();
    return String();
}

void HTMLMediaElement::setPreload(const String& preload)
{
    LOG(Media, "HTMLMediaElement::setPreload(%p) - %s", this, preload.utf8().data());
#if ENABLE(MEDIA_STREAM)
    // http://w3c.github.io/mediacapture-main/#mediastreams-in-media-elements
    // "preload" - On getting: none. On setting: ignored.
    if (m_mediaStreamSrcObject)
        return;
#endif

    setAttributeWithoutSynchronization(preloadAttr, preload);
}

void HTMLMediaElement::play(DOMPromiseDeferred<void>&& promise)
{
    LOG(Media, "HTMLMediaElement::play(%p)", this);

    auto success = m_mediaSession->playbackPermitted(*this);
    if (!success) {
        if (success.value() == MediaPlaybackDenialReason::UserGestureRequired)
            setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);
        promise.reject(NotAllowedError);
        return;
    }

    if (m_error && m_error->code() == MediaError::MEDIA_ERR_SRC_NOT_SUPPORTED) {
        promise.reject(NOT_SUPPORTED_ERR, "The operation is not supported.");
        return;
    }

    if (processingUserGestureForMedia())
        removeBehaviorsRestrictionsAfterFirstUserGesture();

    if (!playInternal()) {
        promise.reject(NotAllowedError);
        return;
    }

    m_pendingPlayPromises.append(WTFMove(promise));
}

void HTMLMediaElement::play()
{
    LOG(Media, "HTMLMediaElement::play(%p)", this);

    auto success = m_mediaSession->playbackPermitted(*this);
    if (!success) {
        if (success.value() == MediaPlaybackDenialReason::UserGestureRequired)
            setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);
        return;
    }
    if (processingUserGestureForMedia())
        removeBehaviorsRestrictionsAfterFirstUserGesture();

    playInternal();
}

bool HTMLMediaElement::playInternal()
{
    LOG(Media, "HTMLMediaElement::playInternal(%p)", this);
    
    if (!m_mediaSession->clientWillBeginPlayback()) {
        LOG(Media, "  returning because of interruption");
        return true; // Treat as success because we will begin playback on cessation of the interruption.
    }

    // 4.8.10.9. Playing the media resource
    if (!m_player || m_networkState == NETWORK_EMPTY)
        prepareForLoad();

    if (endedPlayback())
        seekInternal(MediaTime::zeroTime());

    if (m_mediaController)
        m_mediaController->bringElementUpToSpeed(*this);

    if (m_paused) {
        m_paused = false;
        invalidateCachedTime();
        m_playbackStartedTime = currentMediaTime().toDouble();
        scheduleEvent(eventNames().playEvent);

        if (m_readyState <= HAVE_CURRENT_DATA)
            scheduleEvent(eventNames().waitingEvent);
        else if (m_readyState >= HAVE_FUTURE_DATA)
            scheduleNotifyAboutPlaying();

#if ENABLE(MEDIA_SESSION)
        // 6.3 Activating a media session from a media element
        // When the play() method is invoked, the paused attribute is true, and the readyState attribute has the value
        // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, then
        // 1. Let media session be the value of the current media session.
        // 2. If we are not currently in media session's list of active participating media elements then append
        //    ourselves to this list.
        // 3. Let activated be the result of running the media session invocation algorithm for media session.
        // 4. If activated is failure, pause ourselves.
        if (m_readyState == HAVE_ENOUGH_DATA || m_readyState == HAVE_FUTURE_DATA) {
            if (m_session) {
                m_session->addActiveMediaElement(*this);

                if (m_session->kind() == MediaSessionKind::Content) {
                    if (Page* page = document().page())
                        page->chrome().client().focusedContentMediaElementDidChange(m_elementID);
                }

                if (!m_session->invoke()) {
                    pause();
                    return false;
                }
            }
        }
#endif
    } else if (m_readyState >= HAVE_FUTURE_DATA)
        scheduleResolvePendingPlayPromises();

    if (processingUserGestureForMedia()) {
        if (m_playbackWithoutUserGesture == PlaybackWithoutUserGesture::Prevented) {
            handleAutoplayEvent(AutoplayEvent::DidPlayMediaPreventedFromPlaying);
            setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::None);
        }
    } else
        setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Started);

    m_autoplaying = false;
    updatePlayState();

    return true;
}

void HTMLMediaElement::pause()
{
    LOG(Media, "HTMLMediaElement::pause(%p)", this);
    
    m_temporarilyAllowingInlinePlaybackAfterFullscreen = false;
    
    if (!m_mediaSession->playbackPermitted(*this))
        return;

    if (processingUserGestureForMedia())
        removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::RequireUserGestureToControlControlsManager);

    pauseInternal();
}


void HTMLMediaElement::pauseInternal()
{
    LOG(Media, "HTMLMediaElement::pauseInternal(%p)", this);

    if (!m_mediaSession->clientWillPausePlayback()) {
        LOG(Media, "  returning because of interruption");
        return;
    }

    // 4.8.10.9. Playing the media resource
    if (!m_player || m_networkState == NETWORK_EMPTY) {
        // Unless the restriction on media requiring user action has been lifted
        // don't trigger loading if a script calls pause().
        if (!m_mediaSession->playbackPermitted(*this))
            return;
        prepareForLoad();
    }

    m_autoplaying = false;

    if (processingUserGestureForMedia())
        userDidInterfereWithAutoplay();

    setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::None);

    if (!m_paused) {
        m_paused = true;
        scheduleTimeupdateEvent(false);
        scheduleEvent(eventNames().pauseEvent);
        m_promiseTaskQueue.enqueueTask([this]() {
            rejectPendingPlayPromises(DOMError::create("AbortError", "The operation was aborted."));
        });
        if (MemoryPressureHandler::singleton().isUnderMemoryPressure())
            purgeBufferedDataIfPossible();
    }

    updatePlayState();
}

#if ENABLE(MEDIA_SOURCE)

void HTMLMediaElement::detachMediaSource()
{
    if (!m_mediaSource)
        return;

    m_mediaSource->detachFromElement(*this);
    m_mediaSource = nullptr;
}

#endif

bool HTMLMediaElement::loop() const
{
    return hasAttributeWithoutSynchronization(loopAttr);
}

void HTMLMediaElement::setLoop(bool b)
{
    LOG(Media, "HTMLMediaElement::setLoop(%p) - %s", this, boolString(b));
    setBooleanAttribute(loopAttr, b);
}

bool HTMLMediaElement::controls() const
{
    Frame* frame = document().frame();

    // always show controls when scripting is disabled
    if (frame && !frame->script().canExecuteScripts(NotAboutToExecuteScript))
        return true;

    return hasAttributeWithoutSynchronization(controlsAttr);
}

void HTMLMediaElement::setControls(bool b)
{
    LOG(Media, "HTMLMediaElement::setControls(%p) - %s", this, boolString(b));
    setBooleanAttribute(controlsAttr, b);
}

double HTMLMediaElement::volume() const
{
    return m_volume;
}

ExceptionOr<void> HTMLMediaElement::setVolume(double volume)
{
    LOG(Media, "HTMLMediaElement::setVolume(%p) - %f", this, volume);

    if (!(volume >= 0 && volume <= 1))
        return Exception { INDEX_SIZE_ERR };

#if !PLATFORM(IOS)
    if (m_volume == volume)
        return { };

    if (volume && processingUserGestureForMedia())
        removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);

    m_volume = volume;
    m_volumeInitialized = true;
    updateVolume();
    scheduleEvent(eventNames().volumechangeEvent);

    if (isPlaying() && !m_mediaSession->playbackPermitted(*this)) {
        pauseInternal();
        setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);
    }
#endif
    return { };
}

bool HTMLMediaElement::muted() const
{
    return m_explicitlyMuted ? m_muted : hasAttributeWithoutSynchronization(mutedAttr);
}

void HTMLMediaElement::setMuted(bool muted)
{
    LOG(Media, "HTMLMediaElement::setMuted(%p) - %s", this, boolString(muted));

    bool mutedStateChanged = m_muted != muted;
    if (mutedStateChanged || !m_explicitlyMuted) {
        if (processingUserGestureForMedia()) {
            removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::AllRestrictions & ~MediaElementSession::RequireUserGestureToControlControlsManager);

            if (hasAudio() && muted)
                userDidInterfereWithAutoplay();
        }

        m_muted = muted;
        m_explicitlyMuted = true;

        // Avoid recursion when the player reports volume changes.
        if (!processingMediaPlayerCallback()) {
            if (m_player) {
                m_player->setMuted(effectiveMuted());
                if (hasMediaControls())
                    mediaControls()->changedMute();
            }
        }

        if (mutedStateChanged)
            scheduleEvent(eventNames().volumechangeEvent);

        updateShouldPlay();

#if ENABLE(MEDIA_SESSION)
        document().updateIsPlayingMedia(m_elementID);
#else
        document().updateIsPlayingMedia();
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
        updateMediaState(UpdateState::Asynchronously);
#endif
        m_mediaSession->canProduceAudioChanged();
    }

    scheduleUpdatePlaybackControlsManager();
}

#if USE(AUDIO_SESSION) && PLATFORM(MAC)
void HTMLMediaElement::hardwareMutedStateDidChange(AudioSession* session)
{
    if (!session->isMuted())
        return;

    if (!hasAudio())
        return;

    if (effectiveMuted() || !volume())
        return;

    userDidInterfereWithAutoplay();
}
#endif

void HTMLMediaElement::togglePlayState()
{
    LOG(Media, "HTMLMediaElement::togglePlayState(%p) - canPlay() is %s", this, boolString(canPlay()));

    // We can safely call the internal play/pause methods, which don't check restrictions, because
    // this method is only called from the built-in media controller
    if (canPlay()) {
        updatePlaybackRate();
        playInternal();
    } else 
        pauseInternal();
}

void HTMLMediaElement::beginScrubbing()
{
    LOG(Media, "HTMLMediaElement::beginScrubbing(%p) - paused() is %s", this, boolString(paused()));

    if (!paused()) {
        if (ended()) {
            // Because a media element stays in non-paused state when it reaches end, playback resumes 
            // when the slider is dragged from the end to another position unless we pause first. Do 
            // a "hard pause" so an event is generated, since we want to stay paused after scrubbing finishes.
            pause();
        } else {
            // Not at the end but we still want to pause playback so the media engine doesn't try to
            // continue playing during scrubbing. Pause without generating an event as we will 
            // unpause after scrubbing finishes.
            setPausedInternal(true);
        }
    }

    m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequireUserGestureToControlControlsManager);
}

void HTMLMediaElement::endScrubbing()
{
    LOG(Media, "HTMLMediaElement::endScrubbing(%p) - m_pausedInternal is %s", this, boolString(m_pausedInternal));

    if (m_pausedInternal)
        setPausedInternal(false);
}

void HTMLMediaElement::beginScanning(ScanDirection direction)
{
    m_scanType = supportsScanning() ? Scan : Seek;
    m_scanDirection = direction;

    if (m_scanType == Seek) {
        // Scanning by seeking requires the video to be paused during scanning.
        m_actionAfterScan = paused() ? Nothing : Play;
        pause();
    } else {
        // Scanning by scanning requires the video to be playing during scanninging.
        m_actionAfterScan = paused() ? Pause : Nothing;
        play();
        setPlaybackRate(nextScanRate());
    }

    m_scanTimer.start(0_s, m_scanType == Seek ? SeekRepeatDelay : ScanRepeatDelay);
}

void HTMLMediaElement::endScanning()
{
    if (m_scanType == Scan)
        setPlaybackRate(defaultPlaybackRate());

    if (m_actionAfterScan == Play)
        play();
    else if (m_actionAfterScan == Pause)
        pause();

    if (m_scanTimer.isActive())
        m_scanTimer.stop();
}

double HTMLMediaElement::nextScanRate()
{
    double rate = std::min(ScanMaximumRate, fabs(playbackRate() * 2));
    if (m_scanDirection == Backward)
        rate *= -1;
#if PLATFORM(IOS)
    rate = std::min(std::max(rate, minFastReverseRate()), maxFastForwardRate());
#endif
    return rate;
}

void HTMLMediaElement::scanTimerFired()
{
    if (m_scanType == Seek) {
        double seekTime = m_scanDirection == Forward ? SeekTime : -SeekTime;
        setCurrentTime(currentTime() + seekTime);
    } else
        setPlaybackRate(nextScanRate());
}

// The spec says to fire periodic timeupdate events (those sent while playing) every
// "15 to 250ms", we choose the slowest frequency
static const Seconds maxTimeupdateEventFrequency { 250_ms };

void HTMLMediaElement::startPlaybackProgressTimer()
{
    if (m_playbackProgressTimer.isActive())
        return;

    m_previousProgressTime = monotonicallyIncreasingTime();
    m_playbackProgressTimer.startRepeating(maxTimeupdateEventFrequency);
}

void HTMLMediaElement::playbackProgressTimerFired()
{
    ASSERT(m_player);

    if (m_fragmentEndTime.isValid() && currentMediaTime() >= m_fragmentEndTime && requestedPlaybackRate() > 0) {
        m_fragmentEndTime = MediaTime::invalidTime();
        if (!m_mediaController && !m_paused) {
            // changes paused to true and fires a simple event named pause at the media element.
            pauseInternal();
        }
    }
    
    scheduleTimeupdateEvent(true);

    if (!requestedPlaybackRate())
        return;

    if (!m_paused && hasMediaControls())
        mediaControls()->playbackProgressed();

#if ENABLE(VIDEO_TRACK)
    updateActiveTextTrackCues(currentMediaTime());
#endif

#if ENABLE(MEDIA_SOURCE)
    if (m_mediaSource)
        m_mediaSource->monitorSourceBuffers();
#endif

    if (!seeking() && m_playbackWithoutUserGesture == PlaybackWithoutUserGesture::Started && currentTime() - m_playbackWithoutUserGestureStartedTime->toDouble() > AutoplayInterferenceTimeThreshold) {
        handleAutoplayEvent(AutoplayEvent::DidAutoplayMediaPastThresholdWithoutUserInterference);
        setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::None);
    }
}

void HTMLMediaElement::scheduleTimeupdateEvent(bool periodicEvent)
{
    MonotonicTime now = MonotonicTime::now();
    Seconds timedelta = now - m_clockTimeAtLastUpdateEvent;

    // throttle the periodic events
    if (periodicEvent && timedelta < maxTimeupdateEventFrequency)
        return;

    // Some media engines make multiple "time changed" callbacks at the same time, but we only want one
    // event at a given time so filter here
    MediaTime movieTime = currentMediaTime();
    if (movieTime != m_lastTimeUpdateEventMovieTime) {
        scheduleEvent(eventNames().timeupdateEvent);
        m_clockTimeAtLastUpdateEvent = now;
        m_lastTimeUpdateEventMovieTime = movieTime;
    }
}

bool HTMLMediaElement::canPlay() const
{
    return paused() || ended() || m_readyState < HAVE_METADATA;
}

double HTMLMediaElement::percentLoaded() const
{
    if (!m_player)
        return 0;
    MediaTime duration = m_player->duration();

    if (!duration || duration.isPositiveInfinite() || duration.isNegativeInfinite())
        return 0;

    MediaTime buffered = MediaTime::zeroTime();
    bool ignored;
    std::unique_ptr<PlatformTimeRanges> timeRanges = m_player->buffered();
    for (unsigned i = 0; i < timeRanges->length(); ++i) {
        MediaTime start = timeRanges->start(i, ignored);
        MediaTime end = timeRanges->end(i, ignored);
        buffered += end - start;
    }
    return buffered.toDouble() / duration.toDouble();
}

#if ENABLE(VIDEO_TRACK)

void HTMLMediaElement::mediaPlayerDidAddAudioTrack(AudioTrackPrivate& track)
{
    if (isPlaying() && !m_mediaSession->playbackPermitted(*this)) {
        pauseInternal();
        setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);
    }

    addAudioTrack(AudioTrack::create(*this, track));
}

void HTMLMediaElement::mediaPlayerDidAddTextTrack(InbandTextTrackPrivate& track)
{
    // 4.8.10.12.2 Sourcing in-band text tracks
    // 1. Associate the relevant data with a new text track and its corresponding new TextTrack object.
    auto textTrack = InbandTextTrack::create(*ActiveDOMObject::scriptExecutionContext(), *this, track);
    textTrack->setMediaElement(this);
    
    // 2. Set the new text track's kind, label, and language based on the semantics of the relevant data,
    // as defined by the relevant specification. If there is no label in that data, then the label must
    // be set to the empty string.
    // 3. Associate the text track list of cues with the rules for updating the text track rendering appropriate
    // for the format in question.
    // 4. If the new text track's kind is metadata, then set the text track in-band metadata track dispatch type
    // as follows, based on the type of the media resource:
    // 5. Populate the new text track's list of cues with the cues parsed so far, folllowing the guidelines for exposing
    // cues, and begin updating it dynamically as necessary.
    //   - Thess are all done by the media engine.
    
    // 6. Set the new text track's readiness state to loaded.
    textTrack->setReadinessState(TextTrack::Loaded);
    
    // 7. Set the new text track's mode to the mode consistent with the user's preferences and the requirements of
    // the relevant specification for the data.
    //  - This will happen in configureTextTracks()
    scheduleDelayedAction(ConfigureTextTracks);
    
    // 8. Add the new text track to the media element's list of text tracks.
    // 9. Fire an event with the name addtrack, that does not bubble and is not cancelable, and that uses the TrackEvent
    // interface, with the track attribute initialized to the text track's TextTrack object, at the media element's
    // textTracks attribute's TextTrackList object.
    addTextTrack(WTFMove(textTrack));
}

void HTMLMediaElement::mediaPlayerDidAddVideoTrack(VideoTrackPrivate& track)
{
    addVideoTrack(VideoTrack::create(*this, track));
}

void HTMLMediaElement::mediaPlayerDidRemoveAudioTrack(AudioTrackPrivate& track)
{
    track.willBeRemoved();
}

void HTMLMediaElement::mediaPlayerDidRemoveTextTrack(InbandTextTrackPrivate& track)
{
    track.willBeRemoved();
}

void HTMLMediaElement::mediaPlayerDidRemoveVideoTrack(VideoTrackPrivate& track)
{
    track.willBeRemoved();
}

void HTMLMediaElement::closeCaptionTracksChanged()
{
    if (hasMediaControls())
        mediaControls()->closedCaptionTracksChanged();
}

void HTMLMediaElement::addAudioTrack(Ref<AudioTrack>&& track)
{
    audioTracks().append(WTFMove(track));
}

void HTMLMediaElement::addTextTrack(Ref<TextTrack>&& track)
{
    if (!m_requireCaptionPreferencesChangedCallbacks) {
        m_requireCaptionPreferencesChangedCallbacks = true;
        Document& document = this->document();
        document.registerForCaptionPreferencesChangedCallbacks(this);
        if (Page* page = document.page())
            m_captionDisplayMode = page->group().captionPreferences().captionDisplayMode();
    }

    textTracks().append(WTFMove(track));

    closeCaptionTracksChanged();
}

void HTMLMediaElement::addVideoTrack(Ref<VideoTrack>&& track)
{
    videoTracks().append(WTFMove(track));
}

void HTMLMediaElement::removeAudioTrack(AudioTrack& track)
{
    m_audioTracks->remove(track);
    track.clearClient();
}

void HTMLMediaElement::removeTextTrack(TextTrack& track, bool scheduleEvent)
{
    TrackDisplayUpdateScope scope { *this };
    if (auto* cues = track.cues())
        textTrackRemoveCues(track, *cues);
    track.clearClient();
    if (m_textTracks)
        m_textTracks->remove(track, scheduleEvent);

    closeCaptionTracksChanged();
}

void HTMLMediaElement::removeVideoTrack(VideoTrack& track)
{
    m_videoTracks->remove(track);
    track.clearClient();
}

void HTMLMediaElement::forgetResourceSpecificTracks()
{
    while (m_audioTracks &&  m_audioTracks->length())
        removeAudioTrack(*m_audioTracks->lastItem());

    if (m_textTracks) {
        TrackDisplayUpdateScope scope { *this };
        for (int i = m_textTracks->length() - 1; i >= 0; --i) {
            auto& track = *m_textTracks->item(i);
            if (track.trackType() == TextTrack::InBand)
                removeTextTrack(track, false);
        }
    }

    while (m_videoTracks &&  m_videoTracks->length())
        removeVideoTrack(*m_videoTracks->lastItem());
}

ExceptionOr<TextTrack&> HTMLMediaElement::addTextTrack(const String& kind, const String& label, const String& language)
{
    // 4.8.10.12.4 Text track API
    // The addTextTrack(kind, label, language) method of media elements, when invoked, must run the following steps:

    // 1. If kind is not one of the following strings, then throw a SyntaxError exception and abort these steps
    if (!TextTrack::isValidKindKeyword(kind))
        return Exception { TypeError };

    // 2. If the label argument was omitted, let label be the empty string.
    // 3. If the language argument was omitted, let language be the empty string.
    // 4. Create a new TextTrack object.

    // 5. Create a new text track corresponding to the new object, and set its text track kind to kind, its text 
    // track label to label, its text track language to language...
    auto track = TextTrack::create(ActiveDOMObject::scriptExecutionContext(), this, kind, emptyString(), label, language);
    auto& trackReference = track.get();

    // Note, due to side effects when changing track parameters, we have to
    // first append the track to the text track list.

    // 6. Add the new text track to the media element's list of text tracks.
    addTextTrack(WTFMove(track));

    // ... its text track readiness state to the text track loaded state ...
    trackReference.setReadinessState(TextTrack::Loaded);

    // ... its text track mode to the text track hidden mode, and its text track list of cues to an empty list ...
    trackReference.setMode(TextTrack::Mode::Hidden);

    return trackReference;
}

AudioTrackList& HTMLMediaElement::audioTracks()
{
    if (!m_audioTracks)
        m_audioTracks = AudioTrackList::create(this, ActiveDOMObject::scriptExecutionContext());

    return *m_audioTracks;
}

TextTrackList& HTMLMediaElement::textTracks()
{
    if (!m_textTracks)
        m_textTracks = TextTrackList::create(this, ActiveDOMObject::scriptExecutionContext());

    return *m_textTracks;
}

VideoTrackList& HTMLMediaElement::videoTracks()
{
    if (!m_videoTracks)
        m_videoTracks = VideoTrackList::create(this, ActiveDOMObject::scriptExecutionContext());

    return *m_videoTracks;
}

void HTMLMediaElement::didAddTextTrack(HTMLTrackElement& trackElement)
{
    ASSERT(trackElement.hasTagName(trackTag));

    // 4.8.10.12.3 Sourcing out-of-band text tracks
    // When a track element's parent element changes and the new parent is a media element, 
    // then the user agent must add the track element's corresponding text track to the 
    // media element's list of text tracks ... [continues in TextTrackList::append]
    addTextTrack(trackElement.track());

    // Do not schedule the track loading until parsing finishes so we don't start before all tracks
    // in the markup have been added.
    if (!m_parsingInProgress)
        scheduleDelayedAction(ConfigureTextTracks);

    if (hasMediaControls())
        mediaControls()->closedCaptionTracksChanged();
}

void HTMLMediaElement::didRemoveTextTrack(HTMLTrackElement& trackElement)
{
    ASSERT(trackElement.hasTagName(trackTag));

#if !LOG_DISABLED
    if (trackElement.hasTagName(trackTag)) {
        URL url = trackElement.getNonEmptyURLAttribute(srcAttr);
        LOG(Media, "HTMLMediaElement::didRemoveTrack(%p) - 'src' is %s", this, urlForLoggingMedia(url).utf8().data());
    }
#endif

    auto& textTrack = trackElement.track();

    textTrack.setHasBeenConfigured(false);

    if (!m_textTracks)
        return;
    
    // 4.8.10.12.3 Sourcing out-of-band text tracks
    // When a track element's parent element changes and the old parent was a media element, 
    // then the user agent must remove the track element's corresponding text track from the 
    // media element's list of text tracks.
    removeTextTrack(textTrack);

    m_textTracksWhenResourceSelectionBegan.removeFirst(&textTrack);
}

void HTMLMediaElement::configureTextTrackGroup(const TrackGroup& group)
{
    ASSERT(group.tracks.size());

    LOG(Media, "HTMLMediaElement::configureTextTrackGroup(%p)", this);

    Page* page = document().page();
    CaptionUserPreferences* captionPreferences = page ? &page->group().captionPreferences() : 0;
    CaptionUserPreferences::CaptionDisplayMode displayMode = captionPreferences ? captionPreferences->captionDisplayMode() : CaptionUserPreferences::Automatic;

    // First, find the track in the group that should be enabled (if any).
    Vector<RefPtr<TextTrack>> currentlyEnabledTracks;
    RefPtr<TextTrack> trackToEnable;
    RefPtr<TextTrack> defaultTrack;
    RefPtr<TextTrack> fallbackTrack;
    RefPtr<TextTrack> forcedSubitleTrack;
    int highestTrackScore = 0;
    int highestForcedScore = 0;

    // If there is a visible track, it has already been configured so it won't be considered in the loop below. We don't want to choose another
    // track if it is less suitable, and we do want to disable it if another track is more suitable.
    int alreadyVisibleTrackScore = 0;
    if (group.visibleTrack && captionPreferences) {
        alreadyVisibleTrackScore = captionPreferences->textTrackSelectionScore(group.visibleTrack.get(), this);
        currentlyEnabledTracks.append(group.visibleTrack);
    }

    for (size_t i = 0; i < group.tracks.size(); ++i) {
        RefPtr<TextTrack> textTrack = group.tracks[i];

        if (m_processingPreferenceChange && textTrack->mode() == TextTrack::Mode::Showing)
            currentlyEnabledTracks.append(textTrack);

        int trackScore = captionPreferences ? captionPreferences->textTrackSelectionScore(textTrack.get(), this) : 0;
        LOG(Media, "HTMLMediaElement::configureTextTrackGroup(%p) -  '%s' track with language '%s' and BCP 47 language '%s' has score %d", this, textTrack->kindKeyword().string().utf8().data(), textTrack->language().string().utf8().data(), textTrack->validBCP47Language().string().utf8().data(), trackScore);

        if (trackScore) {

            // * If the text track kind is { [subtitles or captions] [descriptions] } and the user has indicated an interest in having a
            // track with this text track kind, text track language, and text track label enabled, and there is no
            // other text track in the media element's list of text tracks with a text track kind of either subtitles
            // or captions whose text track mode is showing
            // ...
            // * If the text track kind is chapters and the text track language is one that the user agent has reason
            // to believe is appropriate for the user, and there is no other text track in the media element's list of
            // text tracks with a text track kind of chapters whose text track mode is showing
            //    Let the text track mode be showing.
            if (trackScore > highestTrackScore && trackScore > alreadyVisibleTrackScore) {
                highestTrackScore = trackScore;
                trackToEnable = textTrack;
            }

            if (!defaultTrack && textTrack->isDefault())
                defaultTrack = textTrack;
            if (!defaultTrack && !fallbackTrack)
                fallbackTrack = textTrack;
            if (textTrack->containsOnlyForcedSubtitles() && trackScore > highestForcedScore) {
                forcedSubitleTrack = textTrack;
                highestForcedScore = trackScore;
            }
        } else if (!group.visibleTrack && !defaultTrack && textTrack->isDefault()) {
            // * If the track element has a default attribute specified, and there is no other text track in the media
            // element's list of text tracks whose text track mode is showing or showing by default
            //    Let the text track mode be showing by default.
            if (group.kind != TrackGroup::CaptionsAndSubtitles || displayMode != CaptionUserPreferences::ForcedOnly)
                defaultTrack = textTrack;
        }
    }

    if (displayMode != CaptionUserPreferences::Manual) {
        if (!trackToEnable && defaultTrack)
            trackToEnable = defaultTrack;

        // If no track matches the user's preferred language, none was marked as 'default', and there is a forced subtitle track
        // in the same language as the language of the primary audio track, enable it.
        if (!trackToEnable && forcedSubitleTrack)
            trackToEnable = forcedSubitleTrack;

        // If no track matches, don't disable an already visible track unless preferences say they all should be off.
        if (group.kind != TrackGroup::CaptionsAndSubtitles || displayMode != CaptionUserPreferences::ForcedOnly) {
            if (!trackToEnable && !defaultTrack && group.visibleTrack)
                trackToEnable = group.visibleTrack;
        }

        // If no track matches the user's preferred language and non was marked 'default', enable the first track
        // because the user has explicitly stated a preference for this kind of track.
        if (!trackToEnable && fallbackTrack)
            trackToEnable = fallbackTrack;

        if (trackToEnable)
            m_subtitleTrackLanguage = trackToEnable->language();
        else
            m_subtitleTrackLanguage = emptyString();
    }

    if (currentlyEnabledTracks.size()) {
        for (size_t i = 0; i < currentlyEnabledTracks.size(); ++i) {
            RefPtr<TextTrack> textTrack = currentlyEnabledTracks[i];
            if (textTrack != trackToEnable)
                textTrack->setMode(TextTrack::Mode::Disabled);
        }
    }

    if (trackToEnable) {
        trackToEnable->setMode(TextTrack::Mode::Showing);

        // If user preferences indicate we should always display captions, make sure we reflect the
        // proper status via the webkitClosedCaptionsVisible API call:
        if (!webkitClosedCaptionsVisible() && closedCaptionsVisible() && displayMode == CaptionUserPreferences::AlwaysOn)
            m_webkitLegacyClosedCaptionOverride = true;
    }

    m_processingPreferenceChange = false;
}

static JSC::JSValue controllerJSValue(JSC::ExecState& exec, JSDOMGlobalObject& globalObject, HTMLMediaElement& media)
{
    JSC::VM& vm = globalObject.vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    auto mediaJSWrapper = toJS(&exec, &globalObject, media);
    
    // Retrieve the controller through the JS object graph
    JSC::JSObject* mediaJSWrapperObject = jsDynamicDowncast<JSC::JSObject*>(vm, mediaJSWrapper);
    if (!mediaJSWrapperObject)
        return JSC::jsNull();
    
    JSC::Identifier controlsHost = JSC::Identifier::fromString(&vm, "controlsHost");
    JSC::JSValue controlsHostJSWrapper = mediaJSWrapperObject->get(&exec, controlsHost);
    RETURN_IF_EXCEPTION(scope, JSC::jsNull());

    JSC::JSObject* controlsHostJSWrapperObject = jsDynamicDowncast<JSC::JSObject*>(vm, controlsHostJSWrapper);
    if (!controlsHostJSWrapperObject)
        return JSC::jsNull();

    JSC::Identifier controllerID = JSC::Identifier::fromString(&vm, "controller");
    JSC::JSValue controllerJSWrapper = controlsHostJSWrapperObject->get(&exec, controllerID);
    RETURN_IF_EXCEPTION(scope, JSC::jsNull());

    return controllerJSWrapper;
}

void HTMLMediaElement::ensureMediaControlsShadowRoot()
{
    ASSERT(!m_creatingControls);
    m_creatingControls = true;
    ensureUserAgentShadowRoot();
    m_creatingControls = false;
}

void HTMLMediaElement::updateCaptionContainer()
{
    LOG(Media, "HTMLMediaElement::updateCaptionContainer(%p)", this);
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (m_haveSetUpCaptionContainer)
        return;

    Page* page = document().page();
    if (!page)
        return;

    DOMWrapperWorld& world = ensureIsolatedWorld();

    if (!ensureMediaControlsInjectedScript())
        return;

    ensureMediaControlsShadowRoot();

    if (!m_mediaControlsHost)
        m_mediaControlsHost = MediaControlsHost::create(this);

    ScriptController& scriptController = document().frame()->script();
    JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
    JSC::VM& vm = globalObject->vm();
    JSC::JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::ExecState* exec = globalObject->globalExec();

    JSC::JSValue controllerValue = controllerJSValue(*exec, *globalObject, *this);
    JSC::JSObject* controllerObject = jsDynamicDowncast<JSC::JSObject*>(vm, controllerValue);
    if (!controllerObject)
        return;

    // The media controls script must provide a method on the Controller object with the following details.
    // Name: updateCaptionContainer
    // Parameters:
    //     None
    // Return value:
    //     None
    JSC::JSValue methodValue = controllerObject->get(exec, JSC::Identifier::fromString(exec, "updateCaptionContainer"));
    JSC::JSObject* methodObject = jsDynamicDowncast<JSC::JSObject*>(vm, methodValue);
    if (!methodObject)
        return;

    JSC::CallData callData;
    JSC::CallType callType = methodObject->methodTable()->getCallData(methodObject, callData);
    if (callType == JSC::CallType::None)
        return;

    JSC::MarkedArgumentBuffer noArguments;
    JSC::call(exec, methodObject, callType, callData, controllerObject, noArguments);
    scope.clearException();

    m_haveSetUpCaptionContainer = true;
#endif
}

void HTMLMediaElement::layoutSizeChanged()
{
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (auto* frameView = document().view()) {
        auto task = [this, protectedThis = makeRef(*this)] {
            if (ShadowRoot* root = userAgentShadowRoot())
                root->dispatchEvent(Event::create("resize", false, false));
        };
        frameView->queuePostLayoutCallback(WTFMove(task));
    }
#endif

    if (!m_receivedLayoutSizeChanged) {
        m_receivedLayoutSizeChanged = true;
        scheduleUpdatePlaybackControlsManager();
    }

    // If the video is a candidate for main content, we should register it for viewport visibility callbacks
    // if it hasn't already been registered.
    if (renderer() && m_mediaSession && !m_mediaSession->wantsToObserveViewportVisibilityForAutoplay() && m_mediaSession->wantsToObserveViewportVisibilityForMediaControls())
        renderer()->registerForVisibleInViewportCallback();
}

void HTMLMediaElement::visibilityDidChange()
{
    updateShouldAutoplay();
}

void HTMLMediaElement::setSelectedTextTrack(TextTrack* trackToSelect)
{
    TextTrackList& trackList = textTracks();
    if (!trackList.length())
        return;

    if (trackToSelect == TextTrack::captionMenuAutomaticItem()) {
        if (m_captionDisplayMode != CaptionUserPreferences::Automatic)
            m_textTracks->scheduleChangeEvent();
    } else if (trackToSelect == TextTrack::captionMenuOffItem()) {
        for (int i = 0, length = trackList.length(); i < length; ++i)
            trackList.item(i)->setMode(TextTrack::Mode::Disabled);

        if (m_captionDisplayMode != CaptionUserPreferences::ForcedOnly && !trackList.isChangeEventScheduled())
            m_textTracks->scheduleChangeEvent();
    } else {
        if (!trackToSelect || !trackList.contains(*trackToSelect))
            return;

        for (int i = 0, length = trackList.length(); i < length; ++i) {
            auto& track = *trackList.item(i);
            if (&track != trackToSelect)
                track.setMode(TextTrack::Mode::Disabled);
            else
                track.setMode(TextTrack::Mode::Showing);
        }
    }

    if (!document().page())
        return;

    auto& captionPreferences = document().page()->group().captionPreferences();
    CaptionUserPreferences::CaptionDisplayMode displayMode;
    if (trackToSelect == TextTrack::captionMenuOffItem())
        displayMode = CaptionUserPreferences::ForcedOnly;
    else if (trackToSelect == TextTrack::captionMenuAutomaticItem())
        displayMode = CaptionUserPreferences::Automatic;
    else {
        displayMode = CaptionUserPreferences::AlwaysOn;
        if (trackToSelect->validBCP47Language().length())
            captionPreferences.setPreferredLanguage(trackToSelect->validBCP47Language());
    }

    captionPreferences.setCaptionDisplayMode(displayMode);
}

void HTMLMediaElement::configureTextTracks()
{
    TrackGroup captionAndSubtitleTracks(TrackGroup::CaptionsAndSubtitles);
    TrackGroup descriptionTracks(TrackGroup::Description);
    TrackGroup chapterTracks(TrackGroup::Chapter);
    TrackGroup metadataTracks(TrackGroup::Metadata);
    TrackGroup otherTracks(TrackGroup::Other);

    if (!m_textTracks)
        return;

    for (size_t i = 0; i < m_textTracks->length(); ++i) {
        RefPtr<TextTrack> textTrack = m_textTracks->item(i);
        if (!textTrack)
            continue;

        auto kind = textTrack->kind();
        TrackGroup* currentGroup;
        if (kind == TextTrack::Kind::Subtitles || kind == TextTrack::Kind::Captions || kind == TextTrack::Kind::Forced)
            currentGroup = &captionAndSubtitleTracks;
        else if (kind == TextTrack::Kind::Descriptions)
            currentGroup = &descriptionTracks;
        else if (kind == TextTrack::Kind::Chapters)
            currentGroup = &chapterTracks;
        else if (kind == TextTrack::Kind::Metadata)
            currentGroup = &metadataTracks;
        else
            currentGroup = &otherTracks;

        if (!currentGroup->visibleTrack && textTrack->mode() == TextTrack::Mode::Showing)
            currentGroup->visibleTrack = textTrack;
        if (!currentGroup->defaultTrack && textTrack->isDefault())
            currentGroup->defaultTrack = textTrack;

        // Do not add this track to the group if it has already been automatically configured
        // as we only want to call configureTextTrack once per track so that adding another 
        // track after the initial configuration doesn't reconfigure every track - only those 
        // that should be changed by the new addition. For example all metadata tracks are 
        // disabled by default, and we don't want a track that has been enabled by script 
        // to be disabled automatically when a new metadata track is added later.
        if (textTrack->hasBeenConfigured())
            continue;
        
        if (textTrack->language().length())
            currentGroup->hasSrcLang = true;
        currentGroup->tracks.append(textTrack);
    }
    
    if (captionAndSubtitleTracks.tracks.size())
        configureTextTrackGroup(captionAndSubtitleTracks);
    if (descriptionTracks.tracks.size())
        configureTextTrackGroup(descriptionTracks);
    if (chapterTracks.tracks.size())
        configureTextTrackGroup(chapterTracks);
    if (metadataTracks.tracks.size())
        configureTextTrackGroup(metadataTracks);
    if (otherTracks.tracks.size())
        configureTextTrackGroup(otherTracks);

    updateCaptionContainer();
    configureTextTrackDisplay();
    if (hasMediaControls())
        mediaControls()->closedCaptionTracksChanged();
}
#endif

bool HTMLMediaElement::havePotentialSourceChild()
{
    // Stash the current <source> node and next nodes so we can restore them after checking
    // to see there is another potential.
    RefPtr<HTMLSourceElement> currentSourceNode = m_currentSourceNode;
    RefPtr<HTMLSourceElement> nextNode = m_nextChildNodeToConsider;

    URL nextURL = selectNextSourceChild(0, 0, DoNothing);

    m_currentSourceNode = currentSourceNode;
    m_nextChildNodeToConsider = nextNode;

    return nextURL.isValid();
}

URL HTMLMediaElement::selectNextSourceChild(ContentType* contentType, String* keySystem, InvalidURLAction actionIfInvalid)
{
    UNUSED_PARAM(keySystem);
#if !LOG_DISABLED
    // Don't log if this was just called to find out if there are any valid <source> elements.
    bool shouldLog = actionIfInvalid != DoNothing;
    if (shouldLog)
        LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p)", this);
#endif

    if (!m_nextChildNodeToConsider) {
#if !LOG_DISABLED
        if (shouldLog)
            LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p) - end of list, stopping", this);
#endif
        return URL();
    }

    // Because the DOM may be mutated in the course of the following algorithm,
    // keep strong references to each of the child source nodes, and verify that
    // each still is a child of this media element before using.
    Vector<Ref<HTMLSourceElement>> potentialSourceNodes;
    auto sources = childrenOfType<HTMLSourceElement>(*this);
    for (auto next = m_nextChildNodeToConsider ? sources.beginAt(*m_nextChildNodeToConsider) : sources.begin(), end = sources.end(); next != end; ++next)
        potentialSourceNodes.append(*next);

    for (auto& source : potentialSourceNodes) {
        if (source->parentNode() != this)
            continue;

        // If candidate does not have a src attribute, or if its src attribute's value is the empty string ... jump down to the failed step below
        auto mediaURL = source->getNonEmptyURLAttribute(srcAttr);
        String type;
#if !LOG_DISABLED
        if (shouldLog)
            LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p) - 'src' is %s", this, urlForLoggingMedia(mediaURL).utf8().data());
#endif
        if (mediaURL.isEmpty())
            goto CheckAgain;
        
        if (auto* media = source->parsedMediaAttribute()) {
#if !LOG_DISABLED
            if (shouldLog)
                LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p) - 'media' is %s", this, source->attributeWithoutSynchronization(mediaAttr).string().utf8().data());
#endif
            auto* renderer = this->renderer();
            if (!MediaQueryEvaluator { "screen", document(), renderer ? &renderer->style() : nullptr }.evaluate(*media))
                goto CheckAgain;
        }

        type = source->attributeWithoutSynchronization(typeAttr);
        if (type.isEmpty() && mediaURL.protocolIsData())
            type = mimeTypeFromDataURL(mediaURL);
        if (!type.isEmpty()) {
#if !LOG_DISABLED
            if (shouldLog)
                LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p) - 'type' is '%s'", this, type.utf8().data());
#endif
            MediaEngineSupportParameters parameters;
            parameters.type = ContentType(type);
            parameters.url = mediaURL;
#if ENABLE(MEDIA_SOURCE)
            parameters.isMediaSource = mediaURL.protocolIs(mediaSourceBlobProtocol);
#endif
#if ENABLE(MEDIA_STREAM)
            parameters.isMediaStream = mediaURL.protocolIs(mediaStreamBlobProtocol);
#endif
            if (!document().settings().allowMediaContentTypesRequiringHardwareSupportAsFallback() || Traversal<HTMLSourceElement>::nextSkippingChildren(source))
                parameters.contentTypesRequiringHardwareSupport = mediaContentTypesRequiringHardwareSupport();

            if (!MediaPlayer::supportsType(parameters, this))
                goto CheckAgain;
        }

        // Is it safe to load this url?
        if (!isSafeToLoadURL(mediaURL, actionIfInvalid) || !dispatchBeforeLoadEvent(mediaURL.string()))
            goto CheckAgain;

        // A 'beforeload' event handler can mutate the DOM, so check to see if the source element is still a child node.
        if (source->parentNode() != this) {
            LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p) - 'beforeload' removed current element", this);
            continue;
        }

        // Making it this far means the <source> looks reasonable.
        if (contentType)
            *contentType = ContentType(type);
        m_nextChildNodeToConsider = Traversal<HTMLSourceElement>::nextSkippingChildren(source);
        m_currentSourceNode = WTFMove(source);

#if !LOG_DISABLED
        if (shouldLog)
            LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p) -> %p, %s", this, m_currentSourceNode.get(), urlForLoggingMedia(mediaURL).utf8().data());
#endif

        return mediaURL;

CheckAgain:
        if (actionIfInvalid == Complain)
            source->scheduleErrorEvent();
    }

    m_currentSourceNode = nullptr;
    m_nextChildNodeToConsider = nullptr;

#if !LOG_DISABLED
    if (shouldLog)
        LOG(Media, "HTMLMediaElement::selectNextSourceChild(%p) -> %p, failed", this, m_currentSourceNode.get());
#endif
    return URL();
}

void HTMLMediaElement::sourceWasAdded(HTMLSourceElement& source)
{
    LOG(Media, "HTMLMediaElement::sourceWasAdded(%p) - %p", this, &source);

#if !LOG_DISABLED
    if (source.hasTagName(sourceTag)) {
        URL url = source.getNonEmptyURLAttribute(srcAttr);
        LOG(Media, "HTMLMediaElement::sourceWasAdded(%p) - 'src' is %s", this, urlForLoggingMedia(url).utf8().data());
    }
#endif
    
    // We should only consider a <source> element when there is not src attribute at all.
    if (hasAttributeWithoutSynchronization(srcAttr))
        return;

    // 4.8.8 - 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 (m_networkState == NETWORK_EMPTY) {
        m_nextChildNodeToConsider = &source;
#if PLATFORM(IOS)
        if (m_mediaSession->dataLoadingPermitted(*this))
#endif
            selectMediaResource();
        return;
    }

    if (m_currentSourceNode && &source == Traversal<HTMLSourceElement>::nextSibling(*m_currentSourceNode)) {
        LOG(Media, "HTMLMediaElement::sourceWasAdded(%p) - <source> inserted immediately after current source", this);
        m_nextChildNodeToConsider = &source;
        return;
    }

    if (m_nextChildNodeToConsider)
        return;
    
    // 4.8.9.5, resource selection algorithm, source elements section:
    // 21. Wait until the node after pointer is a node other than the end of the list. (This step might wait forever.)
    // 22. Asynchronously await a stable state...
    // 23. Set the element's delaying-the-load-event flag back to true (this delays the load event again, in case 
    // it hasn't been fired yet).
    setShouldDelayLoadEvent(true);

    // 24. Set the networkState back to NETWORK_LOADING.
    m_networkState = NETWORK_LOADING;
    
    // 25. Jump back to the find next candidate step above.
    m_nextChildNodeToConsider = &source;
    scheduleNextSourceChild();
}

void HTMLMediaElement::sourceWasRemoved(HTMLSourceElement& source)
{
    LOG(Media, "HTMLMediaElement::sourceWasRemoved(%p) - %p", this, &source);

#if !LOG_DISABLED
    if (source.hasTagName(sourceTag)) {
        URL url = source.getNonEmptyURLAttribute(srcAttr);
        LOG(Media, "HTMLMediaElement::sourceWasRemoved(%p) - 'src' is %s", this, urlForLoggingMedia(url).utf8().data());
    }
#endif

    if (&source != m_currentSourceNode && &source != m_nextChildNodeToConsider)
        return;

    if (&source == m_nextChildNodeToConsider) {
        m_nextChildNodeToConsider = m_currentSourceNode ? Traversal<HTMLSourceElement>::nextSibling(*m_currentSourceNode) : nullptr;
        LOG(Media, "HTMLMediaElement::sourceRemoved(%p) - m_nextChildNodeToConsider set to %p", this, m_nextChildNodeToConsider.get());
    } else if (&source == m_currentSourceNode) {
        // Clear the current source node pointer, but don't change the movie as the spec says:
        // 4.8.8 - Dynamically modifying a source element and its attribute when the element is already 
        // inserted in a video or audio element will have no effect.
        m_currentSourceNode = nullptr;
        LOG(Media, "HTMLMediaElement::sourceRemoved(%p) - m_currentSourceNode set to 0", this);
    }
}

void HTMLMediaElement::mediaPlayerTimeChanged(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerTimeChanged(%p)", this);

#if ENABLE(VIDEO_TRACK)
    updateActiveTextTrackCues(currentMediaTime());
#endif

    beginProcessingMediaPlayerCallback();

    invalidateCachedTime();
    bool wasSeeking = seeking();

    // 4.8.10.9 step 14 & 15.  Needed if no ReadyState change is associated with the seek.
    if (m_seekRequested && m_readyState >= HAVE_CURRENT_DATA && !m_player->seeking())
        finishSeek();
    
    // Always call scheduleTimeupdateEvent when the media engine reports a time discontinuity, 
    // it will only queue a 'timeupdate' event if we haven't already posted one at the current
    // movie time.
    else
        scheduleTimeupdateEvent(false);

    MediaTime now = currentMediaTime();
    MediaTime dur = durationMediaTime();
    double playbackRate = requestedPlaybackRate();

    // When the current playback position reaches the end of the media resource then the user agent must follow these steps:
    if (dur && dur.isValid() && !dur.isPositiveInfinite() && !dur.isNegativeInfinite()) {
        // If the media element has a loop attribute specified and does not have a current media controller,
        if (loop() && !m_mediaController && playbackRate > 0) {
            m_sentEndEvent = false;
            // then seek to the earliest possible position of the media resource and abort these steps when the direction of
            // playback is forwards,
            if (now >= dur)
                seekInternal(MediaTime::zeroTime());
        } else if ((now <= MediaTime::zeroTime() && playbackRate < 0) || (now >= dur && playbackRate > 0)) {
            // If the media element does not have a current media controller, and the media element
            // has still ended playback and paused is false,
            if (!m_mediaController && !m_paused) {
                // changes paused to true and fires a simple event named pause at the media element.
                m_paused = true;
                scheduleEvent(eventNames().pauseEvent);
                m_mediaSession->clientWillPausePlayback();
            }
            // Queue a task to fire a simple event named ended at the media element.
            if (!m_sentEndEvent) {
                m_sentEndEvent = true;
                scheduleEvent(eventNames().endedEvent);
                if (!wasSeeking)
                    addBehaviorRestrictionsOnEndIfNecessary();

                setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::None);
            }
            // If the media element has a current media controller, then report the controller state
            // for the media element's current media controller.
            updateMediaController();
        } else
            m_sentEndEvent = false;
    } else {
#if ENABLE(MEDIA_STREAM)
        if (m_mediaStreamSrcObject) {
            // http://w3c.github.io/mediacapture-main/#event-mediastream-inactive
            // 6. MediaStreams in Media Elements
            // When the MediaStream state moves from the active to the inactive state, the User Agent
            // must raise an ended event on the HTMLMediaElement and set its ended attribute to true.
            // Note that once ended equals true the HTMLMediaElement will not play media even if new
            // MediaStreamTrack's 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()
            if (!m_sentEndEvent && m_player && m_player->ended()) {
                m_sentEndEvent = true;
                scheduleEvent(eventNames().endedEvent);
                if (!wasSeeking)
                    addBehaviorRestrictionsOnEndIfNecessary();
                m_paused = true;
                setPlaying(false);
            }
        } else
#endif
        m_sentEndEvent = false;
    }

    updatePlayState(UpdateState::Asynchronously);
    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::addBehaviorRestrictionsOnEndIfNecessary()
{
    if (isFullscreen())
        return;

    m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureToControlControlsManager);
    m_playbackControlsManagerBehaviorRestrictionsTimer.stop();
    m_playbackControlsManagerBehaviorRestrictionsTimer.startOneShot(hideMediaControlsAfterEndedDelay);
}

void HTMLMediaElement::handleSeekToPlaybackPosition(double position)
{
#if PLATFORM(MAC)
    // FIXME: This should ideally use faskSeek, but this causes MediaRemote's playhead to flicker upon release.
    // Please see <rdar://problem/28457219> for more details.
    seek(MediaTime::createWithDouble(position));
    m_seekToPlaybackPositionEndedTimer.stop();
    m_seekToPlaybackPositionEndedTimer.startOneShot(500_ms);

    if (!m_isScrubbingRemotely) {
        m_isScrubbingRemotely = true;
        if (!paused())
            pauseInternal();
    }
#else
    fastSeek(position);
#endif
}

void HTMLMediaElement::seekToPlaybackPositionEndedTimerFired()
{
#if PLATFORM(MAC)
    if (!m_isScrubbingRemotely)
        return;

    PlatformMediaSessionManager::sharedManager().sessionDidEndRemoteScrubbing(*m_mediaSession);
    m_isScrubbingRemotely = false;
    m_seekToPlaybackPositionEndedTimer.stop();
#endif
}

void HTMLMediaElement::mediaPlayerVolumeChanged(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerVolumeChanged(%p)", this);

    beginProcessingMediaPlayerCallback();
    if (m_player) {
        double vol = m_player->volume();
        if (vol != m_volume) {
            m_volume = vol;
            updateVolume();
            scheduleEvent(eventNames().volumechangeEvent);
        }
    }
    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::mediaPlayerMuteChanged(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerMuteChanged(%p)", this);

    beginProcessingMediaPlayerCallback();
    if (m_player)
        setMuted(m_player->muted());
    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::mediaPlayerDurationChanged(MediaPlayer* player)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerDurationChanged(%p)", this);

    beginProcessingMediaPlayerCallback();

    scheduleEvent(eventNames().durationchangeEvent);
    mediaPlayerCharacteristicChanged(player);

    MediaTime now = currentMediaTime();
    MediaTime dur = durationMediaTime();
    if (now > dur)
        seekInternal(dur);

    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::mediaPlayerRateChanged(MediaPlayer*)
{
    beginProcessingMediaPlayerCallback();

    // Stash the rate in case the one we tried to set isn't what the engine is
    // using (eg. it can't handle the rate we set)
    m_reportedPlaybackRate = m_player->rate();

    LOG(Media, "HTMLMediaElement::mediaPlayerRateChanged(%p) - rate: %lf", this, m_reportedPlaybackRate);

    if (m_playing)
        invalidateCachedTime();

    updateSleepDisabling();

    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::mediaPlayerPlaybackStateChanged(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerPlaybackStateChanged(%p)", this);

    if (!m_player || m_pausedInternal)
        return;

    beginProcessingMediaPlayerCallback();
    if (m_player->paused())
        pauseInternal();
    else
        playInternal();
    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::mediaPlayerSawUnsupportedTracks(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerSawUnsupportedTracks(%p)", this);

    // The MediaPlayer came across content it cannot completely handle.
    // This is normally acceptable except when we are in a standalone
    // MediaDocument. If so, tell the document what has happened.
    if (is<MediaDocument>(document()))
        downcast<MediaDocument>(document()).mediaElementSawUnsupportedTracks();
}

void HTMLMediaElement::mediaPlayerResourceNotSupported(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerResourceNotSupported(%p)", this);

    // The MediaPlayer came across content which no installed engine supports.
    mediaLoadingFailed(MediaPlayer::FormatError);
}

// MediaPlayerPresentation methods
void HTMLMediaElement::mediaPlayerRepaint(MediaPlayer*)
{
    beginProcessingMediaPlayerCallback();
    updateDisplayState();
    if (auto* renderer = this->renderer())
        renderer->repaint();
    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::mediaPlayerSizeChanged(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerSizeChanged(%p)", this);

    if (is<MediaDocument>(document()) && m_player)
        downcast<MediaDocument>(document()).mediaElementNaturalSizeChanged(expandedIntSize(m_player->naturalSize()));

    beginProcessingMediaPlayerCallback();
    if (m_readyState > HAVE_NOTHING)
        scheduleResizeEventIfSizeChanged();
    updateRenderer();
    endProcessingMediaPlayerCallback();
}

bool HTMLMediaElement::mediaPlayerRenderingCanBeAccelerated(MediaPlayer*)
{
    auto* renderer = this->renderer();
    return is<RenderVideo>(renderer)
        && downcast<RenderVideo>(*renderer).view().compositor().canAccelerateVideoRendering(downcast<RenderVideo>(*renderer));
}

void HTMLMediaElement::mediaPlayerRenderingModeChanged(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerRenderingModeChanged(%p)", this);

    // Kick off a fake recalcStyle that will update the compositing tree.
    invalidateStyleAndLayerComposition();
}

bool HTMLMediaElement::mediaPlayerAcceleratedCompositingEnabled()
{
    return document().settings().acceleratedCompositingEnabled();
}

#if PLATFORM(WIN) && USE(AVFOUNDATION)

GraphicsDeviceAdapter* HTMLMediaElement::mediaPlayerGraphicsDeviceAdapter(const MediaPlayer*) const
{
    auto* page = document().page();
    if (!page)
        return nullptr;
    return page->chrome().client().graphicsDeviceAdapter();
}

#endif

void HTMLMediaElement::mediaEngineWasUpdated()
{
    LOG(Media, "HTMLMediaElement::mediaEngineWasUpdated(%p)", this);
    beginProcessingMediaPlayerCallback();
    updateRenderer();
    endProcessingMediaPlayerCallback();

    m_mediaSession->mediaEngineUpdated(*this);

#if ENABLE(WEB_AUDIO)
    if (m_audioSourceNode && audioSourceProvider()) {
        m_audioSourceNode->lock();
        audioSourceProvider()->setClient(m_audioSourceNode);
        m_audioSourceNode->unlock();
    }
#endif

#if PLATFORM(IOS) || (PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE))
    if (!m_player)
        return;
    m_player->setVideoFullscreenFrame(m_videoFullscreenFrame);
    m_player->setVideoFullscreenGravity(m_videoFullscreenGravity);
    m_player->setVideoFullscreenLayer(m_videoFullscreenLayer.get());
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    updateMediaState(UpdateState::Asynchronously);
#endif
}

void HTMLMediaElement::mediaPlayerEngineUpdated(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerEngineUpdated(%p)", this);

#if ENABLE(MEDIA_SOURCE)
    m_droppedVideoFrames = 0;
#endif

    m_havePreparedToPlay = false;

    scheduleDelayedAction(MediaEngineUpdated);
}

void HTMLMediaElement::mediaPlayerFirstVideoFrameAvailable(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerFirstVideoFrameAvailable(%p) - current display mode = %i", this, (int)displayMode());

    beginProcessingMediaPlayerCallback();
    if (displayMode() == PosterWaitingForVideo) {
        setDisplayMode(Video);
        mediaPlayerRenderingModeChanged(m_player.get());
    }
    endProcessingMediaPlayerCallback();
}

void HTMLMediaElement::mediaPlayerCharacteristicChanged(MediaPlayer*)
{
    LOG(Media, "HTMLMediaElement::mediaPlayerCharacteristicChanged(%p)", this);
    
    beginProcessingMediaPlayerCallback();

#if ENABLE(VIDEO_TRACK)
    if (m_captionDisplayMode == CaptionUserPreferences::Automatic && m_subtitleTrackLanguage != m_player->languageOfPrimaryAudioTrack())
        markCaptionAndSubtitleTracksAsUnconfigured(AfterDelay);
#endif

    if (potentiallyPlaying() && displayMode() == PosterWaitingForVideo) {
        setDisplayMode(Video);
        mediaPlayerRenderingModeChanged(m_player.get());
    }

    if (hasMediaControls())
        mediaControls()->reset();
    updateRenderer();

    if (!paused() && !m_mediaSession->playbackPermitted(*this)) {
        pauseInternal();
        setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);
    }

#if ENABLE(MEDIA_SESSION)
    document().updateIsPlayingMedia(m_elementID);
#else
    document().updateIsPlayingMedia();
#endif

    m_hasEverHadAudio |= hasAudio();
    m_hasEverHadVideo |= hasVideo();

    m_mediaSession->canProduceAudioChanged();

    endProcessingMediaPlayerCallback();
}

Ref<TimeRanges> HTMLMediaElement::buffered() const
{
    if (!m_player)
        return TimeRanges::create();

#if ENABLE(MEDIA_SOURCE)
    if (m_mediaSource)
        return TimeRanges::create(*m_mediaSource->buffered());
#endif

    return TimeRanges::create(*m_player->buffered());
}

double HTMLMediaElement::maxBufferedTime() const
{
    auto bufferedRanges = buffered();
    unsigned numRanges = bufferedRanges->length();
    if (!numRanges)
        return 0;
    return bufferedRanges.get().ranges().end(numRanges - 1).toDouble();
}

Ref<TimeRanges> HTMLMediaElement::played()
{
    if (m_playing) {
        MediaTime time = currentMediaTime();
        if (time > m_lastSeekTime)
            addPlayedRange(m_lastSeekTime, time);
    }

    if (!m_playedTimeRanges)
        m_playedTimeRanges = TimeRanges::create();

    return m_playedTimeRanges->copy();
}

Ref<TimeRanges> HTMLMediaElement::seekable() const
{
#if ENABLE(MEDIA_SOURCE)
    if (m_mediaSource)
        return m_mediaSource->seekable();
#endif

    if (m_player)
        return TimeRanges::create(*m_player->seekable());

    return TimeRanges::create();
}

double HTMLMediaElement::seekableTimeRangesLastModifiedTime() const
{
    return m_player ? m_player->seekableTimeRangesLastModifiedTime() : 0;
}

double HTMLMediaElement::liveUpdateInterval() const
{
    return m_player ? m_player->liveUpdateInterval() : 0;
}

bool HTMLMediaElement::potentiallyPlaying() const
{
    if (isBlockedOnMediaController())
        return false;
    
    if (!couldPlayIfEnoughData())
        return false;

    if (m_readyState >= HAVE_FUTURE_DATA)
        return true;

    return m_readyStateMaximum >= HAVE_FUTURE_DATA && m_readyState < HAVE_FUTURE_DATA;
}

bool HTMLMediaElement::couldPlayIfEnoughData() const
{
    if (paused())
        return false;

    if (endedPlayback())
        return false;

    if (stoppedDueToErrors())
        return false;

    if (pausedForUserInteraction())
        return false;

    return true;
}

bool HTMLMediaElement::endedPlayback() const
{
    MediaTime dur = durationMediaTime();
    if (!m_player || !dur.isValid())
        return false;

    // 4.8.10.8 Playing the media resource

    // A media element is said to have ended playback when the element's 
    // readyState attribute is HAVE_METADATA or greater, 
    if (m_readyState < HAVE_METADATA)
        return false;

    // and the current playback position is the end of the media resource and the direction
    // of playback is forwards, Either the media element does not have a loop attribute specified,
    // or the media element has a current media controller.
    MediaTime now = currentMediaTime();
    if (requestedPlaybackRate() > 0)
        return dur > MediaTime::zeroTime() && now >= dur && (!loop() || m_mediaController);

    // or the current playback position is the earliest possible position and the direction 
    // of playback is backwards
    if (requestedPlaybackRate() < 0)
        return now <= MediaTime::zeroTime();

    return false;
}

bool HTMLMediaElement::stoppedDueToErrors() const
{
    if (m_readyState >= HAVE_METADATA && m_error) {
        RefPtr<TimeRanges> seekableRanges = seekable();
        if (!seekableRanges->contain(currentTime()))
            return true;
    }
    
    return false;
}

bool HTMLMediaElement::pausedForUserInteraction() const
{
    if (m_mediaSession->state() == PlatformMediaSession::Interrupted)
        return true;

    return false;
}

MediaTime HTMLMediaElement::minTimeSeekable() const
{
    return m_player ? m_player->minTimeSeekable() : MediaTime::zeroTime();
}

MediaTime HTMLMediaElement::maxTimeSeekable() const
{
    return m_player ? m_player->maxTimeSeekable() : MediaTime::zeroTime();
}
    
void HTMLMediaElement::updateVolume()
{
    if (!m_player)
        return;
#if PLATFORM(IOS)
    // Only the user can change audio volume so update the cached volume and post the changed event.
    float volume = m_player->volume();
    if (m_volume != volume) {
        m_volume = volume;
        scheduleEvent(eventNames().volumechangeEvent);
    }
#else
    // Avoid recursion when the player reports volume changes.
    if (!processingMediaPlayerCallback()) {
        Page* page = document().page();
        double volumeMultiplier = page ? page->mediaVolume() : 1;
        bool shouldMute = effectiveMuted();

        if (m_mediaController) {
            volumeMultiplier *= m_mediaController->volume();
            shouldMute = m_mediaController->muted() || (page && page->isAudioMuted());
        }

#if ENABLE(MEDIA_SESSION)
        if (m_shouldDuck)
            volumeMultiplier *= 0.25;
#endif

        m_player->setMuted(shouldMute);
        m_player->setVolume(m_volume * volumeMultiplier);
    }

#if ENABLE(MEDIA_SESSION)
    document().updateIsPlayingMedia(m_elementID);
#else
    document().updateIsPlayingMedia();
#endif

    if (hasMediaControls())
        mediaControls()->changedVolume();
#endif
}

void HTMLMediaElement::updatePlayState(UpdateState updateState)
{
    if (updateState == UpdateState::Asynchronously) {
        scheduleDelayedAction(UpdatePlayState);
        return;
    }

    if (!m_player)
        return;

    if (m_pausedInternal) {
        if (!m_player->paused())
            m_player->pause();
        refreshCachedTime();
        m_playbackProgressTimer.stop();
        if (hasMediaControls())
            mediaControls()->playbackStopped();
        return;
    }
    
    bool shouldBePlaying = potentiallyPlaying();
    bool playerPaused = m_player->paused();

    LOG(Media, "HTMLMediaElement::updatePlayState(%p) - shouldBePlaying = %s, playerPaused = %s", this, boolString(shouldBePlaying), boolString(playerPaused));

    if (shouldBePlaying) {
        scheduleUpdatePlaybackControlsManager();

        setDisplayMode(Video);
        invalidateCachedTime();

        if (playerPaused) {
            m_mediaSession->clientWillBeginPlayback();

            if (m_mediaSession->requiresFullscreenForVideoPlayback(*this) && !isFullscreen())
                enterFullscreen();

            // Set rate, muted before calling play in case they were set before the media engine was setup.
            // The media engine should just stash the rate and muted values since it isn't already playing.
            m_player->setRate(requestedPlaybackRate());
            m_player->setMuted(effectiveMuted());

            if (m_firstTimePlaying) {
                // Log that a media element was played.
                if (auto* page = document().page())
                    page->diagnosticLoggingClient().logDiagnosticMessage(isVideo() ? DiagnosticLoggingKeys::videoKey() : DiagnosticLoggingKeys::audioKey(), DiagnosticLoggingKeys::playedKey(), ShouldSample::No);
                m_firstTimePlaying = false;
            }

            m_player->play();
        }

        if (hasMediaControls())
            mediaControls()->playbackStarted();

        startPlaybackProgressTimer();
        setPlaying(true);
    } else {
        scheduleUpdatePlaybackControlsManager();

        if (!playerPaused)
            m_player->pause();
        refreshCachedTime();

        m_playbackProgressTimer.stop();
        setPlaying(false);
        MediaTime time = currentMediaTime();
        if (time > m_lastSeekTime)
            addPlayedRange(m_lastSeekTime, time);

        if (couldPlayIfEnoughData())
            prepareToPlay();

        if (hasMediaControls())
            mediaControls()->playbackStopped();
    }
    
    updateMediaController();
    updateRenderer();

    m_hasEverHadAudio |= hasAudio();
    m_hasEverHadVideo |= hasVideo();
}

void HTMLMediaElement::setPlaying(bool playing)
{
    if (playing && m_mediaSession)
        m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequirePlaybackToControlControlsManager);

    if (m_playing == playing)
        return;

    m_playing = playing;

#if ENABLE(MEDIA_SESSION)
    document().updateIsPlayingMedia(m_elementID);
#else
    document().updateIsPlayingMedia();
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    updateMediaState(UpdateState::Asynchronously);
#endif
}

void HTMLMediaElement::setPausedInternal(bool b)
{
    m_pausedInternal = b;
    updatePlayState(UpdateState::Asynchronously);
}

void HTMLMediaElement::stopPeriodicTimers()
{
    m_progressEventTimer.stop();
    m_playbackProgressTimer.stop();
}

void HTMLMediaElement::userCancelledLoad()
{
    LOG(Media, "HTMLMediaElement::userCancelledLoad(%p)", this);

    // FIXME: We should look to reconcile the iOS and non-iOS code (below).
#if PLATFORM(IOS)
    if (m_networkState == NETWORK_EMPTY || m_readyState >= HAVE_METADATA)
        return;
#else
    if (m_networkState == NETWORK_EMPTY || m_completelyLoaded)
        return;
#endif

    // If the media data fetching process is aborted by the user:

    // 1 - The user agent should cancel the fetching process.
    clearMediaPlayer(EveryDelayedAction);

    // 2 - Set the error attribute to a new MediaError object whose code attribute is set to MEDIA_ERR_ABORTED.
    m_error = MediaError::create(MediaError::MEDIA_ERR_ABORTED);

    // 3 - Queue a task to fire a simple event named error at the media element.
    scheduleEvent(eventNames().abortEvent);

#if ENABLE(MEDIA_SOURCE)
    detachMediaSource();
#endif

    // 4 - If the media element's readyState attribute has a value equal to HAVE_NOTHING, set the 
    // element's networkState attribute to the NETWORK_EMPTY value and queue a task to fire a 
    // simple event named emptied at the element. Otherwise, set the element's networkState 
    // attribute to the NETWORK_IDLE value.
    if (m_readyState == HAVE_NOTHING) {
        m_networkState = NETWORK_EMPTY;
        scheduleEvent(eventNames().emptiedEvent);
    }
    else
        m_networkState = NETWORK_IDLE;

    // 5 - Set the element's delaying-the-load-event flag to false. This stops delaying the load event.
    setShouldDelayLoadEvent(false);

    // 6 - Abort the overall resource selection algorithm.
    m_currentSourceNode = nullptr;

    // Reset m_readyState since m_player is gone.
    m_readyState = HAVE_NOTHING;
    updateMediaController();
#if ENABLE(VIDEO_TRACK)
    updateActiveTextTrackCues(MediaTime::zeroTime());
#endif
}

void HTMLMediaElement::clearMediaPlayer(DelayedActionType flags)
{
    LOG(Media, "HTMLMediaElement::clearMediaPlayer(%p) - flags = %s", this, actionName(flags).utf8().data());

#if ENABLE(MEDIA_STREAM)
    if (!m_settingMediaStreamSrcObject)
        m_mediaStreamSrcObject = nullptr;
#endif

#if ENABLE(MEDIA_SOURCE)
    detachMediaSource();
#endif

    m_blob = nullptr;

#if ENABLE(VIDEO_TRACK)
    forgetResourceSpecificTracks();
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent)) {
        m_hasPlaybackTargetAvailabilityListeners = false;
        m_mediaSession->setHasPlaybackTargetAvailabilityListeners(*this, false);

        // Send an availability event in case scripts want to hide the picker when the element
        // doesn't support playback to a target.
        enqueuePlaybackTargetAvailabilityChangedEvent();
    }

    if (m_isPlayingToWirelessTarget) {
        m_isPlayingToWirelessTarget = false;
        scheduleEvent(eventNames().webkitcurrentplaybacktargetiswirelesschangedEvent);
    }
#endif

    if (m_isWaitingUntilMediaCanStart) {
        m_isWaitingUntilMediaCanStart = false;
        document().removeMediaCanStartListener(this);
    }

    if (m_player) {
        m_player->invalidate();
        m_player = nullptr;
    }
    updatePlaybackControlsManager();

    stopPeriodicTimers();
    m_pendingActionTimer.stop();

    clearFlags(m_pendingActionFlags, flags);
    m_loadState = WaitingForSource;

#if ENABLE(VIDEO_TRACK)
    if (m_textTracks)
        configureTextTrackDisplay();
#endif

    m_mediaSession->clientCharacteristicsChanged();
    m_mediaSession->canProduceAudioChanged();

    m_resourceSelectionTaskQueue.cancelAllTasks();

    updateSleepDisabling();
}

bool HTMLMediaElement::canSuspendForDocumentSuspension() const
{
    return true; 
}

const char* HTMLMediaElement::activeDOMObjectName() const
{
    return "HTMLMediaElement";
}

void HTMLMediaElement::stopWithoutDestroyingMediaPlayer()
{
    LOG(Media, "HTMLMediaElement::stopWithoutDestroyingMediaPlayer(%p)", this);

    if (m_videoFullscreenMode != VideoFullscreenModeNone)
        exitFullscreen();

    setPreparedToReturnVideoLayerToInline(true);

    updatePlaybackControlsManager();
    m_inActiveDocument = false;

    // Stop the playback without generating events
    setPlaying(false);
    setPausedInternal(true);
    m_mediaSession->clientWillPausePlayback();

    setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::None);

    userCancelledLoad();

    updateRenderer();

    stopPeriodicTimers();

    updateSleepDisabling();
}

void HTMLMediaElement::contextDestroyed()
{
    m_seekTaskQueue.close();
    m_shadowDOMTaskQueue.close();
    m_promiseTaskQueue.close();
    m_pauseAfterDetachedTaskQueue.close();
    m_updatePlaybackControlsManagerQueue.close();

    m_pendingPlayPromises.clear();

    ActiveDOMObject::contextDestroyed();
}

void HTMLMediaElement::stop()
{
    LOG(Media, "HTMLMediaElement::stop(%p)", this);

    Ref<HTMLMediaElement> protectedThis(*this);
    stopWithoutDestroyingMediaPlayer();

    m_asyncEventQueue.close();
    m_promiseTaskQueue.close();
    m_updatePlaybackControlsManagerQueue.close();

    // Once an active DOM object has been stopped it can not be restarted, so we can deallocate
    // the media player now. Note that userCancelledLoad will already called clearMediaPlayer
    // if the media was not fully loaded, but we need the same cleanup if the file was completely
    // loaded and calling it again won't cause any problems.
    clearMediaPlayer(EveryDelayedAction);

    m_mediaSession->stopSession();
}

void HTMLMediaElement::suspend(ReasonForSuspension why)
{
    LOG(Media, "HTMLMediaElement::suspend(%p)", this);
    Ref<HTMLMediaElement> protectedThis(*this);

    switch (why)
    {
        case PageCache:
            stopWithoutDestroyingMediaPlayer();
            m_asyncEventQueue.suspend();
            setShouldBufferData(false);
            m_mediaSession->addBehaviorRestriction(MediaElementSession::RequirePageConsentToResumeMedia);
            break;
        case JavaScriptDebuggerPaused:
        case PageWillBeSuspended:
        case WillDeferLoading:
            // Do nothing, we don't pause media playback in these cases.
            break;
    }
}

void HTMLMediaElement::resume()
{
    LOG(Media, "HTMLMediaElement::resume(%p)", this);

    m_inActiveDocument = true;

    m_asyncEventQueue.resume();

    setShouldBufferData(true);

    if (!m_mediaSession->pageAllowsPlaybackAfterResuming(*this))
        document().addMediaCanStartListener(this);
    else
        setPausedInternal(false);

    m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequirePageConsentToResumeMedia);

    if (m_error && m_error->code() == MediaError::MEDIA_ERR_ABORTED) {
        // Restart the load if it was aborted in the middle by moving the document to the page cache.
        // m_error is only left at MEDIA_ERR_ABORTED when the document becomes inactive (it is set to
        //  MEDIA_ERR_ABORTED while the abortEvent is being sent, but cleared immediately afterwards).
        // This behavior is not specified but it seems like a sensible thing to do.
        // As it is not safe to immedately start loading now, let's schedule a load.
        prepareForLoad();
    }

    updateRenderer();
}

bool HTMLMediaElement::hasPendingActivity() const
{
    return (hasAudio() && isPlaying()) || m_asyncEventQueue.hasPendingEvents() || m_creatingControls;
}

void HTMLMediaElement::mediaVolumeDidChange()
{
    LOG(Media, "HTMLMediaElement::mediaVolumeDidChange(%p)", this);
    updateVolume();
}

void HTMLMediaElement::visibilityStateChanged()
{
    m_elementIsHidden = document().hidden() && m_videoFullscreenMode != VideoFullscreenModePictureInPicture;
    LOG(Media, "HTMLMediaElement::visibilityStateChanged(%p) - visible = %s", this, boolString(!m_elementIsHidden));
    updateSleepDisabling();
    m_mediaSession->visibilityChanged();
    if (m_player)
        m_player->setVisible(!m_elementIsHidden);

    bool isPlayingAudio = isPlaying() && hasAudio() && !muted() && volume();
    if (!isPlayingAudio) {
        if (m_elementIsHidden) {
            RELEASE_LOG_IF_ALLOWED("visibilityStateChanged() Suspending playback after going to the background");
            m_mediaSession->beginInterruption(PlatformMediaSession::EnteringBackground);
        } else {
            RELEASE_LOG_IF_ALLOWED("visibilityStateChanged() Resuming playback after entering foreground");
            m_mediaSession->endInterruption(PlatformMediaSession::MayResumePlaying);
        }
    }
}

#if ENABLE(VIDEO_TRACK)
bool HTMLMediaElement::requiresTextTrackRepresentation() const
{
    return (m_videoFullscreenMode != VideoFullscreenModeNone) && m_player ? m_player->requiresTextTrackRepresentation() : false;
}

void HTMLMediaElement::setTextTrackRepresentation(TextTrackRepresentation* representation)
{
    if (m_player)
        m_player->setTextTrackRepresentation(representation);
}

void HTMLMediaElement::syncTextTrackBounds()
{
    if (m_player)
        m_player->syncTextTrackBounds();
}
#endif // ENABLE(VIDEO_TRACK)

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
void HTMLMediaElement::webkitShowPlaybackTargetPicker()
{
    LOG(Media, "HTMLMediaElement::webkitShowPlaybackTargetPicker(%p)", this);
    if (processingUserGestureForMedia())
        removeBehaviorsRestrictionsAfterFirstUserGesture();
    m_mediaSession->showPlaybackTargetPicker(*this);
}

bool HTMLMediaElement::webkitCurrentPlaybackTargetIsWireless() const
{
    return m_isPlayingToWirelessTarget;
}

void HTMLMediaElement::wirelessRoutesAvailableDidChange()
{
    enqueuePlaybackTargetAvailabilityChangedEvent();
}

void HTMLMediaElement::mediaPlayerCurrentPlaybackTargetIsWirelessChanged(MediaPlayer*)
{
    m_isPlayingToWirelessTarget = m_player && m_player->isCurrentPlaybackTargetWireless();

    LOG(Media, "HTMLMediaElement::mediaPlayerCurrentPlaybackTargetIsWirelessChanged(%p) - webkitCurrentPlaybackTargetIsWireless = %s", this, boolString(m_isPlayingToWirelessTarget));
    ASSERT(m_player);
    configureMediaControls();
    scheduleEvent(eventNames().webkitcurrentplaybacktargetiswirelesschangedEvent);
    m_mediaSession->isPlayingToWirelessPlaybackTargetChanged(m_isPlayingToWirelessTarget);
    m_mediaSession->canProduceAudioChanged();
    updateMediaState(UpdateState::Asynchronously);
    updateSleepDisabling();
}

bool HTMLMediaElement::dispatchEvent(Event& event)
{
    if (event.type() == eventNames().webkitcurrentplaybacktargetiswirelesschangedEvent) {
        m_failedToPlayToWirelessTarget = false;
        scheduleDelayedAction(CheckPlaybackTargetCompatablity);
    }
    return HTMLElement::dispatchEvent(event);
}

bool HTMLMediaElement::addEventListener(const AtomicString& eventType, Ref<EventListener>&& listener, const AddEventListenerOptions& options)
{
    if (eventType != eventNames().webkitplaybacktargetavailabilitychangedEvent)
        return Node::addEventListener(eventType, WTFMove(listener), options);

    bool isFirstAvailabilityChangedListener = !hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent);
    if (!Node::addEventListener(eventType, WTFMove(listener), options))
        return false;

    if (isFirstAvailabilityChangedListener) {
        m_hasPlaybackTargetAvailabilityListeners = true;
        m_mediaSession->setHasPlaybackTargetAvailabilityListeners(*this, true);
    }

    LOG(Media, "HTMLMediaElement::addEventListener(%p) - 'webkitplaybacktargetavailabilitychanged'", this);
    
    enqueuePlaybackTargetAvailabilityChangedEvent(); // Ensure the event listener gets at least one event.
    return true;
}

bool HTMLMediaElement::removeEventListener(const AtomicString& eventType, EventListener& listener, const ListenerOptions& options)
{
    if (eventType != eventNames().webkitplaybacktargetavailabilitychangedEvent)
        return Node::removeEventListener(eventType, listener, options);

    if (!Node::removeEventListener(eventType, listener, options))
        return false;

    bool didRemoveLastAvailabilityChangedListener = !hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent);
    LOG(Media, "HTMLMediaElement::removeEventListener(%p) - removed last listener = %s", this, boolString(didRemoveLastAvailabilityChangedListener));
    if (didRemoveLastAvailabilityChangedListener) {
        m_hasPlaybackTargetAvailabilityListeners = false;
        m_mediaSession->setHasPlaybackTargetAvailabilityListeners(*this, false);
        updateMediaState(UpdateState::Asynchronously);
    }

    return true;
}

void HTMLMediaElement::enqueuePlaybackTargetAvailabilityChangedEvent()
{
    bool hasTargets = m_mediaSession->hasWirelessPlaybackTargets(*this);
    LOG(Media, "HTMLMediaElement::enqueuePlaybackTargetAvailabilityChangedEvent(%p) - hasTargets = %s", this, boolString(hasTargets));
    auto event = WebKitPlaybackTargetAvailabilityEvent::create(eventNames().webkitplaybacktargetavailabilitychangedEvent, hasTargets);
    event->setTarget(this);
    m_asyncEventQueue.enqueueEvent(WTFMove(event));
    updateMediaState(UpdateState::Asynchronously);
}

void HTMLMediaElement::setWirelessPlaybackTarget(Ref<MediaPlaybackTarget>&& device)
{
    LOG(Media, "HTMLMediaElement::setWirelessPlaybackTarget(%p)", this);
    if (m_player)
        m_player->setWirelessPlaybackTarget(WTFMove(device));
}

bool HTMLMediaElement::canPlayToWirelessPlaybackTarget() const
{
    bool canPlay = m_player && m_player->canPlayToWirelessPlaybackTarget();

    LOG(Media, "HTMLMediaElement::canPlayToWirelessPlaybackTarget(%p) - returning %s", this, boolString(canPlay));

    return canPlay;
}

bool HTMLMediaElement::isPlayingToWirelessPlaybackTarget() const
{
    return m_isPlayingToWirelessTarget;
}

void HTMLMediaElement::setShouldPlayToPlaybackTarget(bool shouldPlay)
{
    LOG(Media, "HTMLMediaElement::setShouldPlayToPlaybackTarget(%p) - shouldPlay = %s", this, boolString(shouldPlay));

    if (m_player)
        m_player->setShouldPlayToPlaybackTarget(shouldPlay);
}
#else // ENABLE(WIRELESS_PLAYBACK_TARGET)

bool HTMLMediaElement::webkitCurrentPlaybackTargetIsWireless() const
{
    return false;
}

#endif // ENABLE(WIRELESS_PLAYBACK_TARGET)

double HTMLMediaElement::minFastReverseRate() const
{
    return m_player ? m_player->minFastReverseRate() : 0;
}

double HTMLMediaElement::maxFastForwardRate() const
{
    return m_player ? m_player->maxFastForwardRate() : 0;
}
    
bool HTMLMediaElement::isFullscreen() const
{
    if (m_videoFullscreenMode != VideoFullscreenModeNone)
        return true;
    
#if ENABLE(FULLSCREEN_API)
    if (document().webkitIsFullScreen() && document().webkitCurrentFullScreenElement() == this)
        return true;
#endif
    
    return false;
}

bool HTMLMediaElement::isStandardFullscreen() const
{
#if ENABLE(FULLSCREEN_API)
    if (document().webkitIsFullScreen() && document().webkitCurrentFullScreenElement() == this)
        return true;
#endif

    return m_videoFullscreenMode == VideoFullscreenModeStandard;
}

void HTMLMediaElement::toggleStandardFullscreenState()
{
    LOG(Media, "HTMLMediaElement::toggleStandardFullscreenState(%p) - isStandardFullscreen() is %s", this, boolString(isStandardFullscreen()));
    
    if (isStandardFullscreen())
        exitFullscreen();
    else
        enterFullscreen();
}

void HTMLMediaElement::enterFullscreen(VideoFullscreenMode mode)
{
    LOG(Media, "HTMLMediaElement::enterFullscreen(%p)", this);
    ASSERT(mode != VideoFullscreenModeNone);

    if (m_videoFullscreenMode == mode)
        return;

    m_temporarilyAllowingInlinePlaybackAfterFullscreen = false;

#if ENABLE(FULLSCREEN_API)
    if (document().settings().fullScreenEnabled()) {
        if (mode == VideoFullscreenModeStandard) {
            document().requestFullScreenForElement(this, Document::ExemptIFrameAllowFullScreenRequirement);
            return;
        }

        // If this media element is not going to standard fullscreen mode but there's
        // an element that's currently in full screen in the document, exit full screen
        // if it contains this media element.
        if (Element* fullscreenElement = document().webkitCurrentFullScreenElement()) {
            if (fullscreenElement->contains(this))
                document().webkitCancelFullScreen();
        }
    }
#endif

    fullscreenModeChanged(mode);
    configureMediaControls();
    if (hasMediaControls())
        mediaControls()->enteredFullscreen();
    if (document().page() && is<HTMLVideoElement>(*this)) {
        HTMLVideoElement& asVideo = downcast<HTMLVideoElement>(*this);
        if (document().page()->chrome().client().supportsVideoFullscreen(m_videoFullscreenMode)) {
            document().page()->chrome().client().enterVideoFullscreenForVideoElement(asVideo, m_videoFullscreenMode);
            scheduleEvent(eventNames().webkitbeginfullscreenEvent);
        }
    }
}

void HTMLMediaElement::enterFullscreen()
{
    enterFullscreen(VideoFullscreenModeStandard);
}

void HTMLMediaElement::exitFullscreen()
{
    LOG(Media, "HTMLMediaElement::exitFullscreen(%p)", this);

#if ENABLE(FULLSCREEN_API)
    if (document().settings().fullScreenEnabled() && document().webkitCurrentFullScreenElement() == this) {
        if (document().webkitIsFullScreen())
            document().webkitCancelFullScreen();
        return;
    }
#endif

    ASSERT(m_videoFullscreenMode != VideoFullscreenModeNone);
    VideoFullscreenMode oldVideoFullscreenMode = m_videoFullscreenMode;
    fullscreenModeChanged(VideoFullscreenModeNone);
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    updateMediaControlsAfterPresentationModeChange();
#endif
    if (hasMediaControls())
        mediaControls()->exitedFullscreen();

    if (!document().page() || !is<HTMLVideoElement>(*this))
        return;

    if (!paused() && m_mediaSession->requiresFullscreenForVideoPlayback(*this)) {
        if (!document().settings().allowsInlineMediaPlaybackAfterFullscreen() || isVideoTooSmallForInlinePlayback())
            pauseInternal();
        else {
            // Allow inline playback, but set a flag so pausing and starting again (e.g. when scrubbing or looping) won't go back to fullscreen.
            // Also set the controls attribute so the user will be able to control playback.
            m_temporarilyAllowingInlinePlaybackAfterFullscreen = true;
            setControls(true);
        }
    }

#if PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE)
    if (document().activeDOMObjectsAreSuspended() || document().activeDOMObjectsAreStopped())
        document().page()->chrome().client().exitVideoFullscreenToModeWithoutAnimation(downcast<HTMLVideoElement>(*this), VideoFullscreenModeNone);
    else
#endif
    if (document().page()->chrome().client().supportsVideoFullscreen(oldVideoFullscreenMode)) {
        document().page()->chrome().client().exitVideoFullscreenForVideoElement(downcast<HTMLVideoElement>(*this));
        scheduleEvent(eventNames().webkitendfullscreenEvent);
    }
}

void HTMLMediaElement::willBecomeFullscreenElement()
{
#if PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE)
    HTMLMediaElementEnums::VideoFullscreenMode oldVideoFullscreenMode = m_videoFullscreenMode;
#endif

    fullscreenModeChanged(VideoFullscreenModeStandard);

#if PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE)
    switch (oldVideoFullscreenMode) {
    case VideoFullscreenModeNone:
    case VideoFullscreenModeStandard:
        // Don't need to do anything if we are not in any special fullscreen mode or it's already
        // in standard fullscreen mode.
        break;
    case VideoFullscreenModePictureInPicture:
        if (is<HTMLVideoElement>(*this))
            downcast<HTMLVideoElement>(this)->exitToFullscreenModeWithoutAnimationIfPossible(oldVideoFullscreenMode, VideoFullscreenModeStandard);
        break;
    }
#endif

    Element::willBecomeFullscreenElement();
}

void HTMLMediaElement::didBecomeFullscreenElement()
{
    if (hasMediaControls())
        mediaControls()->enteredFullscreen();
}

void HTMLMediaElement::willStopBeingFullscreenElement()
{
    if (hasMediaControls())
        mediaControls()->exitedFullscreen();

    if (fullscreenMode() == VideoFullscreenModeStandard)
        fullscreenModeChanged(VideoFullscreenModeNone);
}

PlatformMedia HTMLMediaElement::platformMedia() const
{
    return m_player ? m_player->platformMedia() : NoPlatformMedia;
}

PlatformLayer* HTMLMediaElement::platformLayer() const
{
    return m_player ? m_player->platformLayer() : nullptr;
}

void HTMLMediaElement::setPreparedToReturnVideoLayerToInline(bool value)
{
    m_preparedForInline = value;
    if (m_preparedForInline && m_preparedForInlineCompletionHandler) {
        m_preparedForInlineCompletionHandler();
        m_preparedForInlineCompletionHandler = nullptr;
    }
}

void HTMLMediaElement::waitForPreparedForInlineThen(WTF::Function<void()>&& completionHandler)
{
    ASSERT(!m_preparedForInlineCompletionHandler);
    if (m_preparedForInline)  {
        completionHandler();
        return;
    }
    
    m_preparedForInlineCompletionHandler = WTFMove(completionHandler);
}

#if PLATFORM(IOS) || (PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE))

bool HTMLMediaElement::isVideoLayerInline()
{
    return !m_videoFullscreenLayer;
};
    
void HTMLMediaElement::setVideoFullscreenLayer(PlatformLayer* platformLayer, WTF::Function<void()>&& completionHandler)
{
    m_videoFullscreenLayer = platformLayer;
    if (!m_player) {
        completionHandler();
        return;
    }
    
    m_player->setVideoFullscreenLayer(platformLayer, WTFMove(completionHandler));
    invalidateStyleAndLayerComposition();
#if ENABLE(VIDEO_TRACK)
    updateTextTrackDisplay();
#endif
}
    
void HTMLMediaElement::setVideoFullscreenFrame(FloatRect frame)
{
    m_videoFullscreenFrame = frame;
    if (m_player)
        m_player->setVideoFullscreenFrame(frame);
}

void HTMLMediaElement::setVideoFullscreenGravity(MediaPlayer::VideoGravity gravity)
{
    m_videoFullscreenGravity = gravity;
    if (m_player)
        m_player->setVideoFullscreenGravity(gravity);
}
    
#else
    
bool HTMLMediaElement::isVideoLayerInline()
{
    return true;
};

#endif

bool HTMLMediaElement::hasClosedCaptions() const
{
    if (m_player && m_player->hasClosedCaptions())
        return true;

#if ENABLE(VIDEO_TRACK)
    if (!m_textTracks)
        return false;

    for (unsigned i = 0; i < m_textTracks->length(); ++i) {
        auto& track = *m_textTracks->item(i);
        if (track.readinessState() == TextTrack::FailedToLoad)
            continue;
        if (track.kind() == TextTrack::Kind::Captions || track.kind() == TextTrack::Kind::Subtitles)
            return true;
    }
#endif

    return false;
}

bool HTMLMediaElement::closedCaptionsVisible() const
{
    return m_closedCaptionsVisible;
}

#if ENABLE(VIDEO_TRACK)

void HTMLMediaElement::updateTextTrackDisplay()
{
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    ensureMediaControlsShadowRoot();
    ASSERT(m_mediaControlsHost);
    m_mediaControlsHost->updateTextTrackContainer();
#else
    if (!hasMediaControls() && !createMediaControls())
        return;
    
    mediaControls()->updateTextTrackDisplay();
#endif
}

#endif

void HTMLMediaElement::setClosedCaptionsVisible(bool closedCaptionVisible)
{
    LOG(Media, "HTMLMediaElement::setClosedCaptionsVisible(%p) - %s", this, boolString(closedCaptionVisible));

    m_closedCaptionsVisible = false;

    if (!m_player || !hasClosedCaptions())
        return;

    m_closedCaptionsVisible = closedCaptionVisible;
    m_player->setClosedCaptionsVisible(closedCaptionVisible);

#if ENABLE(VIDEO_TRACK)
    markCaptionAndSubtitleTracksAsUnconfigured(Immediately);
    updateTextTrackDisplay();
#else
    if (hasMediaControls())
        mediaControls()->changedClosedCaptionsVisibility();
#endif
}

void HTMLMediaElement::setWebkitClosedCaptionsVisible(bool visible)
{
    m_webkitLegacyClosedCaptionOverride = visible;
    setClosedCaptionsVisible(visible);
}

bool HTMLMediaElement::webkitClosedCaptionsVisible() const
{
    return m_webkitLegacyClosedCaptionOverride && m_closedCaptionsVisible;
}


bool HTMLMediaElement::webkitHasClosedCaptions() const
{
    return hasClosedCaptions();
}

#if ENABLE(MEDIA_STATISTICS)
unsigned HTMLMediaElement::webkitAudioDecodedByteCount() const
{
    if (!m_player)
        return 0;
    return m_player->audioDecodedByteCount();
}

unsigned HTMLMediaElement::webkitVideoDecodedByteCount() const
{
    if (!m_player)
        return 0;
    return m_player->videoDecodedByteCount();
}
#endif

void HTMLMediaElement::mediaCanStart(Document& document)
{
    ASSERT_UNUSED(document, &document == &this->document());
    LOG(Media, "HTMLMediaElement::mediaCanStart(%p) - m_isWaitingUntilMediaCanStart = %s, m_pausedInternal = %s",
        this, boolString(m_isWaitingUntilMediaCanStart), boolString(m_pausedInternal) );

    ASSERT(m_isWaitingUntilMediaCanStart || m_pausedInternal);
    if (m_isWaitingUntilMediaCanStart) {
        m_isWaitingUntilMediaCanStart = false;
        selectMediaResource();
    }
    if (m_pausedInternal)
        setPausedInternal(false);
}

bool HTMLMediaElement::isURLAttribute(const Attribute& attribute) const
{
    return attribute.name() == srcAttr || HTMLElement::isURLAttribute(attribute);
}

void HTMLMediaElement::setShouldDelayLoadEvent(bool shouldDelay)
{
    if (m_shouldDelayLoadEvent == shouldDelay)
        return;

    LOG(Media, "HTMLMediaElement::setShouldDelayLoadEvent(%p) - %s", this, boolString(shouldDelay));

    m_shouldDelayLoadEvent = shouldDelay;
    if (shouldDelay)
        document().incrementLoadEventDelayCount();
    else
        document().decrementLoadEventDelayCount();
}

static String& sharedMediaCacheDirectory()
{
    static NeverDestroyed<String> sharedMediaCacheDirectory;
    return sharedMediaCacheDirectory;
}

void HTMLMediaElement::setMediaCacheDirectory(const String& path)
{
    sharedMediaCacheDirectory() = path;
}

const String& HTMLMediaElement::mediaCacheDirectory()
{
    return sharedMediaCacheDirectory();
}

HashSet<RefPtr<SecurityOrigin>> HTMLMediaElement::originsInMediaCache(const String& path)
{
    return MediaPlayer::originsInMediaCache(path);
}

void HTMLMediaElement::clearMediaCache(const String& path, std::chrono::system_clock::time_point modifiedSince)
{
    MediaPlayer::clearMediaCache(path, modifiedSince);
}

void HTMLMediaElement::clearMediaCacheForOrigins(const String& path, const HashSet<RefPtr<SecurityOrigin>>& origins)
{
    MediaPlayer::clearMediaCacheForOrigins(path, origins);
}

void HTMLMediaElement::resetMediaEngines()
{
    MediaPlayer::resetMediaEngines();
}

void HTMLMediaElement::privateBrowsingStateDidChange()
{
    if (!m_player)
        return;

    bool privateMode = document().page() && document().page()->usesEphemeralSession();
    LOG(Media, "HTMLMediaElement::privateBrowsingStateDidChange(%p) - %s", this, boolString(privateMode));
    m_player->setPrivateBrowsingMode(privateMode);
}

MediaControls* HTMLMediaElement::mediaControls() const
{
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    return nullptr;
#else
    ShadowRoot* root = userAgentShadowRoot();
    if (!root)
        return nullptr;
    
    return childrenOfType<MediaControls>(*root).first();
#endif
}

bool HTMLMediaElement::hasMediaControls() const
{
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    return false;
#else

    if (ShadowRoot* userAgent = userAgentShadowRoot()) {
        Node* node = childrenOfType<MediaControls>(*root).first();
        ASSERT_WITH_SECURITY_IMPLICATION(!node || node->isMediaControls());
        return node;
    }

    return false;
#endif
}

bool HTMLMediaElement::createMediaControls()
{
#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    ensureMediaControlsShadowRoot();
    return false;
#else
    if (hasMediaControls())
        return true;

    RefPtr<MediaControls> mediaControls = MediaControls::create(document());
    if (!mediaControls)
        return false;

    mediaControls->setMediaController(m_mediaController ? m_mediaController.get() : static_cast<MediaControllerInterface*>(this));
    mediaControls->reset();
    if (isFullscreen())
        mediaControls->enteredFullscreen();

    ensureUserAgentShadowRoot().appendChild(mediaControls);

    if (!controls() || !isConnected())
        mediaControls->hide();

    return true;
#endif
}

bool HTMLMediaElement::shouldForceControlsDisplay() const
{
    // Always create controls for autoplay video that requires user gesture due to being in low power mode.
    return isVideo() && autoplay() && m_mediaSession->hasBehaviorRestriction(MediaElementSession::RequireUserGestureForVideoDueToLowPowerMode);
}

void HTMLMediaElement::configureMediaControls()
{
    bool requireControls = controls();

    // Always create controls for video when fullscreen playback is required.
    if (isVideo() && m_mediaSession->requiresFullscreenForVideoPlayback(*this))
        requireControls = true;

    if (shouldForceControlsDisplay())
        requireControls = true;

    // Always create controls when in full screen mode.
    if (isFullscreen())
        requireControls = true;

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (m_isPlayingToWirelessTarget)
        requireControls = true;
#endif

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (!requireControls || !isConnected() || !inActiveDocument())
        return;

    ensureMediaControlsShadowRoot();
#else
    if (!requireControls || !isConnected() || !inActiveDocument()) {
        if (hasMediaControls())
            mediaControls()->hide();
        return;
    }

    if (!hasMediaControls() && !createMediaControls())
        return;

    mediaControls()->show();
#endif
}

#if ENABLE(VIDEO_TRACK)
void HTMLMediaElement::configureTextTrackDisplay(TextTrackVisibilityCheckType checkType)
{
    ASSERT(m_textTracks);

    if (m_processingPreferenceChange)
        return;

    if (document().activeDOMObjectsAreStopped())
        return;

    bool haveVisibleTextTrack = false;
    for (unsigned i = 0; i < m_textTracks->length(); ++i) {
        if (m_textTracks->item(i)->mode() == TextTrack::Mode::Showing) {
            haveVisibleTextTrack = true;
            break;
        }
    }

    if (checkType == CheckTextTrackVisibility && m_haveVisibleTextTrack == haveVisibleTextTrack) {
        updateActiveTextTrackCues(currentMediaTime());
        return;
    }

    m_haveVisibleTextTrack = haveVisibleTextTrack;
    m_closedCaptionsVisible = m_haveVisibleTextTrack;

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (!m_haveVisibleTextTrack)
        return;

    ensureMediaControlsShadowRoot();
#else
    if (!m_haveVisibleTextTrack && !hasMediaControls())
        return;
    if (!hasMediaControls() && !createMediaControls())
        return;

    mediaControls()->changedClosedCaptionsVisibility();
    
    updateTextTrackDisplay();
    updateActiveTextTrackCues(currentMediaTime());
#endif
}

void HTMLMediaElement::captionPreferencesChanged()
{
    if (!isVideo())
        return;

    if (hasMediaControls())
        mediaControls()->textTrackPreferencesChanged();

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
    if (m_mediaControlsHost)
        m_mediaControlsHost->updateCaptionDisplaySizes();
#endif

    if (m_player)
        m_player->tracksChanged();

    if (!document().page())
        return;

    CaptionUserPreferences::CaptionDisplayMode displayMode = document().page()->group().captionPreferences().captionDisplayMode();
    if (m_captionDisplayMode == displayMode)
        return;

    m_captionDisplayMode = displayMode;
    setWebkitClosedCaptionsVisible(m_captionDisplayMode == CaptionUserPreferences::AlwaysOn);
}

void HTMLMediaElement::markCaptionAndSubtitleTracksAsUnconfigured(ReconfigureMode mode)
{
    if (!m_textTracks)
        return;

    LOG(Media, "HTMLMediaElement::markCaptionAndSubtitleTracksAsUnconfigured(%p)", this);

    // Mark all tracks as not "configured" so that configureTextTracks()
    // will reconsider which tracks to display in light of new user preferences
    // (e.g. default tracks should not be displayed if the user has turned off
    // captions and non-default tracks should be displayed based on language
    // preferences if the user has turned captions on).
    for (unsigned i = 0; i < m_textTracks->length(); ++i) {
        auto& track = *m_textTracks->item(i);
        auto kind = track.kind();
        if (kind == TextTrack::Kind::Subtitles || kind == TextTrack::Kind::Captions)
            track.setHasBeenConfigured(false);
    }

    m_processingPreferenceChange = true;
    clearFlags(m_pendingActionFlags, ConfigureTextTracks);
    if (mode == Immediately)
        configureTextTracks();
    else
        scheduleDelayedAction(ConfigureTextTracks);
}

#endif

void HTMLMediaElement::createMediaPlayer()
{
    LOG(Media, "HTMLMediaElement::createMediaPlayer(%p)", this);

#if ENABLE(WEB_AUDIO)
    if (m_audioSourceNode)
        m_audioSourceNode->lock();
#endif

#if ENABLE(MEDIA_SOURCE)
    detachMediaSource();
#endif

#if ENABLE(VIDEO_TRACK)
    forgetResourceSpecificTracks();
#endif
    m_player = MediaPlayer::create(*this);
    scheduleUpdatePlaybackControlsManager();

#if ENABLE(WEB_AUDIO)
    if (m_audioSourceNode) {
        // When creating the player, make sure its AudioSourceProvider knows about the MediaElementAudioSourceNode.
        if (audioSourceProvider())
            audioSourceProvider()->setClient(m_audioSourceNode);

        m_audioSourceNode->unlock();
    }
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent)) {
        m_hasPlaybackTargetAvailabilityListeners = true;
        m_mediaSession->setHasPlaybackTargetAvailabilityListeners(*this, true);
        enqueuePlaybackTargetAvailabilityChangedEvent(); // Ensure the event listener gets at least one event.
    }
#endif

    updateSleepDisabling();
}

#if ENABLE(WEB_AUDIO)
void HTMLMediaElement::setAudioSourceNode(MediaElementAudioSourceNode* sourceNode)
{
    m_audioSourceNode = sourceNode;

    if (audioSourceProvider())
        audioSourceProvider()->setClient(m_audioSourceNode);
}

AudioSourceProvider* HTMLMediaElement::audioSourceProvider()
{
    if (m_player)
        return m_player->audioSourceProvider();

    return 0;
}
#endif

const String& HTMLMediaElement::mediaGroup() const
{
    return m_mediaGroup;
}

void HTMLMediaElement::setMediaGroup(const String& group)
{
    if (m_mediaGroup == group)
        return;
    m_mediaGroup = group;

    // When a media element is created with a mediagroup attribute, and when a media element's mediagroup 
    // attribute is set, changed, or removed, the user agent must run the following steps:
    // 1. Let m [this] be the media element in question.
    // 2. Let m have no current media controller, if it currently has one.
    setController(nullptr);

    // 3. If m's mediagroup attribute is being removed, then abort these steps.
    if (group.isEmpty())
        return;

    // 4. If there is another media element whose Document is the same as m's Document (even if one or both
    // of these elements are not actually in the Document), 
    HashSet<HTMLMediaElement*> elements = documentToElementSetMap().get(&document());
    for (auto& element : elements) {
        if (element == this)
            continue;

        // and which also has a mediagroup attribute, and whose mediagroup attribute has the same value as
        // the new value of m's mediagroup attribute,        
        if (element->mediaGroup() == group) {
            //  then let controller be that media element's current media controller.
            setController(element->controller());
            return;
        }
    }

    // Otherwise, let controller be a newly created MediaController.
    setController(MediaController::create(document()));
}

MediaController* HTMLMediaElement::controller() const
{
    return m_mediaController.get();
}

void HTMLMediaElement::setController(RefPtr<MediaController>&& controller)
{
    if (m_mediaController)
        m_mediaController->removeMediaElement(*this);

    m_mediaController = WTFMove(controller);

    if (m_mediaController)
        m_mediaController->addMediaElement(*this);

    if (hasMediaControls())
        mediaControls()->setMediaController(m_mediaController ? m_mediaController.get() : static_cast<MediaControllerInterface*>(this));
}

void HTMLMediaElement::setControllerForBindings(MediaController* controller)
{
    // 4.8.10.11.2 Media controllers: controller attribute.
    // On setting, it must first remove the element's mediagroup attribute, if any, 
    setMediaGroup({ });
    // and then set the current media controller to the given value.
    setController(controller);
}

void HTMLMediaElement::updateMediaController()
{
    if (m_mediaController)
        m_mediaController->reportControllerState();
}

bool HTMLMediaElement::isBlocked() const
{
    // A media element is a blocked media element if its readyState attribute is in the
    // HAVE_NOTHING state, the HAVE_METADATA state, or the HAVE_CURRENT_DATA state,
    if (m_readyState <= HAVE_CURRENT_DATA)
        return true;

    // or if the element has paused for user interaction.
    return pausedForUserInteraction();
}

bool HTMLMediaElement::isBlockedOnMediaController() const
{
    if (!m_mediaController)
        return false;

    // A media element is blocked on its media controller if the MediaController is a blocked 
    // media controller,
    if (m_mediaController->isBlocked())
        return true;

    // or if its media controller position is either before the media resource's earliest possible 
    // position relative to the MediaController's timeline or after the end of the media resource 
    // relative to the MediaController's timeline.
    double mediaControllerPosition = m_mediaController->currentTime();
    if (mediaControllerPosition < 0 || mediaControllerPosition > duration())
        return true;

    return false;
}

void HTMLMediaElement::prepareMediaFragmentURI()
{
    MediaFragmentURIParser fragmentParser(m_currentSrc);
    MediaTime dur = durationMediaTime();
    
    MediaTime start = fragmentParser.startTime();
    if (start.isValid() && start > MediaTime::zeroTime()) {
        m_fragmentStartTime = start;
        if (m_fragmentStartTime > dur)
            m_fragmentStartTime = dur;
    } else
        m_fragmentStartTime = MediaTime::invalidTime();
    
    MediaTime end = fragmentParser.endTime();
    if (end.isValid() && end > MediaTime::zeroTime() && (!m_fragmentStartTime.isValid() || end > m_fragmentStartTime)) {
        m_fragmentEndTime = end;
        if (m_fragmentEndTime > dur)
            m_fragmentEndTime = dur;
    } else
        m_fragmentEndTime = MediaTime::invalidTime();
    
    if (m_fragmentStartTime.isValid() && m_readyState < HAVE_FUTURE_DATA)
        prepareToPlay();
}

void HTMLMediaElement::applyMediaFragmentURI()
{
    if (m_fragmentStartTime.isValid()) {
        m_sentEndEvent = false;
        seek(m_fragmentStartTime);
    }
}

void HTMLMediaElement::updateSleepDisabling()
{
    SleepType shouldDisableSleep = this->shouldDisableSleep();
    if (shouldDisableSleep == SleepType::None && m_sleepDisabler)
        m_sleepDisabler = nullptr;
    else if (shouldDisableSleep != SleepType::None) {
        auto type = shouldDisableSleep == SleepType::Display ? SleepDisabler::Type::Display : SleepDisabler::Type::System;
        if (!m_sleepDisabler || m_sleepDisabler->type() != type)
            m_sleepDisabler = SleepDisabler::create("com.apple.WebCore: HTMLMediaElement playback", type);
    }

    if (m_player)
        m_player->setShouldDisableSleep(shouldDisableSleep == SleepType::Display);
}

HTMLMediaElement::SleepType HTMLMediaElement::shouldDisableSleep() const
{
#if !PLATFORM(COCOA)
    return SleepType::None;
#endif
    if (!m_player || m_player->paused() || loop())
        return SleepType::None;

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    // If the media is playing remotely, we can't know definitively whether it has audio or video tracks.
    if (m_isPlayingToWirelessTarget)
        return SleepType::System;
#endif

#if ENABLE(MEDIA_STREAM)
    if (m_mediaStreamSrcObject) {
        // Do not block system from sleeping if element is only rendering local (capture) sources.
        if (WTF::allOf(m_mediaStreamSrcObject->getTracks(), [] (RefPtr<MediaStreamTrack>& track) { return track && track->isCaptureTrack(); }))
            return SleepType::None;
    }
#endif

    if (!hasVideo() || !hasAudio())
        return SleepType::None;

    if (m_elementIsHidden)
        return SleepType::System;

    return SleepType::Display;
}

String HTMLMediaElement::mediaPlayerReferrer() const
{
    Frame* frame = document().frame();
    if (!frame)
        return String();

    return SecurityPolicy::generateReferrerHeader(document().referrerPolicy(), m_currentSrc, frame->loader().outgoingReferrer());
}

String HTMLMediaElement::mediaPlayerUserAgent() const
{
    Frame* frame = document().frame();
    if (!frame)
        return String();

    return frame->loader().userAgent(m_currentSrc);
}

#if ENABLE(AVF_CAPTIONS)

static inline PlatformTextTrack::TrackKind toPlatform(TextTrack::Kind kind)
{
    switch (kind) {
    case TextTrack::Kind::Captions:
        return PlatformTextTrack::Caption;
    case TextTrack::Kind::Chapters:
        return PlatformTextTrack::Chapter;
    case TextTrack::Kind::Descriptions:
        return PlatformTextTrack::Description;
    case TextTrack::Kind::Forced:
        return PlatformTextTrack::Forced;
    case TextTrack::Kind::Metadata:
        return PlatformTextTrack::MetaData;
    case TextTrack::Kind::Subtitles:
        return PlatformTextTrack::Subtitle;
    }
    ASSERT_NOT_REACHED();
    return PlatformTextTrack::Caption;
}

static inline PlatformTextTrack::TrackMode toPlatform(TextTrack::Mode mode)
{
    switch (mode) {
    case TextTrack::Mode::Disabled:
        return PlatformTextTrack::Disabled;
    case TextTrack::Mode::Hidden:
        return PlatformTextTrack::Hidden;
    case TextTrack::Mode::Showing:
        return PlatformTextTrack::Showing;
    }
    ASSERT_NOT_REACHED();
    return PlatformTextTrack::Disabled;
}

Vector<RefPtr<PlatformTextTrack>> HTMLMediaElement::outOfBandTrackSources()
{
    Vector<RefPtr<PlatformTextTrack>> outOfBandTrackSources;
    for (auto& trackElement : childrenOfType<HTMLTrackElement>(*this)) {
        URL url = trackElement.getNonEmptyURLAttribute(srcAttr);
        if (url.isEmpty())
            continue;

        if (!isAllowedToLoadMediaURL(*this, url, trackElement.isInUserAgentShadowTree()))
            continue;

        auto& track = trackElement.track();
        auto kind = track.kind();

        // FIXME: The switch statement below preserves existing behavior where we ignore chapters and metadata tracks.
        // If we confirm this behavior is valuable, we should remove this comment. Otherwise, remove both comment and switch.
        switch (kind) {
        case TextTrack::Kind::Captions:
        case TextTrack::Kind::Descriptions:
        case TextTrack::Kind::Forced:
        case TextTrack::Kind::Subtitles:
            break;
        case TextTrack::Kind::Chapters:
        case TextTrack::Kind::Metadata:
            continue;
        }

        outOfBandTrackSources.append(PlatformTextTrack::createOutOfBand(trackElement.label(), trackElement.srclang(), url.string(), toPlatform(track.mode()), toPlatform(kind), track.uniqueId(), trackElement.isDefault()));
    }

    return outOfBandTrackSources;
}

#endif

bool HTMLMediaElement::mediaPlayerNeedsSiteSpecificHacks() const
{
    return document().settings().needsSiteSpecificQuirks();
}

String HTMLMediaElement::mediaPlayerDocumentHost() const
{
    return document().url().host();
}

void HTMLMediaElement::mediaPlayerEnterFullscreen()
{
    enterFullscreen();
}

void HTMLMediaElement::mediaPlayerExitFullscreen()
{
    exitFullscreen();
}

bool HTMLMediaElement::mediaPlayerIsFullscreen() const
{
    return isFullscreen();
}

bool HTMLMediaElement::mediaPlayerIsFullscreenPermitted() const
{
    return m_mediaSession->fullscreenPermitted(*this);
}

bool HTMLMediaElement::mediaPlayerIsVideo() const
{
    return isVideo();
}

LayoutRect HTMLMediaElement::mediaPlayerContentBoxRect() const
{
    auto* renderer = this->renderer();
    if (!renderer)
        return { };
    return renderer->enclosingBox().contentBoxRect();
}

float HTMLMediaElement::mediaPlayerContentsScale() const
{
    if (auto page = document().page())
        return page->pageScaleFactor() * page->deviceScaleFactor();
    return 1;
}

void HTMLMediaElement::mediaPlayerSetSize(const IntSize& size)
{
    setIntegralAttribute(widthAttr, size.width());
    setIntegralAttribute(heightAttr, size.height());
}

void HTMLMediaElement::mediaPlayerPause()
{
    pause();
}

void HTMLMediaElement::mediaPlayerPlay()
{
    play();
}

bool HTMLMediaElement::mediaPlayerPlatformVolumeConfigurationRequired() const
{
    return !m_volumeInitialized;
}

bool HTMLMediaElement::mediaPlayerIsPaused() const
{
    return paused();
}

bool HTMLMediaElement::mediaPlayerIsLooping() const
{
    return loop();
}

CachedResourceLoader* HTMLMediaElement::mediaPlayerCachedResourceLoader()
{
    return &document().cachedResourceLoader();
}

RefPtr<PlatformMediaResourceLoader> HTMLMediaElement::mediaPlayerCreateResourceLoader()
{
    auto mediaResourceLoader = adoptRef(*new MediaResourceLoader(document(), *this, crossOrigin()));

    m_lastMediaResourceLoaderForTesting = mediaResourceLoader->createWeakPtr();

    return WTFMove(mediaResourceLoader);
}

const MediaResourceLoader* HTMLMediaElement::lastMediaResourceLoaderForTesting() const
{
    return m_lastMediaResourceLoaderForTesting.get();
}

bool HTMLMediaElement::mediaPlayerShouldUsePersistentCache() const
{
    if (Page* page = document().page())
        return !page->usesEphemeralSession() && !page->isResourceCachingDisabled();

    return false;
}

const String& HTMLMediaElement::mediaPlayerMediaCacheDirectory() const
{
    return mediaCacheDirectory();
}

bool HTMLMediaElement::mediaPlayerShouldWaitForResponseToAuthenticationChallenge(const AuthenticationChallenge& challenge)
{
    Frame* frame = document().frame();
    if (!frame)
        return false;

    Page* page = frame->page();
    if (!page)
        return false;

    ResourceRequest request(m_currentSrc);
    ResourceLoadNotifier& notifier = frame->loader().notifier();
    DocumentLoader* documentLoader = document().loader();
    unsigned long identifier = page->progress().createUniqueIdentifier();

    notifier.assignIdentifierToInitialRequest(identifier, documentLoader, request);
    notifier.didReceiveAuthenticationChallenge(identifier, documentLoader, challenge);

    return true;
}

String HTMLMediaElement::sourceApplicationIdentifier() const
{
    if (Frame* frame = document().frame()) {
        if (NetworkingContext* networkingContext = frame->loader().networkingContext())
            return networkingContext->sourceApplicationIdentifier();
    }
    return emptyString();
}

Vector<String> HTMLMediaElement::mediaPlayerPreferredAudioCharacteristics() const
{
    if (Page* page = document().page())
        return page->group().captionPreferences().preferredAudioCharacteristics();
    return Vector<String>();
}

#if PLATFORM(IOS)
String HTMLMediaElement::mediaPlayerNetworkInterfaceName() const
{
    return document().settings().networkInterfaceName();
}

bool HTMLMediaElement::mediaPlayerGetRawCookies(const URL& url, Vector<Cookie>& cookies) const
{
    return getRawCookies(document(), url, cookies);
}
#endif

bool HTMLMediaElement::mediaPlayerIsInMediaDocument() const
{
    return document().isMediaDocument();
}

void HTMLMediaElement::mediaPlayerEngineFailedToLoad() const
{
    if (!m_player)
        return;

    if (auto* page = document().page())
        page->diagnosticLoggingClient().logDiagnosticMessageWithValue(DiagnosticLoggingKeys::engineFailedToLoadKey(), m_player->engineDescription(), m_player->platformErrorCode(), 4, ShouldSample::No);
}

double HTMLMediaElement::mediaPlayerRequestedPlaybackRate() const
{
    return potentiallyPlaying() ? requestedPlaybackRate() : 0;
}

const Vector<ContentType>& HTMLMediaElement::mediaContentTypesRequiringHardwareSupport() const
{
    return document().settings().mediaContentTypesRequiringHardwareSupport();
}

bool HTMLMediaElement::mediaPlayerShouldCheckHardwareSupport() const
{
    if (!document().settings().allowMediaContentTypesRequiringHardwareSupportAsFallback())
        return true;

    if (m_loadState == LoadingFromSourceElement && m_currentSourceNode && !m_nextChildNodeToConsider)
        return false;

    if (m_loadState == LoadingFromSrcAttr)
        return false;

    return true;
}

#if USE(GSTREAMER)
void HTMLMediaElement::requestInstallMissingPlugins(const String& details, const String& description, MediaPlayerRequestInstallMissingPluginsCallback& callback)
{
    if (!document().page())
        return;

    document().page()->chrome().client().requestInstallMissingMediaPlugins(details, description, callback);
}
#endif

void HTMLMediaElement::removeBehaviorsRestrictionsAfterFirstUserGesture(MediaElementSession::BehaviorRestrictions mask)
{
    MediaElementSession::BehaviorRestrictions restrictionsToRemove = mask &
        (MediaElementSession::RequireUserGestureForLoad
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
        | MediaElementSession::RequireUserGestureToShowPlaybackTargetPicker
        | MediaElementSession::RequireUserGestureToAutoplayToExternalDevice
#endif
        | MediaElementSession::RequireUserGestureForLoad
        | MediaElementSession::RequireUserGestureForVideoRateChange
        | MediaElementSession::RequireUserGestureForAudioRateChange
        | MediaElementSession::RequireUserGestureForFullscreen
        | MediaElementSession::RequireUserGestureForVideoDueToLowPowerMode
        | MediaElementSession::InvisibleAutoplayNotPermitted
        | MediaElementSession::RequireUserGestureToControlControlsManager);

    m_mediaSession->removeBehaviorRestriction(restrictionsToRemove);
    document().topDocument().noteUserInteractionWithMediaElement();
}

void HTMLMediaElement::updateRateChangeRestrictions()
{
    const auto& document = this->document();
    if (!document.ownerElement() && document.isMediaDocument())
        return;

    const auto& topDocument = document.topDocument();
    if (topDocument.videoPlaybackRequiresUserGesture())
        m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureForVideoRateChange);
    else
        m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequireUserGestureForVideoRateChange);

    if (topDocument.audioPlaybackRequiresUserGesture())
        m_mediaSession->addBehaviorRestriction(MediaElementSession::RequireUserGestureForAudioRateChange);
    else
        m_mediaSession->removeBehaviorRestriction(MediaElementSession::RequireUserGestureForAudioRateChange);
}

#if ENABLE(MEDIA_SOURCE)
RefPtr<VideoPlaybackQuality> HTMLMediaElement::getVideoPlaybackQuality()
{
    DOMWindow* domWindow = document().domWindow();
    double timestamp = domWindow ? 1000 * domWindow->nowTimestamp() : 0;

    auto metrics = m_player ? m_player->videoPlaybackQualityMetrics() : std::nullopt;
    if (!metrics)
        return VideoPlaybackQuality::create(timestamp, 0, 0, 0, 0);

    return VideoPlaybackQuality::create(timestamp,
        metrics.value().totalVideoFrames + m_droppedVideoFrames,
        metrics.value().droppedVideoFrames + m_droppedVideoFrames,
        metrics.value().corruptedVideoFrames,
        metrics.value().totalFrameDelay);
}
#endif

#if ENABLE(MEDIA_CONTROLS_SCRIPT)
DOMWrapperWorld& HTMLMediaElement::ensureIsolatedWorld()
{
    if (!m_isolatedWorld)
        m_isolatedWorld = DOMWrapperWorld::create(commonVM());
    return *m_isolatedWorld;
}

bool HTMLMediaElement::ensureMediaControlsInjectedScript()
{
    LOG(Media, "HTMLMediaElement::ensureMediaControlsInjectedScript(%p)", this);
    Page* page = document().page();
    if (!page)
        return false;

    String mediaControlsScript = RenderTheme::singleton().mediaControlsScript();
    if (!mediaControlsScript.length())
        return false;

    DOMWrapperWorld& world = ensureIsolatedWorld();
    ScriptController& scriptController = document().frame()->script();
    JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
    JSC::VM& vm = globalObject->vm();
    JSC::JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::ExecState* exec = globalObject->globalExec();

    JSC::JSValue functionValue = globalObject->get(exec, JSC::Identifier::fromString(exec, "createControls"));
    if (functionValue.isFunction())
        return true;

#ifndef NDEBUG
    // Setting a scriptURL allows the source to be debuggable in the inspector.
    URL scriptURL = URL(ParsedURLString, ASCIILiteral("mediaControlsScript"));
#else
    URL scriptURL;
#endif
    scriptController.evaluateInWorld(ScriptSourceCode(mediaControlsScript, scriptURL), world);
    if (UNLIKELY(scope.exception())) {
        scope.clearException();
        return false;
    }

    return true;
}

void HTMLMediaElement::updatePageScaleFactorJSProperty()
{
    Page* page = document().page();
    if (!page)
        return;

    setControllerJSProperty("pageScaleFactor", JSC::jsNumber(page->pageScaleFactor()));
}

void HTMLMediaElement::updateUsesLTRUserInterfaceLayoutDirectionJSProperty()
{
    Page* page = document().page();
    if (!page)
        return;

    bool usesLTRUserInterfaceLayoutDirectionProperty = page->userInterfaceLayoutDirection() == UserInterfaceLayoutDirection::LTR;
    setControllerJSProperty("usesLTRUserInterfaceLayoutDirection", JSC::jsBoolean(usesLTRUserInterfaceLayoutDirectionProperty));
}

void HTMLMediaElement::setControllerJSProperty(const char* propertyName, JSC::JSValue propertyValue)
{
    DOMWrapperWorld& world = ensureIsolatedWorld();
    ScriptController& scriptController = document().frame()->script();
    JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
    JSC::ExecState* exec = globalObject->globalExec();
    JSC::JSLockHolder lock(exec);

    JSC::JSValue controllerValue = controllerJSValue(*exec, *globalObject, *this);
    if (controllerValue.isNull())
        return;

    JSC::PutPropertySlot propertySlot(controllerValue);
    JSC::JSObject* controllerObject = controllerValue.toObject(exec);
    if (!controllerObject)
        return;

    controllerObject->methodTable()->put(controllerObject, exec, JSC::Identifier::fromString(exec, propertyName), propertyValue, propertySlot);
}

void HTMLMediaElement::didAddUserAgentShadowRoot(ShadowRoot* root)
{
    LOG(Media, "HTMLMediaElement::didAddUserAgentShadowRoot(%p)", this);

    Page* page = document().page();
    if (!page)
        return;

    DOMWrapperWorld& world = ensureIsolatedWorld();

    if (!ensureMediaControlsInjectedScript())
        return;

    ScriptController& scriptController = document().frame()->script();
    JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
    JSC::VM& vm = globalObject->vm();
    JSC::JSLockHolder lock(vm);
    auto scope = DECLARE_CATCH_SCOPE(vm);
    JSC::ExecState* exec = globalObject->globalExec();

    // The media controls script must provide a method with the following details.
    // Name: createControls
    // Parameters:
    //     1. The ShadowRoot element that will hold the controls.
    //     2. This object (and HTMLMediaElement).
    //     3. The MediaControlsHost object.
    // Return value:
    //     A reference to the created media controller instance.

    JSC::JSValue functionValue = globalObject->get(exec, JSC::Identifier::fromString(exec, "createControls"));
    if (functionValue.isUndefinedOrNull())
        return;

    if (!m_mediaControlsHost)
        m_mediaControlsHost = MediaControlsHost::create(this);

    auto mediaJSWrapper = toJS(exec, globalObject, *this);
    auto mediaControlsHostJSWrapper = toJS(exec, globalObject, *m_mediaControlsHost);
    
    JSC::MarkedArgumentBuffer argList;
    argList.append(toJS(exec, globalObject, root));
    argList.append(mediaJSWrapper);
    argList.append(mediaControlsHostJSWrapper);

    JSC::JSObject* function = functionValue.toObject(exec);
    scope.assertNoException();
    JSC::CallData callData;
    JSC::CallType callType = function->methodTable()->getCallData(function, callData);
    if (callType == JSC::CallType::None)
        return;

    JSC::JSValue controllerValue = JSC::call(exec, function, callType, callData, globalObject, argList);
    scope.clearException();
    JSC::JSObject* controllerObject = jsDynamicDowncast<JSC::JSObject*>(vm, controllerValue);
    if (!controllerObject)
        return;

    // Connect the Media, MediaControllerHost, and Controller so the GC knows about their relationship
    JSC::JSObject* mediaJSWrapperObject = mediaJSWrapper.toObject(exec);
    scope.assertNoException();
    JSC::Identifier controlsHost = JSC::Identifier::fromString(&exec->vm(), "controlsHost");
    
    ASSERT(!mediaJSWrapperObject->hasProperty(exec, controlsHost));

    mediaJSWrapperObject->putDirect(exec->vm(), controlsHost, mediaControlsHostJSWrapper, JSC::DontDelete | JSC::DontEnum | JSC::ReadOnly);

    JSC::JSObject* mediaControlsHostJSWrapperObject = jsDynamicDowncast<JSC::JSObject*>(vm, mediaControlsHostJSWrapper);
    if (!mediaControlsHostJSWrapperObject)
        return;
    
    JSC::Identifier controller = JSC::Identifier::fromString(&exec->vm(), "controller");

    ASSERT(!controllerObject->hasProperty(exec, controller));

    mediaControlsHostJSWrapperObject->putDirect(exec->vm(), controller, controllerValue, JSC::DontDelete | JSC::DontEnum | JSC::ReadOnly);

    updatePageScaleFactorJSProperty();
    updateUsesLTRUserInterfaceLayoutDirectionJSProperty();

    if (UNLIKELY(scope.exception()))
        scope.clearException();
}

void HTMLMediaElement::setMediaControlsDependOnPageScaleFactor(bool dependsOnPageScale)
{
    LOG(Media, "MediaElement::setMediaControlsDependPageScaleFactor(%p) = %s", this, boolString(dependsOnPageScale));

    if (document().settings().mediaControlsScaleWithPageZoom()) {
        LOG(Media, "MediaElement::setMediaControlsDependPageScaleFactor(%p) forced to false by Settings value", this);
        m_mediaControlsDependOnPageScaleFactor = false;
        return;
    }

    if (m_mediaControlsDependOnPageScaleFactor == dependsOnPageScale)
        return;

    m_mediaControlsDependOnPageScaleFactor = dependsOnPageScale;

    if (m_mediaControlsDependOnPageScaleFactor)
        document().registerForPageScaleFactorChangedCallbacks(this);
    else
        document().unregisterForPageScaleFactorChangedCallbacks(this);
}

void HTMLMediaElement::updateMediaControlsAfterPresentationModeChange()
{
    // Don't execute script if the controls script hasn't been injected yet, or we have
    // stopped/suspended the object.
    if (!m_mediaControlsHost || document().activeDOMObjectsAreSuspended() || document().activeDOMObjectsAreStopped())
        return;

    DOMWrapperWorld& world = ensureIsolatedWorld();
    ScriptController& scriptController = document().frame()->script();
    JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
    JSC::VM& vm = globalObject->vm();
    JSC::JSLockHolder lock(vm);
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSC::ExecState* exec = globalObject->globalExec();

    JSC::JSValue controllerValue = controllerJSValue(*exec, *globalObject, *this);
    JSC::JSObject* controllerObject = controllerValue.toObject(exec);

    RETURN_IF_EXCEPTION(scope, void());

    JSC::JSValue functionValue = controllerObject->get(exec, JSC::Identifier::fromString(exec, "handlePresentationModeChange"));
    if (UNLIKELY(scope.exception()) || functionValue.isUndefinedOrNull())
        return;

    JSC::JSObject* function = functionValue.toObject(exec);
    scope.assertNoException();
    JSC::CallData callData;
    JSC::CallType callType = function->methodTable()->getCallData(function, callData);
    if (callType == JSC::CallType::None)
        return;

    JSC::MarkedArgumentBuffer argList;
    JSC::call(exec, function, callType, callData, controllerObject, argList);
}

void HTMLMediaElement::pageScaleFactorChanged()
{
    updatePageScaleFactorJSProperty();
}

void HTMLMediaElement::userInterfaceLayoutDirectionChanged()
{
    updateUsesLTRUserInterfaceLayoutDirectionJSProperty();
}

String HTMLMediaElement::getCurrentMediaControlsStatus()
{
    DOMWrapperWorld& world = ensureIsolatedWorld();
    ensureMediaControlsShadowRoot();

    ScriptController& scriptController = document().frame()->script();
    JSDOMGlobalObject* globalObject = JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(world));
    JSC::VM& vm = globalObject->vm();
    JSC::JSLockHolder lock(vm);
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSC::ExecState* exec = globalObject->globalExec();

    JSC::JSValue controllerValue = controllerJSValue(*exec, *globalObject, *this);
    JSC::JSObject* controllerObject = controllerValue.toObject(exec);

    RETURN_IF_EXCEPTION(scope, emptyString());

    JSC::JSValue functionValue = controllerObject->get(exec, JSC::Identifier::fromString(exec, "getCurrentControlsStatus"));
    if (UNLIKELY(scope.exception()) || functionValue.isUndefinedOrNull())
        return emptyString();

    JSC::JSObject* function = functionValue.toObject(exec);
    scope.assertNoException();
    JSC::CallData callData;
    JSC::CallType callType = function->methodTable()->getCallData(function, callData);
    JSC::MarkedArgumentBuffer argList;
    if (callType == JSC::CallType::None)
        return emptyString();

    JSC::JSValue outputValue = JSC::call(exec, function, callType, callData, controllerObject, argList);

    RETURN_IF_EXCEPTION(scope, emptyString());

    return outputValue.getString(exec);
}
#endif // ENABLE(MEDIA_CONTROLS_SCRIPT)

unsigned long long HTMLMediaElement::fileSize() const
{
    if (m_player)
        return m_player->fileSize();
    
    return 0;
}

PlatformMediaSession::MediaType HTMLMediaElement::mediaType() const
{
    if (m_player && m_readyState >= HAVE_METADATA) {
        if (hasVideo() && hasAudio() && !muted())
            return PlatformMediaSession::VideoAudio;
        return hasVideo() ? PlatformMediaSession::Video : PlatformMediaSession::Audio;
    }

    return presentationType();
}

PlatformMediaSession::MediaType HTMLMediaElement::presentationType() const
{
    if (hasTagName(HTMLNames::videoTag))
        return muted() ? PlatformMediaSession::Video : PlatformMediaSession::VideoAudio;

    return PlatformMediaSession::Audio;
}

PlatformMediaSession::DisplayType HTMLMediaElement::displayType() const
{
    if (m_videoFullscreenMode == VideoFullscreenModeStandard)
        return PlatformMediaSession::Fullscreen;
    if (m_videoFullscreenMode & VideoFullscreenModePictureInPicture)
        return PlatformMediaSession::Optimized;
    if (m_videoFullscreenMode == VideoFullscreenModeNone)
        return PlatformMediaSession::Normal;

    ASSERT_NOT_REACHED();
    return PlatformMediaSession::Normal;
}

PlatformMediaSession::CharacteristicsFlags HTMLMediaElement::characteristics() const
{
    if (m_readyState < HAVE_METADATA)
        return PlatformMediaSession::HasNothing;

    PlatformMediaSession::CharacteristicsFlags state = PlatformMediaSession::HasNothing;
    if (isVideo() && hasVideo())
        state |= PlatformMediaSession::HasVideo;
    if (this->hasAudio())
        state |= PlatformMediaSession::HasAudio;

    return state;
}

bool HTMLMediaElement::canProduceAudio() const
{
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    // Because the remote target could unmute playback without notifying us, we must assume
    // that we may be playing audio.
    if (m_isPlayingToWirelessTarget)
        return true;
#endif

    if (muted())
        return false;

    return m_player && m_readyState >= HAVE_METADATA && hasAudio();
}

bool HTMLMediaElement::isSuspended() const
{
    return document().activeDOMObjectsAreSuspended() || document().activeDOMObjectsAreStopped();
}

#if ENABLE(MEDIA_SOURCE)
size_t HTMLMediaElement::maximumSourceBufferSize(const SourceBuffer& buffer) const
{
    return m_mediaSession->maximumMediaSourceBufferSize(buffer);
}
#endif

void HTMLMediaElement::suspendPlayback()
{
    LOG(Media, "HTMLMediaElement::suspendPlayback(%p) - paused = %s", this, boolString(paused()));
    if (!paused())
        pause();
}

void HTMLMediaElement::resumeAutoplaying()
{
    LOG(Media, "HTMLMediaElement::resumeAutoplaying(%p) - paused = %s", this, boolString(paused()));
    m_autoplaying = true;

    if (canTransitionFromAutoplayToPlay())
        play();
}

void HTMLMediaElement::mayResumePlayback(bool shouldResume)
{
    LOG(Media, "HTMLMediaElement::mayResumePlayback(%p) - paused = %s", this, boolString(paused()));
    if (paused() && shouldResume)
        play();
}
    
String HTMLMediaElement::mediaSessionTitle() const
{
    if (hasAttributeWithoutSynchronization(titleAttr))
        return attributeWithoutSynchronization(titleAttr);
    
    return m_currentSrc;
}

void HTMLMediaElement::didReceiveRemoteControlCommand(PlatformMediaSession::RemoteControlCommandType command, const PlatformMediaSession::RemoteCommandArgument* argument)
{
    LOG(Media, "HTMLMediaElement::didReceiveRemoteControlCommand(%p) - %i", this, static_cast<int>(command));

    UserGestureIndicator remoteControlUserGesture(ProcessingUserGesture, &document());
    switch (command) {
    case PlatformMediaSession::PlayCommand:
        play();
        break;
    case PlatformMediaSession::StopCommand:
    case PlatformMediaSession::PauseCommand:
        pause();
        break;
    case PlatformMediaSession::TogglePlayPauseCommand:
        canPlay() ? play() : pause();
        break;
    case PlatformMediaSession::BeginSeekingBackwardCommand:
        beginScanning(Backward);
        break;
    case PlatformMediaSession::BeginSeekingForwardCommand:
        beginScanning(Forward);
        break;
    case PlatformMediaSession::EndSeekingBackwardCommand:
    case PlatformMediaSession::EndSeekingForwardCommand:
        endScanning();
        break;
    case PlatformMediaSession::SeekToPlaybackPositionCommand:
        ASSERT(argument);
        if (argument)
            handleSeekToPlaybackPosition(argument->asDouble);
        break;
    default:
        { } // Do nothing
    }
}

static bool needsSeekingSupportQuirk(Document& document)
{
    if (!document.settings().needsSiteSpecificQuirks())
        return false;
            
    String host = document.topDocument().url().host();
    return equalLettersIgnoringASCIICase(host, "netflix.com") || host.endsWithIgnoringASCIICase(".netflix.com");
}

bool HTMLMediaElement::supportsSeeking() const 
{
    return !needsSeekingSupportQuirk(document()) && !isLiveStream();
}

bool HTMLMediaElement::shouldOverrideBackgroundPlaybackRestriction(PlatformMediaSession::InterruptionType type) const
{
    if (type == PlatformMediaSession::EnteringBackground) {
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
        if (m_isPlayingToWirelessTarget) {
            LOG(Media, "HTMLMediaElement::shouldOverrideBackgroundPlaybackRestriction(%p) - returning true because m_isPlayingToWirelessTarget is true", this);
            return true;
        }
#endif
        if (m_videoFullscreenMode & VideoFullscreenModePictureInPicture)
            return true;
#if PLATFORM(IOS) || (PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE))
        if (m_videoFullscreenMode == VideoFullscreenModeStandard && supportsPictureInPicture() && isPlaying())
            return true;
#endif
    } else if (type == PlatformMediaSession::SuspendedUnderLock) {
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
        if (m_isPlayingToWirelessTarget) {
            LOG(Media, "HTMLMediaElement::shouldOverrideBackgroundPlaybackRestriction(%p) - returning true because m_isPlayingToWirelessTarget is true", this);
            return true;
        }
#endif
    }
    return false;
}

bool HTMLMediaElement::processingUserGestureForMedia() const
{
    return document().processingUserGestureForMedia();
}

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
void HTMLMediaElement::updateMediaState(UpdateState updateState)
{
    if (updateState == UpdateState::Asynchronously) {
        scheduleDelayedAction(CheckMediaState);
        return;
    }

    MediaProducer::MediaStateFlags state = mediaState();
    if (m_mediaState == state)
        return;

    m_mediaState = state;
    m_mediaSession->mediaStateDidChange(*this, m_mediaState);
#if ENABLE(MEDIA_SESSION)
    document().updateIsPlayingMedia(m_elementID);
#else
    document().updateIsPlayingMedia();
#endif
}
#endif

MediaProducer::MediaStateFlags HTMLMediaElement::mediaState() const
{
    MediaStateFlags state = IsNotPlaying;

    bool hasActiveVideo = isVideo() && hasVideo();
    bool hasAudio = this->hasAudio();
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (m_isPlayingToWirelessTarget)
        state |= IsPlayingToExternalDevice;

    if (m_hasPlaybackTargetAvailabilityListeners) {
        state |= HasPlaybackTargetAvailabilityListener;
        if (!m_mediaSession->wirelessVideoPlaybackDisabled(*this))
            state |= RequiresPlaybackTargetMonitoring;
    }

    bool requireUserGesture = m_mediaSession->hasBehaviorRestriction(MediaElementSession::RequireUserGestureToAutoplayToExternalDevice);
    if (m_readyState >= HAVE_METADATA && !requireUserGesture && !m_failedToPlayToWirelessTarget)
        state |= ExternalDeviceAutoPlayCandidate;

    if (hasActiveVideo || hasAudio)
        state |= HasAudioOrVideo;

    if (hasActiveVideo && endedPlayback())
        state |= DidPlayToEnd;
#endif

    if (!isPlaying())
        return state;

    if (hasAudio && !muted() && volume())
        state |= IsPlayingAudio;

    if (hasActiveVideo)
        state |= IsPlayingVideo;

    return state;
}

void HTMLMediaElement::handleAutoplayEvent(AutoplayEvent event)
{
    if (Page* page = document().page()) {
        bool hasAudio = this->hasAudio() && !muted() && volume();
        page->chrome().client().handleAutoplayEvent(event, hasAudio ? AutoplayEventFlags::HasAudio : OptionSet<AutoplayEventFlags>());
    }
}

void HTMLMediaElement::userDidInterfereWithAutoplay()
{
    if (m_playbackWithoutUserGesture != PlaybackWithoutUserGesture::Started)
        return;

    // Only consider interference in the first 10 seconds of automatic playback.
    if (currentTime() - m_playbackWithoutUserGestureStartedTime->toDouble() > AutoplayInterferenceTimeThreshold)
        return;

    handleAutoplayEvent(AutoplayEvent::UserDidInterfereWithPlayback);
    setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::None);
}

void HTMLMediaElement::setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture playbackWithoutUserGesture)
{
    m_playbackWithoutUserGesture = playbackWithoutUserGesture;

    switch (playbackWithoutUserGesture) {
    case PlaybackWithoutUserGesture::Started:
        m_playbackWithoutUserGestureStartedTime = currentMediaTime();
        break;
    case PlaybackWithoutUserGesture::None:
        m_playbackWithoutUserGestureStartedTime = std::nullopt;
        break;
    case PlaybackWithoutUserGesture::Prevented:
        m_playbackWithoutUserGestureStartedTime = std::nullopt;

        dispatchPlayPauseEventsIfNeedsQuirks();
        handleAutoplayEvent(AutoplayEvent::DidPreventMediaFromPlaying);

        break;
    }
}

void HTMLMediaElement::pageMutedStateDidChange()
{
    updateVolume();

    if (Page* page = document().page()) {
        if (hasAudio() && !muted() && page->isAudioMuted())
            userDidInterfereWithAutoplay();
    }
}

bool HTMLMediaElement::effectiveMuted() const
{
    return muted() || (document().page() && document().page()->isAudioMuted());
}

bool HTMLMediaElement::doesHaveAttribute(const AtomicString& attribute, AtomicString* value) const
{
    QualifiedName attributeName(nullAtom(), attribute, nullAtom());

    auto& elementValue = attributeWithoutSynchronization(attributeName);
    if (elementValue.isNull())
        return false;
    
    if (attributeName == HTMLNames::x_itunes_inherit_uri_query_componentAttr && !document().settings().enableInheritURIQueryComponent())
        return false;

    if (value)
        *value = elementValue;
    
    return true;
}

void HTMLMediaElement::setShouldBufferData(bool shouldBuffer)
{
    if (m_player)
        m_player->setShouldBufferData(shouldBuffer);
}

void HTMLMediaElement::purgeBufferedDataIfPossible()
{
#if PLATFORM(IOS)
    if (!MemoryPressureHandler::singleton().isUnderMemoryPressure() && PlatformMediaSessionManager::sharedManager().sessionCanLoadMedia(*m_mediaSession))
        return;

    if (m_isPlayingToWirelessTarget) {
        LOG(Media, "HTMLMediaElement::purgeBufferedDataIfPossible(%p) - early return because m_isPlayingToWirelessTarget is true", this);
        return;
    }

    // This is called to relieve memory pressure. Turning off buffering causes the media playback
    // daemon to release memory associated with queued-up video frames.
    // We turn it back on right away, but new frames won't get loaded unless playback is resumed.
    setShouldBufferData(false);
    setShouldBufferData(true);
#endif
}

bool HTMLMediaElement::canSaveMediaData() const
{
    if (m_player)
        return m_player->canSaveMediaData();

    return false;
}

#if ENABLE(MEDIA_SESSION)
double HTMLMediaElement::playerVolume() const
{
    return m_player ? m_player->volume() : 0;
}

MediaSession* HTMLMediaElement::session() const
{
    MediaSession* session = m_session.get();
    if (session && session == &document().defaultMediaSession())
        return nullptr;

    return session;
}

void HTMLMediaElement::setSession(MediaSession* session)
{
    // 6.1. Extensions to the HTMLMediaElement interface
    // 1. Let m be the media element in question.
    // 2. Let old media session be m’s current media session, if it has one, and null otherwise.
    // 3. Let m’s current media session be the new value or the top-level browsing context’s media session if the new value is null.
    // 4. Let new media session be m’s current media session.

    // 5. Update media sessions: If old media session and new media session are the same (whether both null or both the same media session), then terminate these steps.
    if (m_session.get() == session)
        return;

    if (m_session) {
        // 6. If m is an audio-producing participant of old media session, then pause m and remove m from old media session’s list of audio-producing participants.
        if (m_session->isMediaElementActive(*this))
            pause();

        m_session->removeMediaElement(*this);

        // 7. If old media session is not null and no longer has one or more audio-producing participants, then run the media session deactivation algorithm for old media session.
        if (!m_session->hasActiveMediaElements())
            m_session->deactivate();
    }

    if (session)
        setSessionInternal(*session);
    else
        setSessionInternal(document().defaultMediaSession());
}

void HTMLMediaElement::setSessionInternal(MediaSession& session)
{
    m_session = &session;
    session.addMediaElement(*this);
    m_kind = session.kind();
}

void HTMLMediaElement::setShouldDuck(bool duck)
{
    if (m_shouldDuck == duck)
        return;

    m_shouldDuck = duck;
    updateVolume();
}

#endif

void HTMLMediaElement::allowsMediaDocumentInlinePlaybackChanged()
{
    if (potentiallyPlaying() && m_mediaSession->requiresFullscreenForVideoPlayback(*this) && !isFullscreen())
        enterFullscreen();
}
    
bool HTMLMediaElement::isVideoTooSmallForInlinePlayback()
{
    auto* renderer = this->renderer();
    
    if (!renderer || !is<RenderVideo>(*renderer))
        return true;
    
    IntRect videoBox = downcast<RenderVideo>(*renderer).videoBox();
    return (videoBox.width() <= 1 || videoBox.height() <= 1);
}

void HTMLMediaElement::isVisibleInViewportChanged()
{
    m_visibilityChangeTaskQueue.enqueueTask([this] {
        updateShouldAutoplay();
        scheduleUpdatePlaybackControlsManager();
    });
}

void HTMLMediaElement::updateShouldAutoplay()
{
    if (!autoplay())
        return;

    if (!m_mediaSession->hasBehaviorRestriction(MediaElementSession::InvisibleAutoplayNotPermitted))
        return;

    bool canAutoplay = mediaSession().autoplayPermitted();
    if (canAutoplay
        && m_mediaSession->state() == PlatformMediaSession::Interrupted
        && m_mediaSession->interruptionType() == PlatformMediaSession::InvisibleAutoplay)
        m_mediaSession->endInterruption(PlatformMediaSession::MayResumePlaying);
    else if (!canAutoplay
        && m_mediaSession->state() != PlatformMediaSession::Interrupted)
        m_mediaSession->beginInterruption(PlatformMediaSession::InvisibleAutoplay);
}

void HTMLMediaElement::updateShouldPlay()
{
    if (!paused() && !m_mediaSession->playbackPermitted(*this)) {
        pauseInternal();
        setPlaybackWithoutUserGesture(PlaybackWithoutUserGesture::Prevented);
    } else if (canTransitionFromAutoplayToPlay())
        play();
}

void HTMLMediaElement::resetPlaybackSessionState()
{
    if (m_mediaSession)
        m_mediaSession->resetPlaybackSessionState();
}

bool HTMLMediaElement::isVisibleInViewport() const
{
    auto renderer = this->renderer();
    return renderer && renderer->visibleInViewportState() == VisibleInViewportState::Yes;
}

void HTMLMediaElement::updatePlaybackControlsManager()
{
    Page* page = document().page();
    if (!page)
        return;

    // FIXME: Ensure that the renderer here should be up to date.
    if (auto bestMediaElement = bestMediaElementForShowingPlaybackControlsManager(MediaElementSession::PlaybackControlsPurpose::ControlsManager))
        page->chrome().client().setUpPlaybackControlsManager(*bestMediaElement);
    else
        page->chrome().client().clearPlaybackControlsManager();
}

void HTMLMediaElement::scheduleUpdatePlaybackControlsManager()
{
    if (!m_updatePlaybackControlsManagerQueue.hasPendingTasks())
        m_updatePlaybackControlsManagerQueue.enqueueTask(std::bind(&HTMLMediaElement::updatePlaybackControlsManager, this));
}

void HTMLMediaElement::playbackControlsManagerBehaviorRestrictionsTimerFired()
{
    if (m_playbackControlsManagerBehaviorRestrictionsQueue.hasPendingTasks())
        return;

    if (!m_mediaSession->hasBehaviorRestriction(MediaElementSession::RequireUserGestureToControlControlsManager))
        return;

    RefPtr<HTMLMediaElement> protectedThis(this);
    m_playbackControlsManagerBehaviorRestrictionsQueue.enqueueTask([protectedThis] () {
        MediaElementSession* mediaElementSession = protectedThis->m_mediaSession.get();
        if (protectedThis->isPlaying() || mediaElementSession->state() == PlatformMediaSession::Autoplaying || mediaElementSession->state() == PlatformMediaSession::Playing)
            return;

        mediaElementSession->addBehaviorRestriction(MediaElementSession::RequirePlaybackToControlControlsManager);
        protectedThis->scheduleUpdatePlaybackControlsManager();
    });
}

bool HTMLMediaElement::shouldOverrideBackgroundLoadingRestriction() const
{
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
    if (isPlayingToWirelessPlaybackTarget())
        return true;
#endif

    return m_videoFullscreenMode == VideoFullscreenModePictureInPicture;
}

void HTMLMediaElement::fullscreenModeChanged(VideoFullscreenMode mode)
{
    if (m_videoFullscreenMode == mode)
        return;

    m_videoFullscreenMode = mode;
    visibilityStateChanged();
    m_mediaSession->scheduleClientDataBufferingCheck();
    scheduleUpdatePlaybackControlsManager();
}

}

#endif