From 40736c5763bf61337c8c14e16d8587db021a87d4 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Fri, 6 Jan 2012 14:44:00 +0100 Subject: Imported WebKit commit 2ea9d364d0f6efa8fa64acf19f451504c59be0e4 (http://svn.webkit.org/repository/webkit/trunk@104285) --- Source/WebCore/html/HTMLMediaElement.cpp | 3727 ++++++++++++++++++++++++++++++ 1 file changed, 3727 insertions(+) create mode 100644 Source/WebCore/html/HTMLMediaElement.cpp (limited to 'Source/WebCore/html/HTMLMediaElement.cpp') diff --git a/Source/WebCore/html/HTMLMediaElement.cpp b/Source/WebCore/html/HTMLMediaElement.cpp new file mode 100644 index 000000000..0b693b2b9 --- /dev/null +++ b/Source/WebCore/html/HTMLMediaElement.cpp @@ -0,0 +1,3727 @@ +/* + * Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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 COMPUTER, 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 COMPUTER, 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" + +#if ENABLE(VIDEO) +#include "HTMLMediaElement.h" + +#include "ApplicationCacheHost.h" +#include "ApplicationCacheResource.h" +#include "Attribute.h" +#include "Chrome.h" +#include "ChromeClient.h" +#include "ClientRect.h" +#include "ClientRectList.h" +#include "ContentSecurityPolicy.h" +#include "ContentType.h" +#include "CSSPropertyNames.h" +#include "CSSValueKeywords.h" +#include "DocumentLoader.h" +#include "Event.h" +#include "EventNames.h" +#include "ExceptionCode.h" +#include "Frame.h" +#include "FrameLoader.h" +#include "FrameLoaderClient.h" +#include "FrameView.h" +#include "HTMLDocument.h" +#include "HTMLNames.h" +#include "HTMLSourceElement.h" +#include "HTMLVideoElement.h" +#include "Logging.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 "MouseEvent.h" +#include "MIMETypeRegistry.h" +#include "Page.h" +#include "RenderVideo.h" +#include "RenderView.h" +#include "ScriptController.h" +#include "ScriptEventListener.h" +#include "SecurityOrigin.h" +#include "Settings.h" +#include "ShadowRoot.h" +#include "TimeRanges.h" +#include "UUID.h" +#include +#include +#include +#include +#include + +#if USE(ACCELERATED_COMPOSITING) +#include "RenderView.h" +#include "RenderLayerCompositor.h" +#endif + +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) +#include "RenderEmbeddedObject.h" +#include "Widget.h" +#endif + +#if ENABLE(VIDEO_TRACK) +#include "HTMLTrackElement.h" +#include "RuntimeEnabledFeatures.h" +#include "TextTrackCueList.h" +#include "TextTrackList.h" +#endif + +#if ENABLE(WEB_AUDIO) +#include "AudioSourceProvider.h" +#include "MediaElementAudioSourceNode.h" +#endif + +#if PLATFORM(MAC) +#include "DisplaySleepDisabler.h" +#endif + +using namespace std; + +namespace WebCore { + +#if !LOG_DISABLED +static String urlForLogging(const KURL& 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"; +} +#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 + +static const float invalidMediaTime = -1; + +#if ENABLE(MEDIA_SOURCE) +// URL protocol used to signal that the media source API is being used. +static const char* mediaSourceURLProtocol = "x-media-source"; +#endif + +using namespace HTMLNames; +using namespace std; + +typedef HashMap > DocumentElementSetMap; +static DocumentElementSetMap& documentToElementSetMap() +{ + DEFINE_STATIC_LOCAL(DocumentElementSetMap, map, ()); + return map; +} + +static void addElementToDocumentMap(HTMLMediaElement* element, Document* document) +{ + DocumentElementSetMap& map = documentToElementSetMap(); + HashSet set = map.take(document); + set.add(element); + map.add(document, set); +} + +static void removeElementFromDocumentMap(HTMLMediaElement* element, Document* document) +{ + DocumentElementSetMap& map = documentToElementSetMap(); + HashSet set = map.take(document); + set.remove(element); + if (!set.isEmpty()) + map.add(document, set); +} + +HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document* document, bool createdByParser) + : HTMLElement(tagName, document) + , ActiveDOMObject(document, this) + , m_loadTimer(this, &HTMLMediaElement::loadTimerFired) + , m_asyncEventTimer(this, &HTMLMediaElement::asyncEventTimerFired) + , m_progressEventTimer(this, &HTMLMediaElement::progressEventTimerFired) + , m_playbackProgressTimer(this, &HTMLMediaElement::playbackProgressTimerFired) + , m_playedTimeRanges() + , m_playbackRate(1.0f) + , m_defaultPlaybackRate(1.0f) + , m_webkitPreservesPitch(true) + , m_networkState(NETWORK_EMPTY) + , m_readyState(HAVE_NOTHING) + , m_readyStateMaximum(HAVE_NOTHING) + , m_volume(1.0f) + , m_lastSeekTime(0) + , m_previousProgress(0) + , m_previousProgressTime(numeric_limits::max()) + , m_lastTimeUpdateEventWallTime(0) + , m_lastTimeUpdateEventMovieTime(numeric_limits::max()) + , m_loadState(WaitingForSource) + , m_currentSourceNode(0) + , m_nextChildNodeToConsider(0) +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + , m_proxyWidget(0) +#endif + , m_restrictions(RequireUserGestureForFullscreenRestriction | RequirePageConsentToLoadMediaRestriction) + , m_preload(MediaPlayer::Auto) + , m_displayMode(Unknown) + , m_processingMediaPlayerCallback(0) +#if ENABLE(MEDIA_SOURCE) + , m_sourceState(SOURCE_CLOSED) +#endif + , m_cachedTime(invalidMediaTime) + , m_cachedTimeWallClockUpdateTime(0) + , m_minimumWallClockTimeToCacheMediaTime(0) + , m_fragmentStartTime(invalidMediaTime) + , m_fragmentEndTime(invalidMediaTime) + , m_pendingLoadFlags(0) + , m_playing(false) + , m_isWaitingUntilMediaCanStart(false) + , m_shouldDelayLoadEvent(false) + , m_haveFiredLoadedData(false) + , m_inActiveDocument(true) + , m_autoplaying(true) + , m_muted(false) + , m_paused(true) + , m_seeking(false) + , m_sentStalledEvent(false) + , m_sentEndEvent(false) + , m_pausedInternal(false) + , m_sendProgressEvents(true) + , m_isFullscreen(false) + , m_closedCaptionsVisible(false) +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + , m_needWidgetUpdate(false) +#endif + , m_dispatchingCanPlayEvent(false) + , m_loadInitiatedByUserGesture(false) + , m_completelyLoaded(false) + , m_havePreparedToPlay(false) + , m_parsingInProgress(createdByParser) +#if ENABLE(VIDEO_TRACK) + , m_tracksAreReady(true) + , m_haveVisibleTextTrack(false) + , m_textTracks(0) +#endif +#if ENABLE(WEB_AUDIO) + , m_audioSourceNode(0) +#endif +{ + LOG(Media, "HTMLMediaElement::HTMLMediaElement"); + document->registerForMediaVolumeCallbacks(this); + document->registerForPrivateBrowsingStateChangedCallbacks(this); + + if (document->settings() && document->settings()->mediaPlaybackRequiresUserGesture()) + addBehaviorRestriction(RequireUserGestureForRateChangeRestriction); + +#if ENABLE(MEDIA_SOURCE) + m_mediaSourceURL.setProtocol(mediaSourceURLProtocol); + m_mediaSourceURL.setPath(createCanonicalUUIDString()); +#endif + + setHasCustomWillOrDidRecalcStyle(); + addElementToDocumentMap(this, document); +} + +HTMLMediaElement::~HTMLMediaElement() +{ + LOG(Media, "HTMLMediaElement::~HTMLMediaElement"); + if (m_isWaitingUntilMediaCanStart) + document()->removeMediaCanStartListener(this); + setShouldDelayLoadEvent(false); + document()->unregisterForMediaVolumeCallbacks(this); + document()->unregisterForPrivateBrowsingStateChangedCallbacks(this); +#if ENABLE(VIDEO_TRACK) + if (m_textTracks) + m_textTracks->clearOwner(); + if (m_textTracks) { + for (unsigned i = 0; i < m_textTracks->length(); ++i) + m_textTracks->item(i)->clearClient(); + } +#endif + + if (m_mediaController) + m_mediaController->removeMediaElement(this); + + removeElementFromDocumentMap(this, document()); +} + +void HTMLMediaElement::didMoveToNewDocument(Document* oldDocument) +{ + if (m_isWaitingUntilMediaCanStart) { + if (oldDocument) + oldDocument->removeMediaCanStartListener(this); + document()->addMediaCanStartListener(this); + } + + if (m_shouldDelayLoadEvent) { + if (oldDocument) + oldDocument->decrementLoadEventDelayCount(); + document()->incrementLoadEventDelayCount(); + } + + if (oldDocument) { + oldDocument->unregisterForMediaVolumeCallbacks(this); + removeElementFromDocumentMap(this, oldDocument); + } + + document()->registerForMediaVolumeCallbacks(this); + addElementToDocumentMap(this, document()); + + HTMLElement::didMoveToNewDocument(oldDocument); +} + +bool HTMLMediaElement::supportsFocus() const +{ + if (ownerDocument()->isMediaDocument()) + return false; + + // If no controls specified, we should still be able to focus the element if it has tabIndex. + return controls() || HTMLElement::supportsFocus(); +} + +void HTMLMediaElement::attributeChanged(Attribute* attr, bool preserveDecls) +{ + HTMLElement::attributeChanged(attr, preserveDecls); + + const QualifiedName& attrName = attr->name(); + if (attrName == srcAttr) { + // Trigger a reload, as long as the 'src' attribute is present. + if (!getAttribute(srcAttr).isEmpty()) + scheduleLoad(MediaResource); + } else if (attrName == controlsAttr) + configureMediaControls(); +} + +void HTMLMediaElement::parseMappedAttribute(Attribute* attr) +{ + const QualifiedName& attrName = attr->name(); + + if (attrName == preloadAttr) { + String value = attr->value(); + + if (equalIgnoringCase(value, "none")) + m_preload = MediaPlayer::None; + else if (equalIgnoringCase(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_player) + m_player->setPreload(m_preload); + + } else if (attrName == mediagroupAttr) + setMediaGroup(attr->value()); + else if (attrName == onabortAttr) + setAttributeEventListener(eventNames().abortEvent, createAttributeEventListener(this, attr)); + else if (attrName == onbeforeloadAttr) + setAttributeEventListener(eventNames().beforeloadEvent, createAttributeEventListener(this, attr)); + else if (attrName == oncanplayAttr) + setAttributeEventListener(eventNames().canplayEvent, createAttributeEventListener(this, attr)); + else if (attrName == oncanplaythroughAttr) + setAttributeEventListener(eventNames().canplaythroughEvent, createAttributeEventListener(this, attr)); + else if (attrName == ondurationchangeAttr) + setAttributeEventListener(eventNames().durationchangeEvent, createAttributeEventListener(this, attr)); + else if (attrName == onemptiedAttr) + setAttributeEventListener(eventNames().emptiedEvent, createAttributeEventListener(this, attr)); + else if (attrName == onendedAttr) + setAttributeEventListener(eventNames().endedEvent, createAttributeEventListener(this, attr)); + else if (attrName == onerrorAttr) + setAttributeEventListener(eventNames().errorEvent, createAttributeEventListener(this, attr)); + else if (attrName == onloadeddataAttr) + setAttributeEventListener(eventNames().loadeddataEvent, createAttributeEventListener(this, attr)); + else if (attrName == onloadedmetadataAttr) + setAttributeEventListener(eventNames().loadedmetadataEvent, createAttributeEventListener(this, attr)); + else if (attrName == onloadstartAttr) + setAttributeEventListener(eventNames().loadstartEvent, createAttributeEventListener(this, attr)); + else if (attrName == onpauseAttr) + setAttributeEventListener(eventNames().pauseEvent, createAttributeEventListener(this, attr)); + else if (attrName == onplayAttr) + setAttributeEventListener(eventNames().playEvent, createAttributeEventListener(this, attr)); + else if (attrName == onplayingAttr) + setAttributeEventListener(eventNames().playingEvent, createAttributeEventListener(this, attr)); + else if (attrName == onprogressAttr) + setAttributeEventListener(eventNames().progressEvent, createAttributeEventListener(this, attr)); + else if (attrName == onratechangeAttr) + setAttributeEventListener(eventNames().ratechangeEvent, createAttributeEventListener(this, attr)); + else if (attrName == onseekedAttr) + setAttributeEventListener(eventNames().seekedEvent, createAttributeEventListener(this, attr)); + else if (attrName == onseekingAttr) + setAttributeEventListener(eventNames().seekingEvent, createAttributeEventListener(this, attr)); + else if (attrName == onstalledAttr) + setAttributeEventListener(eventNames().stalledEvent, createAttributeEventListener(this, attr)); + else if (attrName == onsuspendAttr) + setAttributeEventListener(eventNames().suspendEvent, createAttributeEventListener(this, attr)); + else if (attrName == ontimeupdateAttr) + setAttributeEventListener(eventNames().timeupdateEvent, createAttributeEventListener(this, attr)); + else if (attrName == onvolumechangeAttr) + setAttributeEventListener(eventNames().volumechangeEvent, createAttributeEventListener(this, attr)); + else if (attrName == onwaitingAttr) + setAttributeEventListener(eventNames().waitingEvent, createAttributeEventListener(this, attr)); + else if (attrName == onwebkitbeginfullscreenAttr) + setAttributeEventListener(eventNames().webkitbeginfullscreenEvent, createAttributeEventListener(this, attr)); + else if (attrName == onwebkitendfullscreenAttr) + setAttributeEventListener(eventNames().webkitendfullscreenEvent, createAttributeEventListener(this, attr)); + else + HTMLElement::parseMappedAttribute(attr); +} + +void HTMLMediaElement::finishParsingChildren() +{ + HTMLElement::finishParsingChildren(); + m_parsingInProgress = false; + +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + document()->updateStyleIfNeeded(); + createMediaPlayerProxy(); +#endif + +#if ENABLE(VIDEO_TRACK) + if (!RuntimeEnabledFeatures::webkitVideoTrackEnabled()) + return; + + for (Node* node = firstChild(); node; node = node->nextSibling()) { + if (node->hasTagName(trackTag)) { + scheduleLoad(TextTrackResource); + break; + } + } +#endif +} + +bool HTMLMediaElement::rendererIsNeeded(const NodeRenderingContext& context) +{ +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + UNUSED_PARAM(context); + Frame* frame = document()->frame(); + if (!frame) + return false; + + return true; +#else + return controls() ? HTMLElement::rendererIsNeeded(context) : false; +#endif +} + +RenderObject* HTMLMediaElement::createRenderer(RenderArena* arena, RenderStyle*) +{ +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + // Setup the renderer if we already have a proxy widget. + RenderEmbeddedObject* mediaRenderer = new (arena) RenderEmbeddedObject(this); + if (m_proxyWidget) { + mediaRenderer->setWidget(m_proxyWidget); + + if (Frame* frame = document()->frame()) + frame->loader()->client()->showMediaPlayerProxyPlugin(m_proxyWidget.get()); + } + return mediaRenderer; +#else + return new (arena) RenderMedia(this); +#endif +} + +void HTMLMediaElement::insertedIntoDocument() +{ + LOG(Media, "HTMLMediaElement::insertedIntoDocument"); + HTMLElement::insertedIntoDocument(); + if (!getAttribute(srcAttr).isEmpty() && m_networkState == NETWORK_EMPTY) + scheduleLoad(MediaResource); +} + +void HTMLMediaElement::removedFromDocument() +{ + LOG(Media, "HTMLMediaElement::removedFromDocument"); + if (m_networkState > NETWORK_EMPTY) + pause(); + if (m_isFullscreen) + exitFullscreen(); + HTMLElement::removedFromDocument(); +} + +void HTMLMediaElement::attach() +{ + ASSERT(!attached()); + +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + m_needWidgetUpdate = true; +#endif + + HTMLElement::attach(); + + if (renderer()) + renderer()->updateFromElement(); +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + else if (m_proxyWidget) { + if (Frame* frame = document()->frame()) + frame->loader()->client()->hideMediaPlayerProxyPlugin(m_proxyWidget.get()); + } +#endif +} + +void HTMLMediaElement::didRecalcStyle(StyleChange) +{ + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::scheduleLoad(LoadType loadType) +{ + LOG(Media, "HTMLMediaElement::scheduleLoad"); + + if ((loadType & MediaResource) && !(m_pendingLoadFlags & MediaResource)) { +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + createMediaPlayerProxy(); +#endif + + prepareForLoad(); + m_pendingLoadFlags |= MediaResource; + } + +#if ENABLE(VIDEO_TRACK) + if (loadType & TextTrackResource) + m_pendingLoadFlags |= TextTrackResource; +#endif + + if (!m_loadTimer.isActive()) + m_loadTimer.startOneShot(0); +} + +void HTMLMediaElement::scheduleNextSourceChild() +{ + // Schedule the timer to try the next element WITHOUT resetting state ala prepareForLoad. + m_pendingLoadFlags |= MediaResource; + m_loadTimer.startOneShot(0); +} + +void HTMLMediaElement::scheduleEvent(const AtomicString& eventName) +{ +#if LOG_MEDIA_EVENTS + LOG(Media, "HTMLMediaElement::scheduleEvent - scheduling '%s'", eventName.string().ascii().data()); +#endif + m_pendingEvents.append(Event::create(eventName, false, true)); + if (!m_asyncEventTimer.isActive()) + m_asyncEventTimer.startOneShot(0); +} + +void HTMLMediaElement::asyncEventTimerFired(Timer*) +{ + Vector > pendingEvents; + ExceptionCode ec = 0; + + m_pendingEvents.swap(pendingEvents); + unsigned count = pendingEvents.size(); + for (unsigned ndx = 0; ndx < count; ++ndx) { +#if LOG_MEDIA_EVENTS + LOG(Media, "HTMLMediaElement::asyncEventTimerFired - dispatching '%s'", pendingEvents[ndx]->type().string().ascii().data()); +#endif + if (pendingEvents[ndx]->type() == eventNames().canplayEvent) { + m_dispatchingCanPlayEvent = true; + dispatchEvent(pendingEvents[ndx].release(), ec); + m_dispatchingCanPlayEvent = false; + } else + dispatchEvent(pendingEvents[ndx].release(), ec); + } +} + +void HTMLMediaElement::loadTimerFired(Timer*) +{ + if (m_pendingLoadFlags & MediaResource) { + if (m_loadState == LoadingFromSourceElement) + loadNextSourceChild(); + else + loadInternal(); + } + +#if ENABLE(VIDEO_TRACK) + if (m_pendingLoadFlags & TextTrackResource) + configureTextTracks(); +#endif + + m_pendingLoadFlags = 0; +} + +PassRefPtr HTMLMediaElement::error() const +{ + return m_error; +} + +void HTMLMediaElement::setSrc(const String& url) +{ + setAttribute(srcAttr, url); +} + +HTMLMediaElement::NetworkState HTMLMediaElement::networkState() const +{ + return m_networkState; +} + +String HTMLMediaElement::canPlayType(const String& mimeType) const +{ + MediaPlayer::SupportsType support = MediaPlayer::supportsType(ContentType(mimeType)); + String canPlay; + + // 4.8.10.3 + switch (support) + { + case MediaPlayer::IsNotSupported: + canPlay = ""; + break; + case MediaPlayer::MayBeSupported: + canPlay = "maybe"; + break; + case MediaPlayer::IsSupported: + canPlay = "probably"; + break; + } + + LOG(Media, "HTMLMediaElement::canPlayType(%s) -> %s", mimeType.utf8().data(), canPlay.utf8().data()); + + return canPlay; +} + +void HTMLMediaElement::load(ExceptionCode& ec) +{ + LOG(Media, "HTMLMediaElement::load()"); + + if (userGestureRequiredForLoad() && !ScriptController::processingUserGesture()) + ec = INVALID_STATE_ERR; + else { + m_loadInitiatedByUserGesture = ScriptController::processingUserGesture(); + prepareForLoad(); + loadInternal(); + } + prepareToPlay(); +} + +void HTMLMediaElement::prepareForLoad() +{ + LOG(Media, "HTMLMediaElement::prepareForLoad"); + + // Perform the cleanup required for the resource load algorithm to run. + stopPeriodicTimers(); + m_loadTimer.stop(); + m_sentEndEvent = false; + m_sentStalledEvent = false; + m_haveFiredLoadedData = false; + m_completelyLoaded = false; + m_havePreparedToPlay = false; + m_displayMode = Unknown; + + // 1 - Abort any already-running instance of the resource selection algorithm for this element. + m_loadState = WaitingForSource; + m_currentSourceNode = 0; + + // 2 - If there are any tasks from the media element's media element event task source in + // one of the task queues, then remove those tasks. + cancelPendingEventsAndCallbacks(); + + // 3 - 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); + +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + createMediaPlayer(); +#else + if (m_player) + m_player->cancelLoad(); + else + createMediaPlayerProxy(); +#endif + +#if ENABLE(MEDIA_SOURCE) + if (m_sourceState != SOURCE_CLOSED) + setSourceState(SOURCE_CLOSED); +#endif + + // 4 - If the media element's networkState is not set to NETWORK_EMPTY, then run these substeps + if (m_networkState != NETWORK_EMPTY) { + m_networkState = NETWORK_EMPTY; + m_readyState = HAVE_NOTHING; + m_readyStateMaximum = HAVE_NOTHING; + refreshCachedTime(); + m_paused = true; + m_seeking = false; + invalidateCachedTime(); + scheduleEvent(eventNames().emptiedEvent); + updateMediaController(); + } + + // 5 - Set the playbackRate attribute to the value of the defaultPlaybackRate attribute. + setPlaybackRate(defaultPlaybackRate()); + + // 6 - Set the error attribute to null and the autoplaying flag to true. + m_error = 0; + m_autoplaying = true; + + // 7 - Invoke the media element's resource selection algorithm. + + // 8 - Note: Playback of any previously playing media resource for this element stops. + + // The resource selection algorithm + // 1 - Set the networkState to NETWORK_NO_SOURCE + m_networkState = NETWORK_NO_SOURCE; + + // 2 - Asynchronously await a stable state. + + m_playedTimeRanges = TimeRanges::create(); + m_lastSeekTime = 0; + m_closedCaptionsVisible = false; + + // The spec doesn't say to block the load event until we actually run the asynchronous section + // algorithm, but do it now because we won't start that until after the timer fires and the + // event may have already fired by then. + setShouldDelayLoadEvent(true); + +#if ENABLE(VIDEO_TRACK) + // HTMLMediaElement::textTracksAreReady will need "... the text tracks whose mode was not in the + // disabled state when the element's resource selection algorithm last started". + 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::DISABLED) + m_textTracksWhenResourceSelectionBegan.append(track); + } + } +#endif + + configureMediaControls(); +} + +void HTMLMediaElement::loadInternal() +{ + // If we can't start a load right away, start it later. + Page* page = document()->page(); + if (pageConsentRequiredForLoad() && page && !page->canStartMedia()) { + if (m_isWaitingUntilMediaCanStart) + return; + document()->addMediaCanStartListener(this); + m_isWaitingUntilMediaCanStart = true; + 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 in the the background. + removeBehaviorRestriction(RequirePageConsentToLoadMediaRestriction); + + selectMediaResource(); +} + +void HTMLMediaElement::selectMediaResource() +{ + LOG(Media, "HTMLMediaElement::selectMediaResource"); + + enum Mode { attribute, children }; + + // 3 - If the media element has a src attribute, then let mode be attribute. + Mode mode = attribute; + if (!fastHasAttribute(srcAttr)) { + Node* node; + for (node = firstChild(); node; node = node->nextSibling()) { + if (node->hasTagName(sourceTag)) + break; + } + + // Otherwise, if the media element does not have a src attribute but has a source + // element child, then let mode be children and let candidate be the first such + // source element child in tree order. + if (node) { + mode = children; + m_nextChildNodeToConsider = 0; + m_currentSourceNode = 0; + } else { + // Otherwise the media element has neither a src attribute nor a source element + // child: set the networkState to NETWORK_EMPTY, and abort these steps; the + // synchronous section ends. + m_loadState = WaitingForSource; + setShouldDelayLoadEvent(false); + m_networkState = NETWORK_EMPTY; + + LOG(Media, "HTMLMediaElement::selectMediaResource, nothing to load"); + return; + } + } + + // 4 - Set the media element's delaying-the-load-event flag to true (this delays the load event), + // and set its networkState to NETWORK_LOADING. + setShouldDelayLoadEvent(true); + m_networkState = NETWORK_LOADING; + + // 5 - Queue a task to fire a simple event named loadstart at the media element. + scheduleEvent(eventNames().loadstartEvent); + + // 6 - If mode is attribute, then run these substeps + if (mode == attribute) { + m_loadState = LoadingFromSrcAttr; + + // If the src attribute's value is the empty string ... jump down to the failed step below + KURL mediaURL = getNonEmptyURLAttribute(srcAttr); + if (mediaURL.isEmpty()) { + mediaLoadingFailed(MediaPlayer::FormatError); + LOG(Media, "HTMLMediaElement::selectMediaResource, empty 'src'"); + return; + } + + if (!isSafeToLoadURL(mediaURL, Complain) || !dispatchBeforeLoadEvent(mediaURL.string())) { + mediaLoadingFailed(MediaPlayer::FormatError); + return; + } + + // No type 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(mediaURL, contentType); + LOG(Media, "HTMLMediaElement::selectMediaResource, using 'src' attribute url"); + return; + } + + // Otherwise, the source elements will be used + loadNextSourceChild(); +} + +void HTMLMediaElement::loadNextSourceChild() +{ + ContentType contentType(""); + KURL mediaURL = selectNextSourceChild(&contentType, Complain); + if (!mediaURL.isValid()) { + waitForSourceChange(); + return; + } + +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + // Recreate the media player for the new url + createMediaPlayer(); +#endif + + m_loadState = LoadingFromSourceElement; + loadResource(mediaURL, contentType); +} + +#if !PLATFORM(CHROMIUM) +static KURL createFileURLForApplicationCacheResource(const String& path) +{ + // KURL should have a function to create a url from a path, but it does not. This function + // is not suitable because KURL::setPath uses encodeWithURLEscapeSequences, which it notes + // does not correctly escape '#' and '?'. This function works for our purposes because + // app cache media files are always created with encodeForFileName(createCanonicalUUIDString()). + +#if USE(CF) && PLATFORM(WIN) + RetainPtr cfPath(AdoptCF, path.createCFString()); + RetainPtr cfURL(AdoptCF, CFURLCreateWithFileSystemPath(0, cfPath.get(), kCFURLWindowsPathStyle, false)); + KURL url(cfURL.get()); +#else + KURL url; + + url.setProtocol("file"); + url.setPath(path); +#endif + return url; +} +#endif + +void HTMLMediaElement::loadResource(const KURL& initialURL, ContentType& contentType) +{ + ASSERT(isSafeToLoadURL(initialURL, Complain)); + + LOG(Media, "HTMLMediaElement::loadResource(%s, %s)", urlForLogging(initialURL).utf8().data(), contentType.raw().utf8().data()); + + Frame* frame = document()->frame(); + if (!frame) { + mediaLoadingFailed(MediaPlayer::FormatError); + return; + } + + KURL url = initialURL; + if (!frame->loader()->willLoadMediaElementURL(url)) { + mediaLoadingFailed(MediaPlayer::FormatError); + return; + } + +#if ENABLE(MEDIA_SOURCE) + // If this is a media source URL, make sure it is the one for this media element. + if (url.protocolIs(mediaSourceURLProtocol) && url != m_mediaSourceURL) { + mediaLoadingFailed(MediaPlayer::FormatError); + return; + } +#endif + + // The resource fetch algorithm + m_networkState = NETWORK_LOADING; + +#if !PLATFORM(CHROMIUM) + // If the url should be loaded from the application cache, pass the url of the cached file + // to the media engine. + ApplicationCacheHost* cacheHost = frame->loader()->documentLoader()->applicationCacheHost(); + ApplicationCacheResource* resource = 0; + if (cacheHost && cacheHost->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; + } + } +#endif + + // 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 !PLATFORM(CHROMIUM) + if (resource) { + url = createFileURLForApplicationCacheResource(resource->path()); + LOG(Media, "HTMLMediaElement::loadResource - will load from app cache -> %s", urlForLogging(url).utf8().data()); + } +#endif + + LOG(Media, "HTMLMediaElement::loadResource - m_currentSrc -> %s", urlForLogging(m_currentSrc).utf8().data()); + + if (m_sendProgressEvents) + startProgressEventTimer(); + + Settings* settings = document()->settings(); + bool privateMode = !settings || settings->privateBrowsingEnabled(); + 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_player->setPreload(m_preload); + m_player->setPreservesPitch(m_webkitPreservesPitch); + + if (fastHasAttribute(mutedAttr)) + m_muted = true; + updateVolume(); + + if (!m_player->load(url, contentType)) + 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(); + + if (renderer()) + renderer()->updateFromElement(); +} + +#if ENABLE(VIDEO_TRACK) +void HTMLMediaElement::updateActiveTextTrackCues(float movieTime) +{ + CueList previouslyActiveCues = m_currentlyActiveCues; + bool activeSetChanged = false; + + m_currentlyActiveCues = m_cueTree.allOverlaps(m_cueTree.createInterval(movieTime, movieTime)); + + // FIXME(72171): Events need to be sorted and filtered before dispatching. + + for (size_t i = 0; i < previouslyActiveCues.size(); ++i) { + if (!m_currentlyActiveCues.contains(previouslyActiveCues[i])) { + previouslyActiveCues[i].data()->setIsActive(false); + activeSetChanged = true; + } + } + for (size_t i = 0; i < m_currentlyActiveCues.size(); ++i) { + if (!previouslyActiveCues.contains(m_currentlyActiveCues[i])) { + m_currentlyActiveCues[i].data()->setIsActive(true); + activeSetChanged = true; + } + } + + // FIXME(72173): Pause the media element for cues going past their endTime + // during a monotonic time increase. + + if (activeSetChanged && hasMediaControls()) + mediaControls()->updateTextTrackDisplay(); +} + +bool HTMLMediaElement::textTracksAreReady() const +{ + // 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) + 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()); + } +} + +void HTMLMediaElement::textTrackModeChanged(TextTrack* track) +{ + if (track->trackType() == TextTrack::TrackElement) { + // 4.8.10.12.3 Sourcing out-of-band text tracks + // ... when a text track corresponding to a track element is created with text track + // mode set to disabled and subsequently changes its text track mode to hidden, showing, + // or showing by default for the first time, the user agent must immediately and synchronously + // run the following algorithm ... + + for (Node* node = firstChild(); node; node = node->nextSibling()) { + if (!node->hasTagName(trackTag)) + continue; + HTMLTrackElement* trackElement = static_cast(node); + if (trackElement->track() != track) + continue; + + // Mark this track as "configured" so configureTextTrack won't change the mode again. + trackElement->setHasBeenConfigured(true); + if (track->mode() != TextTrack::DISABLED && trackElement->readyState() == HTMLTrackElement::NONE) + trackElement->scheduleLoad(); + break; + } + } + + configureTextTrackDisplay(); +} + +void HTMLMediaElement::textTrackKindChanged(TextTrack*) +{ + // FIXME(62885): Implement. +} + +void HTMLMediaElement::textTrackAddCues(TextTrack*, const TextTrackCueList* cues) +{ + for (size_t i = 0; i < cues->length(); ++i) + textTrackAddCue(cues->item(i)->track(), cues->item(i)); +} + +void HTMLMediaElement::textTrackRemoveCues(TextTrack*, const TextTrackCueList* cues) +{ + for (size_t i = 0; i < cues->length(); ++i) + textTrackRemoveCue(cues->item(i)->track(), cues->item(i)); +} + +void HTMLMediaElement::textTrackAddCue(TextTrack*, PassRefPtr cue) +{ + m_cueTree.add(m_cueTree.createInterval(cue->startTime(), cue->endTime(), cue.get())); +} + +void HTMLMediaElement::textTrackRemoveCue(TextTrack*, PassRefPtr cue) +{ + m_cueTree.remove(m_cueTree.createInterval(cue->startTime(), cue->endTime(), cue.get())); +} + +#endif + +bool HTMLMediaElement::isSafeToLoadURL(const KURL& url, InvalidURLAction actionIfInvalid) +{ + if (!url.isValid()) { + LOG(Media, "HTMLMediaElement::isSafeToLoadURL(%s) -> FALSE because url is invalid", urlForLogging(url).utf8().data()); + return false; + } + + Frame* frame = document()->frame(); + if (!frame || !document()->securityOrigin()->canDisplay(url)) { + if (actionIfInvalid == Complain) + FrameLoader::reportLocalLoadFailed(frame, url.string()); + LOG(Media, "HTMLMediaElement::isSafeToLoadURL(%s) -> FALSE rejected by SecurityOrigin", urlForLogging(url).utf8().data()); + return false; + } + + if (!document()->contentSecurityPolicy()->allowMediaFromSource(url)) { + LOG(Media, "HTMLMediaElement::isSafeToLoadURL(%s) -> rejected by Content Security Policy", urlForLogging(url).utf8().data()); + return false; + } + + return true; +} + +void HTMLMediaElement::startProgressEventTimer() +{ + if (m_progressEventTimer.isActive()) + return; + + m_previousProgressTime = WTF::currentTime(); + m_previousProgress = 0; + // 350ms is not magic, it is in the spec! + m_progressEventTimer.startRepeating(0.350); +} + +void HTMLMediaElement::waitForSourceChange() +{ + LOG(Media, "HTMLMediaElement::waitForSourceChange"); + + 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(); + + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::noneSupported() +{ + LOG(Media, "HTMLMediaElement::noneSupported"); + + stopPeriodicTimers(); + m_loadState = WaitingForSource; + m_currentSourceNode = 0; + + // 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. + + // 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); + + // 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(); + + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::mediaEngineError(PassRefPtr err) +{ + LOG(Media, "HTMLMediaElement::mediaEngineError(%d)", static_cast(err->code())); + + // 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. + m_error = err; + + // 3 - Queue a task to fire a simple event named error at the media element. + scheduleEvent(eventNames().errorEvent); + +#if ENABLE(MEDIA_SOURCE) + if (m_sourceState != SOURCE_CLOSED) + setSourceState(SOURCE_CLOSED); +#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 = 0; +} + +void HTMLMediaElement::cancelPendingEventsAndCallbacks() +{ + LOG(Media, "HTMLMediaElement::cancelPendingEventsAndCallbacks"); + + m_pendingEvents.clear(); + + for (Node* node = firstChild(); node; node = node->nextSibling()) { + if (node->hasTagName(sourceTag)) + static_cast(node)->cancelPendingErrorEvent(); + } +} + +Document* HTMLMediaElement::mediaPlayerOwningDocument() +{ + Document* d = document(); + + if (!d) + d = ownerDocument(); + + return d; +} + +void HTMLMediaElement::mediaPlayerNetworkStateChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + setNetworkState(m_player->networkState()); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaLoadingFailed(MediaPlayer::NetworkState error) +{ + stopPeriodicTimers(); + + // If we failed while trying to load a element, the movie was never parsed, and there are more + // children, schedule the next one + if (m_readyState < HAVE_METADATA && m_loadState == LoadingFromSourceElement) { + + if (m_currentSourceNode) + m_currentSourceNode->scheduleErrorEvent(); + else + LOG(Media, "HTMLMediaElement::setNetworkState - error event not sent, was removed"); + + if (havePotentialSourceChild()) { + LOG(Media, "HTMLMediaElement::setNetworkState - scheduling next "); + scheduleNextSourceChild(); + } else { + LOG(Media, "HTMLMediaElement::setNetworkState - no more elements, waiting"); + waitForSourceChange(); + } + + return; + } + + if (error == MediaPlayer::NetworkError && m_readyState >= HAVE_METADATA) + mediaEngineError(MediaError::create(MediaError::MEDIA_ERR_NETWORK)); + else if (error == MediaPlayer::DecodeError) + mediaEngineError(MediaError::create(MediaError::MEDIA_ERR_DECODE)); + else if ((error == MediaPlayer::FormatError || error == MediaPlayer::NetworkError) && m_loadState == LoadingFromSrcAttr) + noneSupported(); + + updateDisplayState(); + if (hasMediaControls()) { + mediaControls()->reset(); + mediaControls()->reportedError(); + } +} + +void HTMLMediaElement::setNetworkState(MediaPlayer::NetworkState state) +{ + LOG(Media, "HTMLMediaElement::setNetworkState(%d) - current state is %d", static_cast(state), static_cast(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) { + m_progressEventTimer.stop(); + scheduleEvent(eventNames().suspendEvent); + setShouldDelayLoadEvent(false); + } + 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) { + m_progressEventTimer.stop(); + + // Schedule one last progress event so we guarantee that at least one is fired + // for files that load very quickly. + scheduleEvent(eventNames().progressEvent); + } + m_networkState = NETWORK_IDLE; + m_completelyLoaded = true; + } + + if (hasMediaControls()) + mediaControls()->updateStatusDisplay(); +} + +void HTMLMediaElement::mediaPlayerReadyStateChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + + setReadyState(m_player->readyState()); + + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::setReadyState(MediaPlayer::ReadyState state) +{ + LOG(Media, "HTMLMediaElement::setReadyState(%d) - current state is %d,", static_cast(state), static_cast(m_readyState)); + + // Set "wasPotentiallyPlaying" BEFORE updating m_readyState, potentiallyPlaying() uses it + bool wasPotentiallyPlaying = potentiallyPlaying(); + + ReadyState oldState = m_readyState; + m_readyState = static_cast(state); + +#if ENABLE(VIDEO_TRACK) + bool tracksAreReady = textTracksAreReady(); + + if (m_readyState == oldState && m_tracksAreReady == tracksAreReady) + return; + + m_tracksAreReady = tracksAreReady; +#else + if (m_readyState == oldState) + return; + bool tracksAreReady = true; +#endif + + 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_readyState >= HAVE_CURRENT_DATA) + finishSeek(); + } else { + if (wasPotentiallyPlaying && m_readyState < HAVE_FUTURE_DATA) { + // 4.8.10.8 + scheduleTimeupdateEvent(false); + scheduleEvent(eventNames().waitingEvent); + } + } + + if (m_readyState >= HAVE_METADATA && oldState < HAVE_METADATA && tracksAreReady) { + prepareMediaFragmentURI(); + scheduleEvent(eventNames().durationchangeEvent); + scheduleEvent(eventNames().loadedmetadataEvent); + if (hasMediaControls()) + mediaControls()->loadedMetadata(); + if (renderer()) + renderer()->updateFromElement(); + } + + bool shouldUpdateDisplayState = false; + + if (m_readyState >= HAVE_CURRENT_DATA && oldState < HAVE_CURRENT_DATA && !m_haveFiredLoadedData && tracksAreReady) { + 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) + scheduleEvent(eventNames().playingEvent); + 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) + scheduleEvent(eventNames().playingEvent); + + if (m_autoplaying && m_paused && autoplay()) { + m_paused = false; + invalidateCachedTime(); + scheduleEvent(eventNames().playEvent); + scheduleEvent(eventNames().playingEvent); + } + + shouldUpdateDisplayState = true; + } + + if (shouldUpdateDisplayState) { + updateDisplayState(); + if (hasMediaControls()) + mediaControls()->updateStatusDisplay(); + } + + updatePlayState(); + updateMediaController(); +} + +#if ENABLE(MEDIA_SOURCE) +void HTMLMediaElement::mediaPlayerSourceOpened() +{ + beginProcessingMediaPlayerCallback(); + + setSourceState(SOURCE_OPEN); + + endProcessingMediaPlayerCallback(); +} + +String HTMLMediaElement::mediaPlayerSourceURL() const +{ + return m_mediaSourceURL.string(); +} +#endif + +void HTMLMediaElement::progressEventTimerFired(Timer*) +{ + ASSERT(m_player); + if (m_networkState != NETWORK_LOADING) + return; + + unsigned progress = m_player->bytesLoaded(); + double time = WTF::currentTime(); + double timedelta = time - m_previousProgressTime; + + if (progress == m_previousProgress) { + if (timedelta > 3.0 && !m_sentStalledEvent) { + scheduleEvent(eventNames().stalledEvent); + m_sentStalledEvent = true; + setShouldDelayLoadEvent(false); + } + } else { + scheduleEvent(eventNames().progressEvent); + m_previousProgress = progress; + m_previousProgressTime = time; + m_sentStalledEvent = false; + if (renderer()) + renderer()->updateFromElement(); + } +} + +void HTMLMediaElement::rewind(float timeDelta) +{ + LOG(Media, "HTMLMediaElement::rewind(%f)", timeDelta); + + ExceptionCode e; + setCurrentTime(max(currentTime() - timeDelta, minTimeSeekable()), e); +} + +void HTMLMediaElement::returnToRealtime() +{ + LOG(Media, "HTMLMediaElement::returnToRealtime"); + ExceptionCode e; + setCurrentTime(maxTimeSeekable(), e); +} + +void HTMLMediaElement::addPlayedRange(float start, float end) +{ + LOG(Media, "HTMLMediaElement::addPlayedRange(%f, %f)", start, end); + if (!m_playedTimeRanges) + m_playedTimeRanges = TimeRanges::create(); + m_playedTimeRanges->add(start, end); +} + +bool HTMLMediaElement::supportsSave() const +{ + return m_player ? m_player->supportsSave() : false; +} + +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; + m_player->prepareToPlay(); +} + +void HTMLMediaElement::seek(float time, ExceptionCode& ec) +{ + LOG(Media, "HTMLMediaElement::seek(%f)", time); + + // 4.8.9.9 Seeking + + // 1 - If the media element's readyState is HAVE_NOTHING, then raise an INVALID_STATE_ERR exception. + if (m_readyState == HAVE_NOTHING || !m_player) { + ec = INVALID_STATE_ERR; + 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(); + float now = currentTime(); + + // 2 - 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. + // Nothing specific to be done here. + + // 3 - 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; + + // 5 - 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 = min(time, duration()); + + // 6 - If the new playback position is less than the earliest possible position, let it be that position instead. + float earliestTime = m_player->startTime(); + time = 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 + float mediaTime = m_player->mediaTimeForTimeValue(time); + if (time != mediaTime) + LOG(Media, "HTMLMediaElement::seek(%f) - media timeline equivalent is %f", time, mediaTime); +#endif + time = m_player->mediaTimeForTimeValue(time); + + // 7 - 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 seekableRanges = seekable(); + + // 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 we are in poster mode because a seek should always + // cancel poster display. + bool noSeekRequired = !seekableRanges->length() || (time == now && displayMode() != Poster); + +#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_sourceState != SOURCE_CLOSED) + noSeekRequired = false; +#endif + + if (noSeekRequired) { + if (time == now) { + scheduleEvent(eventNames().seekingEvent); + scheduleTimeupdateEvent(false); + scheduleEvent(eventNames().seekedEvent); + } + m_seeking = false; + return; + } + time = seekableRanges->nearest(time); + + if (m_playing) { + if (m_lastSeekTime < now) + addPlayedRange(m_lastSeekTime, now); + } + m_lastSeekTime = time; + m_sentEndEvent = false; + +#if ENABLE(MEDIA_SOURCE) + if (m_sourceState == SOURCE_ENDED) + setSourceState(SOURCE_OPEN); +#endif + + // 8 - Set the current playback position to the given new playback position + m_player->seek(time); + + // 9 - Queue a task to fire a simple event named seeking at the element. + scheduleEvent(eventNames().seekingEvent); + + // 10 - Queue a task to fire a simple event named timeupdate at the element. + scheduleTimeupdateEvent(false); + + // 11-15 are handled, if necessary, when the engine signals a readystate change. +} + +void HTMLMediaElement::finishSeek() +{ + LOG(Media, "HTMLMediaElement::finishSeek"); + + // 4.8.10.9 Seeking step 14 + m_seeking = false; + + // 4.8.10.9 Seeking step 15 + scheduleEvent(eventNames().seekedEvent); + + setDisplayMode(Video); +} + +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 +{ + m_cachedTime = m_player->currentTime(); + m_cachedTimeWallClockUpdateTime = WTF::currentTime(); +} + +void HTMLMediaElement::invalidateCachedTime() +{ + LOG(Media, "HTMLMediaElement::invalidateCachedTime"); + + // 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_minimumWallClockTimeToCacheMediaTime = WTF::currentTime() + minimumTimePlayingBeforeCacheSnapshot; + m_cachedTime = invalidMediaTime; +} + +// playback state +float HTMLMediaElement::currentTime() const +{ +#if LOG_CACHED_TIME_WARNINGS + static const double minCachedDeltaForWarning = 0.01; +#endif + + if (!m_player) + return 0; + + if (m_seeking) { + LOG(Media, "HTMLMediaElement::currentTime - seeking, returning %f", m_lastSeekTime); + return m_lastSeekTime; + } + + if (m_cachedTime != invalidMediaTime && m_paused) { +#if LOG_CACHED_TIME_WARNINGS + float delta = m_cachedTime - m_player->currentTime(); + if (delta > minCachedDeltaForWarning) + LOG(Media, "HTMLMediaElement::currentTime - WARNING, cached time is %f seconds off of media time when paused", delta); +#endif + return m_cachedTime; + } + + // Is it too soon use a cached time? + double now = WTF::currentTime(); + double maximumDurationToCacheMediaTime = m_player->maximumDurationToCacheMediaTime(); + + if (maximumDurationToCacheMediaTime && m_cachedTime != invalidMediaTime && !m_paused && now > m_minimumWallClockTimeToCacheMediaTime) { + double wallClockDelta = now - m_cachedTimeWallClockUpdateTime; + + // Not too soon, use the cached time only if it hasn't expired. + if (wallClockDelta < maximumDurationToCacheMediaTime) { + float adjustedCacheTime = static_cast(m_cachedTime + (m_playbackRate * wallClockDelta)); + +#if LOG_CACHED_TIME_WARNINGS + float delta = adjustedCacheTime - m_player->currentTime(); + if (delta > minCachedDeltaForWarning) + LOG(Media, "HTMLMediaElement::currentTime - WARNING, cached time is %f seconds off of media time when playing", delta); +#endif + return adjustedCacheTime; + } + } + +#if LOG_CACHED_TIME_WARNINGS + if (maximumDurationToCacheMediaTime && now > m_minimumWallClockTimeToCacheMediaTime && m_cachedTime != invalidMediaTime) { + double wallClockDelta = now - m_cachedTimeWallClockUpdateTime; + float delta = m_cachedTime + (m_playbackRate * wallClockDelta) - m_player->currentTime(); + LOG(Media, "HTMLMediaElement::currentTime - cached time was %f seconds off of media time when it expired", delta); + } +#endif + + refreshCachedTime(); + + return m_cachedTime; +} + +void HTMLMediaElement::setCurrentTime(float time, ExceptionCode& ec) +{ + if (m_mediaController) { + ec = INVALID_STATE_ERR; + return; + } + seek(time, ec); +} + +float HTMLMediaElement::startTime() const +{ + if (!m_player) + return 0; + return m_player->startTime(); +} + +double HTMLMediaElement::initialTime() const +{ + if (m_fragmentStartTime != invalidMediaTime) + return m_fragmentStartTime; + + if (!m_player) + return 0; + + return m_player->initialTime(); +} + +float HTMLMediaElement::duration() const +{ + if (m_player && m_readyState >= HAVE_METADATA) + return m_player->duration(); + + return numeric_limits::quiet_NaN(); +} + +bool HTMLMediaElement::paused() const +{ + return m_paused; +} + +float HTMLMediaElement::defaultPlaybackRate() const +{ + return m_defaultPlaybackRate; +} + +void HTMLMediaElement::setDefaultPlaybackRate(float rate) +{ + if (m_defaultPlaybackRate != rate) { + m_defaultPlaybackRate = rate; + scheduleEvent(eventNames().ratechangeEvent); + } +} + +float HTMLMediaElement::playbackRate() const +{ + return m_playbackRate; +} + +void HTMLMediaElement::setPlaybackRate(float rate) +{ + LOG(Media, "HTMLMediaElement::setPlaybackRate(%f)", rate); + + if (m_playbackRate != rate) { + m_playbackRate = rate; + invalidateCachedTime(); + scheduleEvent(eventNames().ratechangeEvent); + } + + if (m_player && potentiallyPlaying() && m_player->rate() != rate && !m_mediaController) + m_player->setRate(rate); +} + +void HTMLMediaElement::updatePlaybackRate() +{ + float effectiveRate = m_mediaController ? m_mediaController->playbackRate() : m_playbackRate; + if (m_player && potentiallyPlaying() && m_player->rate() != effectiveRate && !m_mediaController) + m_player->setRate(effectiveRate); +} + +bool HTMLMediaElement::webkitPreservesPitch() const +{ + return m_webkitPreservesPitch; +} + +void HTMLMediaElement::setWebkitPreservesPitch(bool preservesPitch) +{ + LOG(Media, "HTMLMediaElement::setWebkitPreservesPitch(%s)", boolString(preservesPitch)); + + m_webkitPreservesPitch = preservesPitch; + + if (!m_player) + return; + + m_player->setPreservesPitch(preservesPitch); +} + +bool HTMLMediaElement::ended() const +{ + // 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() && m_playbackRate > 0; +} + +bool HTMLMediaElement::autoplay() const +{ + return fastHasAttribute(autoplayAttr); +} + +void HTMLMediaElement::setAutoplay(bool b) +{ + LOG(Media, "HTMLMediaElement::setAutoplay(%s)", boolString(b)); + setBooleanAttribute(autoplayAttr, b); +} + +String HTMLMediaElement::preload() const +{ + switch (m_preload) { + case MediaPlayer::None: + return "none"; + break; + case MediaPlayer::MetaData: + return "metadata"; + break; + case MediaPlayer::Auto: + return "auto"; + break; + } + + ASSERT_NOT_REACHED(); + return String(); +} + +void HTMLMediaElement::setPreload(const String& preload) +{ + LOG(Media, "HTMLMediaElement::setPreload(%s)", preload.utf8().data()); + setAttribute(preloadAttr, preload); +} + +void HTMLMediaElement::play() +{ + LOG(Media, "HTMLMediaElement::play()"); + + if (userGestureRequiredForRateChange() && !ScriptController::processingUserGesture()) + return; + + Settings* settings = document()->settings(); + if (settings && settings->needsSiteSpecificQuirks() && m_dispatchingCanPlayEvent && !m_loadInitiatedByUserGesture) { + // It should be impossible to be processing the canplay event while handling a user gesture + // since it is dispatched asynchronously. + ASSERT(!ScriptController::processingUserGesture()); + String host = document()->baseURL().host(); + if (host.endsWith(".npr.org", false) || equalIgnoringCase(host, "npr.org")) + return; + } + + playInternal(); +} + +void HTMLMediaElement::playInternal() +{ + LOG(Media, "HTMLMediaElement::playInternal"); + + // 4.8.10.9. Playing the media resource + if (!m_player || m_networkState == NETWORK_EMPTY) + scheduleLoad(MediaResource); + + if (endedPlayback()) { + ExceptionCode unused; + seek(0, unused); + } + + if (m_mediaController) + m_mediaController->bringElementUpToSpeed(this); + + if (m_paused) { + m_paused = false; + invalidateCachedTime(); + scheduleEvent(eventNames().playEvent); + + if (m_readyState <= HAVE_CURRENT_DATA) + scheduleEvent(eventNames().waitingEvent); + else if (m_readyState >= HAVE_FUTURE_DATA) + scheduleEvent(eventNames().playingEvent); + } + m_autoplaying = false; + + updatePlayState(); + updateMediaController(); +} + +void HTMLMediaElement::pause() +{ + LOG(Media, "HTMLMediaElement::pause()"); + + if (userGestureRequiredForRateChange() && !ScriptController::processingUserGesture()) + return; + + pauseInternal(); +} + + +void HTMLMediaElement::pauseInternal() +{ + LOG(Media, "HTMLMediaElement::pauseInternal"); + + // 4.8.10.9. Playing the media resource + if (!m_player || m_networkState == NETWORK_EMPTY) + scheduleLoad(MediaResource); + + m_autoplaying = false; + + if (!m_paused) { + m_paused = true; + scheduleTimeupdateEvent(false); + scheduleEvent(eventNames().pauseEvent); + } + + updatePlayState(); +} + +#if ENABLE(MEDIA_SOURCE) +void HTMLMediaElement::webkitSourceAppend(PassRefPtr data, ExceptionCode& ec) +{ + if (!m_player || m_currentSrc != m_mediaSourceURL || m_sourceState != SOURCE_OPEN) { + ec = INVALID_STATE_ERR; + return; + } + + if (!data.get() || !m_player->sourceAppend(data->data(), data->length())) { + ec = SYNTAX_ERR; + return; + } +} + +void HTMLMediaElement::webkitSourceEndOfStream(unsigned short status, ExceptionCode& ec) +{ + if (!m_player || m_currentSrc != m_mediaSourceURL || m_sourceState != SOURCE_OPEN) { + ec = INVALID_STATE_ERR; + return; + } + + MediaPlayer::EndOfStreamStatus eosStatus = MediaPlayer::EosNoError; + + switch (status) { + case EOS_NO_ERROR: + eosStatus = MediaPlayer::EosNoError; + break; + case EOS_NETWORK_ERR: + eosStatus = MediaPlayer::EosNetworkError; + break; + case EOS_DECODE_ERR: + eosStatus = MediaPlayer::EosDecodeError; + break; + default: + ec = SYNTAX_ERR; + return; + } + + setSourceState(SOURCE_ENDED); + m_player->sourceEndOfStream(eosStatus); +} + +HTMLMediaElement::SourceState HTMLMediaElement::webkitSourceState() const +{ + return m_sourceState; +} + +void HTMLMediaElement::setSourceState(SourceState state) +{ + SourceState oldState = m_sourceState; + m_sourceState = static_cast(state); + + if (m_sourceState == oldState) + return; + + if (m_sourceState == SOURCE_CLOSED) { + scheduleEvent(eventNames().webkitsourcecloseEvent); + return; + } + + if (oldState == SOURCE_OPEN && m_sourceState == SOURCE_ENDED) { + scheduleEvent(eventNames().webkitsourceendedEvent); + return; + } + + if (m_sourceState == SOURCE_OPEN) { + scheduleEvent(eventNames().webkitsourceopenEvent); + return; + } +} +#endif + +bool HTMLMediaElement::loop() const +{ + return fastHasAttribute(loopAttr); +} + +void HTMLMediaElement::setLoop(bool b) +{ + LOG(Media, "HTMLMediaElement::setLoop(%s)", 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; + + // always show controls for video when fullscreen playback is required. + if (isVideo() && document()->page() && document()->page()->chrome()->requiresFullscreenForVideoPlayback()) + return true; + + // Always show controls when in full screen mode. + if (isFullscreen()) + return true; + + return fastHasAttribute(controlsAttr); +} + +void HTMLMediaElement::setControls(bool b) +{ + LOG(Media, "HTMLMediaElement::setControls(%s)", boolString(b)); + setBooleanAttribute(controlsAttr, b); +} + +float HTMLMediaElement::volume() const +{ + return m_volume; +} + +void HTMLMediaElement::setVolume(float vol, ExceptionCode& ec) +{ + LOG(Media, "HTMLMediaElement::setVolume(%f)", vol); + + if (vol < 0.0f || vol > 1.0f) { + ec = INDEX_SIZE_ERR; + return; + } + + if (m_volume != vol) { + m_volume = vol; + updateVolume(); + scheduleEvent(eventNames().volumechangeEvent); + } +} + +bool HTMLMediaElement::muted() const +{ + return m_muted; +} + +void HTMLMediaElement::setMuted(bool muted) +{ + LOG(Media, "HTMLMediaElement::setMuted(%s)", boolString(muted)); + + if (m_muted != muted) { + m_muted = muted; + // Avoid recursion when the player reports volume changes. + if (!processingMediaPlayerCallback()) { + if (m_player) { + m_player->setMuted(m_muted); + if (hasMediaControls()) + mediaControls()->changedMute(); + } + } + scheduleEvent(eventNames().volumechangeEvent); + } +} + +void HTMLMediaElement::togglePlayState() +{ + LOG(Media, "HTMLMediaElement::togglePlayState - canPlay() is %s", 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 - paused() is %s", 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); + } + } +} + +void HTMLMediaElement::endScrubbing() +{ + LOG(Media, "HTMLMediaElement::endScrubbing - m_pausedInternal is %s", boolString(m_pausedInternal)); + + if (m_pausedInternal) + setPausedInternal(false); +} + +// The spec says to fire periodic timeupdate events (those sent while playing) every +// "15 to 250ms", we choose the slowest frequency +static const double maxTimeupdateEventFrequency = 0.25; + +static const double timeWithoutMouseMovementBeforeHidingControls = 3; + +void HTMLMediaElement::startPlaybackProgressTimer() +{ + if (m_playbackProgressTimer.isActive()) + return; + + m_previousProgressTime = WTF::currentTime(); + m_previousProgress = 0; + m_playbackProgressTimer.startRepeating(maxTimeupdateEventFrequency); +} + +void HTMLMediaElement::playbackProgressTimerFired(Timer*) +{ + ASSERT(m_player); + + if (m_fragmentEndTime != invalidMediaTime && currentTime() >= m_fragmentEndTime && m_playbackRate > 0) { + m_fragmentEndTime = invalidMediaTime; + if (!m_mediaController && !m_paused) { + // changes paused to true and fires a simple event named pause at the media element. + pauseInternal(); + } + } + + scheduleTimeupdateEvent(true); + + if (!m_playbackRate) + return; + + if (!m_paused && hasMediaControls()) + mediaControls()->playbackProgressed(); + +#if ENABLE(VIDEO_TRACK) + updateActiveTextTrackCues(currentTime()); +#endif +} + +void HTMLMediaElement::scheduleTimeupdateEvent(bool periodicEvent) +{ + double now = WTF::currentTime(); + double timedelta = now - m_lastTimeUpdateEventWallTime; + + // 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 + float movieTime = currentTime(); + if (movieTime != m_lastTimeUpdateEventMovieTime) { + scheduleEvent(eventNames().timeupdateEvent); + m_lastTimeUpdateEventWallTime = now; + m_lastTimeUpdateEventMovieTime = movieTime; + } +} + +bool HTMLMediaElement::canPlay() const +{ + return paused() || ended() || m_readyState < HAVE_METADATA; +} + +float HTMLMediaElement::percentLoaded() const +{ + if (!m_player) + return 0; + float duration = m_player->duration(); + + if (!duration || isinf(duration)) + return 0; + + float buffered = 0; + RefPtr timeRanges = m_player->buffered(); + for (unsigned i = 0; i < timeRanges->length(); ++i) { + ExceptionCode ignoredException; + float start = timeRanges->start(i, ignoredException); + float end = timeRanges->end(i, ignoredException); + buffered += end - start; + } + return buffered / duration; +} + +#if ENABLE(VIDEO_TRACK) +PassRefPtr HTMLMediaElement::addTrack(const String& kind, const String& label, const String& language, ExceptionCode& ec) +{ + if (!RuntimeEnabledFeatures::webkitVideoTrackEnabled()) + return 0; + + // 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)) { + ec = SYNTAX_ERR; + return 0; + } + + // 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. + RefPtr textTrack = TextTrack::create(ActiveDOMObject::scriptExecutionContext(), this, kind, label, language); + + // 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, its text track readiness state to the text track + // loaded state, its text track mode to the text track hidden mode, and its text track list of cues to an empty list. + + // 6. Add the new text track to the media element's list of text tracks. + textTracks()->append(textTrack); + + return textTrack.release(); +} + +TextTrackList* HTMLMediaElement::textTracks() +{ + if (!RuntimeEnabledFeatures::webkitVideoTrackEnabled()) + return 0; + + if (!m_textTracks) + m_textTracks = TextTrackList::create(this, ActiveDOMObject::scriptExecutionContext()); + + return m_textTracks.get(); +} + +HTMLTrackElement* HTMLMediaElement::showingTrackWithSameKind(HTMLTrackElement* trackElement) const +{ + HTMLTrackElement* showingTrack = 0; + + for (Node* node = firstChild(); node; node = node->nextSibling()) { + if (trackElement == node) + continue; + if (!node->hasTagName(trackTag)) + continue; + + showingTrack = static_cast(node); + if (showingTrack->kind() == trackElement->kind() && showingTrack->track()->mode() == TextTrack::SHOWING) + return showingTrack; + } + + return 0; +} + +void HTMLMediaElement::trackWasAdded(HTMLTrackElement* trackElement) +{ + ASSERT(trackElement->hasTagName(trackTag)); + + if (!RuntimeEnabledFeatures::webkitVideoTrackEnabled()) + return; + + // 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] + RefPtr textTrack = trackElement->track(); + if (!textTrack) + return; + + textTracks()->append(textTrack); + + // 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) + scheduleLoad(TextTrackResource); +} + +void HTMLMediaElement::trackWillBeRemoved(HTMLTrackElement* trackElement) +{ + ASSERT(trackElement->hasTagName(trackTag)); + + if (!RuntimeEnabledFeatures::webkitVideoTrackEnabled()) + return; + +#if !LOG_DISABLED + if (trackElement->hasTagName(trackTag)) { + KURL url = trackElement->getNonEmptyURLAttribute(srcAttr); + LOG(Media, "HTMLMediaElement::trackWillBeRemoved - 'src' is %s", urlForLogging(url).utf8().data()); + } +#endif + + trackElement->setHasBeenConfigured(false); + + RefPtr textTrack = trackElement->track(); + if (!textTrack) + 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. + m_textTracks->remove(textTrack.get()); + size_t index = m_textTracksWhenResourceSelectionBegan.find(textTrack.get()); + if (index != notFound) + m_textTracksWhenResourceSelectionBegan.remove(index); +} + +bool HTMLMediaElement::userIsInterestedInThisLanguage(const String&) const +{ + // FIXME: check the user's language preference - bugs.webkit.org/show_bug.cgi?id=74121 + return true; +} + +bool HTMLMediaElement::userIsInterestedInThisTrack(HTMLTrackElement* trackElement) const +{ + RefPtr textTrack = trackElement->track(); + if (!textTrack) + return false; + + String kind = textTrack->kind(); + if (!TextTrack::isValidKindKeyword(kind)) + return false; + + // If ... the user has indicated an interest in having a track with this text track kind, text track language, ... + Settings* settings = document()->settings(); + if (!settings) + return false; + + if (kind == TextTrack::subtitlesKeyword() || kind == TextTrack::captionsKeyword()) { + if (kind == TextTrack::subtitlesKeyword() && !settings->shouldDisplaySubtitles()) + return false; + if (kind == TextTrack::captionsKeyword() && !settings->shouldDisplayCaptions()) + return false; + return userIsInterestedInThisLanguage(trackElement->srclang()); + } + + if (kind == TextTrack::descriptionsKeyword()) { + if (!settings->shouldDisplayTextDescriptions()) + return false; + return userIsInterestedInThisLanguage(trackElement->srclang()); + } + + return false; +} + +void HTMLMediaElement::configureTextTrack(HTMLTrackElement* trackElement) +{ +#if !LOG_DISABLED + if (trackElement->hasTagName(trackTag)) { + KURL url = trackElement->getNonEmptyURLAttribute(srcAttr); + LOG(Media, "HTMLMediaElement::configureTextTrack - 'src' is %s", urlForLogging(url).utf8().data()); + } +#endif + + // 4.8.10.12.3 Sourcing out-of-band text tracks + + // When a text track corresponding to a track element is added to a media element's list of text tracks, + // the user agent must set the text track mode appropriately, as determined by the following conditions: + RefPtr textTrack = trackElement->track(); + if (!textTrack) + return; + + TextTrack::Mode mode = TextTrack::HIDDEN; + HTMLTrackElement* trackElementCurrentlyShowing = showingTrackWithSameKind(trackElement); + String kind = textTrack->kind(); + bool hideDefaultTrack = false; + + if (userIsInterestedInThisTrack(trackElement)) { + if (kind == TextTrack::subtitlesKeyword() || kind == TextTrack::captionsKeyword()) { + // * If the text track kind is subtitles or captions 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 + hideDefaultTrack = trackElementCurrentlyShowing && trackElementCurrentlyShowing->track()->showingByDefault(); + if (!trackElementCurrentlyShowing || hideDefaultTrack) { + // Let the text track mode be showing. + // If there is a text track in the media element's list of text tracks whose text track mode is showing + // by default, the user agent must furthermore change that text track's text track mode to hidden. + mode = TextTrack::SHOWING; + } + } else if (kind == TextTrack::descriptionsKeyword()) { + // * If the text track kind is descriptions and the user has indicated an interest in having text + // descriptions with this 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 descriptions whose text + // track mode is showing + hideDefaultTrack = trackElementCurrentlyShowing && trackElementCurrentlyShowing->track()->showingByDefault(); + if (!trackElementCurrentlyShowing || hideDefaultTrack) { + // Let the text track mode be showing. + // If there is a text track in the media element's list of text tracks whose text track mode is showing + // by default, the user agent must furthermore change that text track's text track mode to hidden. + mode = TextTrack::SHOWING; + } + } else if (kind == TextTrack::chaptersKeyword()) { + // * 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 (!trackElementCurrentlyShowing) + mode = TextTrack::SHOWING; + } + } else if (!trackElementCurrentlyShowing && trackElement->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. + mode = TextTrack::SHOWING; + textTrack->setShowingByDefault(false); + } else { + // Otherwise + // Let the text track mode be disabled. + mode = TextTrack::DISABLED; + } + + ExceptionCode unusedException; + if (hideDefaultTrack) { + trackElementCurrentlyShowing->track()->setMode(TextTrack::HIDDEN, unusedException); + trackElementCurrentlyShowing->track()->setShowingByDefault(false); + } + + textTrack->setMode(mode, unusedException); +} + +void HTMLMediaElement::configureTextTracks() +{ + for (Node* node = firstChild(); node; node = node->nextSibling()) { + if (!node->hasTagName(trackTag)) + continue; + HTMLTrackElement* trackElement = static_cast(node); + + // Only 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 track element is added later. + if (!trackElement->hasBeenConfigured()) + configureTextTrack(trackElement); + } +} + +#endif + +bool HTMLMediaElement::havePotentialSourceChild() +{ + // Stash the current node and next nodes so we can restore them after checking + // to see there is another potential. + HTMLSourceElement* currentSourceNode = m_currentSourceNode; + Node* nextNode = m_nextChildNodeToConsider; + + KURL nextURL = selectNextSourceChild(0, DoNothing); + + m_currentSourceNode = currentSourceNode; + m_nextChildNodeToConsider = nextNode; + + return nextURL.isValid(); +} + +KURL HTMLMediaElement::selectNextSourceChild(ContentType *contentType, InvalidURLAction actionIfInvalid) +{ +#if !LOG_DISABLED + // Don't log if this was just called to find out if there are any valid elements. + bool shouldLog = actionIfInvalid != DoNothing; + if (shouldLog) + LOG(Media, "HTMLMediaElement::selectNextSourceChild"); +#endif + + if (m_nextChildNodeToConsider == sourceChildEndOfListValue()) { +#if !LOG_DISABLED + if (shouldLog) + LOG(Media, "HTMLMediaElement::selectNextSourceChild -> 0x0000, \"\""); +#endif + return KURL(); + } + + KURL mediaURL; + Node* node; + HTMLSourceElement* source = 0; + bool lookingForStartNode = m_nextChildNodeToConsider; + bool canUse = false; + + for (node = firstChild(); !canUse && node; node = node->nextSibling()) { + if (lookingForStartNode && m_nextChildNodeToConsider != node) + continue; + lookingForStartNode = false; + + if (!node->hasTagName(sourceTag)) + continue; + + source = static_cast(node); + + // 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 + mediaURL = source->getNonEmptyURLAttribute(srcAttr); +#if !LOG_DISABLED + if (shouldLog) + LOG(Media, "HTMLMediaElement::selectNextSourceChild - 'src' is %s", urlForLogging(mediaURL).utf8().data()); +#endif + if (mediaURL.isEmpty()) + goto check_again; + + if (source->fastHasAttribute(mediaAttr)) { + MediaQueryEvaluator screenEval("screen", document()->frame(), renderer() ? renderer()->style() : 0); + RefPtr media = MediaList::createAllowingDescriptionSyntax(source->media()); +#if !LOG_DISABLED + if (shouldLog) + LOG(Media, "HTMLMediaElement::selectNextSourceChild - 'media' is %s", source->media().utf8().data()); +#endif + if (!screenEval.eval(media.get())) + goto check_again; + } + + if (source->fastHasAttribute(typeAttr)) { +#if !LOG_DISABLED + if (shouldLog) + LOG(Media, "HTMLMediaElement::selectNextSourceChild - 'type' is %s", source->type().utf8().data()); +#endif + if (!MediaPlayer::supportsType(ContentType(source->type()))) + goto check_again; + } + + // Is it safe to load this url? + if (!isSafeToLoadURL(mediaURL, actionIfInvalid) || !dispatchBeforeLoadEvent(mediaURL.string())) + goto check_again; + + // Making it this far means the looks reasonable. + canUse = true; + +check_again: + if (!canUse && actionIfInvalid == Complain) + source->scheduleErrorEvent(); + } + + if (canUse) { + if (contentType) + *contentType = ContentType(source->type()); + m_currentSourceNode = source; + m_nextChildNodeToConsider = source->nextSibling(); + if (!m_nextChildNodeToConsider) + m_nextChildNodeToConsider = sourceChildEndOfListValue(); + } else { + m_currentSourceNode = 0; + m_nextChildNodeToConsider = sourceChildEndOfListValue(); + } + +#if !LOG_DISABLED + if (shouldLog) + LOG(Media, "HTMLMediaElement::selectNextSourceChild -> %p, %s", m_currentSourceNode, canUse ? urlForLogging(mediaURL).utf8().data() : ""); +#endif + return canUse ? mediaURL : KURL(); +} + +void HTMLMediaElement::sourceWasAdded(HTMLSourceElement* source) +{ + LOG(Media, "HTMLMediaElement::sourceWasAdded(%p)", source); + +#if !LOG_DISABLED + if (source->hasTagName(sourceTag)) { + KURL url = source->getNonEmptyURLAttribute(srcAttr); + LOG(Media, "HTMLMediaElement::sourceWasAdded - 'src' is %s", urlForLogging(url).utf8().data()); + } +#endif + + // We should only consider a element when there is not src attribute at all. + if (fastHasAttribute(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 (networkState() == HTMLMediaElement::NETWORK_EMPTY) { + scheduleLoad(MediaResource); + return; + } + + if (m_currentSourceNode && source == m_currentSourceNode->nextSibling()) { + LOG(Media, "HTMLMediaElement::sourceWasAdded - inserted immediately after current source"); + m_nextChildNodeToConsider = source; + return; + } + + if (m_nextChildNodeToConsider != sourceChildEndOfListValue()) + return; + + // 4.8.9.5, resource selection algorithm, source elements section: + // 20 - Wait until the node after pointer is a node other than the end of the list. (This step might wait forever.) + // 21 - Asynchronously await a stable state... + // 22 - 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); + + // 23 - Set the networkState back to NETWORK_LOADING. + m_networkState = NETWORK_LOADING; + + // 24 - Jump back to the find next candidate step above. + m_nextChildNodeToConsider = source; + scheduleNextSourceChild(); +} + +void HTMLMediaElement::sourceWillBeRemoved(HTMLSourceElement* source) +{ + LOG(Media, "HTMLMediaElement::sourceWillBeRemoved(%p)", source); + +#if !LOG_DISABLED + if (source->hasTagName(sourceTag)) { + KURL url = source->getNonEmptyURLAttribute(srcAttr); + LOG(Media, "HTMLMediaElement::sourceWillBeRemoved - 'src' is %s", urlForLogging(url).utf8().data()); + } +#endif + + if (source != m_currentSourceNode && source != m_nextChildNodeToConsider) + return; + + if (source == m_nextChildNodeToConsider) { + m_nextChildNodeToConsider = m_nextChildNodeToConsider->nextSibling(); + if (!m_nextChildNodeToConsider) + m_nextChildNodeToConsider = sourceChildEndOfListValue(); + LOG(Media, "HTMLMediaElement::sourceRemoved - m_nextChildNodeToConsider set to %p", m_nextChildNodeToConsider); + } 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 = 0; + LOG(Media, "HTMLMediaElement::sourceRemoved - m_currentSourceNode set to 0"); + } +} + +void HTMLMediaElement::mediaPlayerTimeChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerTimeChanged"); + + beginProcessingMediaPlayerCallback(); + + invalidateCachedTime(); + + // 4.8.10.9 step 14 & 15. Needed if no ReadyState change is associated with the seek. + if (m_seeking && m_readyState >= HAVE_CURRENT_DATA) + 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. + scheduleTimeupdateEvent(false); + + float now = currentTime(); + float dur = duration(); + + // When the current playback position reaches the end of the media resource when the direction of + // playback is forwards, then the user agent must follow these steps: + if (!isnan(dur) && dur && now >= dur && m_playbackRate > 0) { + // If the media element has a loop attribute specified and does not have a current media controller, + if (loop() && !m_mediaController) { + ExceptionCode ignoredException; + m_sentEndEvent = false; + // then seek to the earliest possible position of the media resource and abort these steps. + seek(startTime(), ignoredException); + } else { + // If the media element does not have a current media controller, and the media element + // has still ended playback, and the direction of playback is still forwards, 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); + } + // Queue a task to fire a simple event named ended at the media element. + if (!m_sentEndEvent) { + m_sentEndEvent = true; + scheduleEvent(eventNames().endedEvent); + } + // 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; + + updatePlayState(); +#if ENABLE(VIDEO_TRACK) + updateActiveTextTrackCues(now); +#endif + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerVolumeChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerVolumeChanged"); + + beginProcessingMediaPlayerCallback(); + if (m_player) { + float vol = m_player->volume(); + if (vol != m_volume) { + m_volume = vol; + updateVolume(); + scheduleEvent(eventNames().volumechangeEvent); + } + } + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerMuteChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerMuteChanged"); + + beginProcessingMediaPlayerCallback(); + if (m_player) + setMuted(m_player->muted()); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerDurationChanged(MediaPlayer* player) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerDurationChanged"); + + beginProcessingMediaPlayerCallback(); + scheduleEvent(eventNames().durationchangeEvent); + mediaPlayerCharacteristicChanged(player); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerRateChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerRateChanged"); + + 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_playbackRate = m_player->rate(); + if (m_playing) + invalidateCachedTime(); + +#if PLATFORM(MAC) + if (m_player->paused() && m_sleepDisabler) + m_sleepDisabler = nullptr; + else if (!m_player->paused() && !m_sleepDisabler) + m_sleepDisabler = DisplaySleepDisabler::create("com.apple.WebCore: HTMLMediaElement playback"); +#endif + + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerPlaybackStateChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerPlaybackStateChanged"); + + if (!m_player || m_pausedInternal) + return; + + beginProcessingMediaPlayerCallback(); + if (m_player->paused()) + pauseInternal(); + else + playInternal(); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerSawUnsupportedTracks(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerSawUnsupportedTracks"); + + // 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 (ownerDocument()->isMediaDocument()) { + MediaDocument* mediaDocument = static_cast(ownerDocument()); + mediaDocument->mediaElementSawUnsupportedTracks(); + } +} + +// MediaPlayerPresentation methods +void HTMLMediaElement::mediaPlayerRepaint(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + updateDisplayState(); + if (renderer()) + renderer()->repaint(); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerSizeChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerSizeChanged"); + + beginProcessingMediaPlayerCallback(); + if (renderer()) + renderer()->updateFromElement(); + endProcessingMediaPlayerCallback(); +} + +#if USE(ACCELERATED_COMPOSITING) +bool HTMLMediaElement::mediaPlayerRenderingCanBeAccelerated(MediaPlayer*) +{ + if (renderer() && renderer()->isVideo()) { + ASSERT(renderer()->view()); + return renderer()->view()->compositor()->canAccelerateVideoRendering(toRenderVideo(renderer())); + } + return false; +} + +void HTMLMediaElement::mediaPlayerRenderingModeChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerRenderingModeChanged"); + + // Kick off a fake recalcStyle that will update the compositing tree. + setNeedsStyleRecalc(SyntheticStyleChange); +} +#endif + +void HTMLMediaElement::mediaPlayerEngineUpdated(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerEngineUpdated"); + beginProcessingMediaPlayerCallback(); + if (renderer()) + renderer()->updateFromElement(); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerFirstVideoFrameAvailable(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerFirstVideoFrameAvailable"); + beginProcessingMediaPlayerCallback(); + if (displayMode() == PosterWaitingForVideo) { + setDisplayMode(Video); +#if USE(ACCELERATED_COMPOSITING) + mediaPlayerRenderingModeChanged(m_player.get()); +#endif + } + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerCharacteristicChanged(MediaPlayer*) +{ + LOG(Media, "HTMLMediaElement::mediaPlayerCharacteristicChanged"); + + beginProcessingMediaPlayerCallback(); + if (hasMediaControls()) + mediaControls()->reset(); + if (renderer()) + renderer()->updateFromElement(); + endProcessingMediaPlayerCallback(); +} + +PassRefPtr HTMLMediaElement::buffered() const +{ + if (!m_player) + return TimeRanges::create(); + return m_player->buffered(); +} + +PassRefPtr HTMLMediaElement::played() +{ + if (m_playing) { + float time = currentTime(); + if (time > m_lastSeekTime) + addPlayedRange(m_lastSeekTime, time); + } + + if (!m_playedTimeRanges) + m_playedTimeRanges = TimeRanges::create(); + + return m_playedTimeRanges->copy(); +} + +PassRefPtr HTMLMediaElement::seekable() const +{ + return m_player ? m_player->seekable() : TimeRanges::create(); +} + +bool HTMLMediaElement::potentiallyPlaying() const +{ + // "pausedToBuffer" means the media engine's rate is 0, but only because it had to stop playing + // when it ran out of buffered data. A movie is this state is "potentially playing", modulo the + // checks in couldPlayIfEnoughData(). + bool pausedToBuffer = m_readyStateMaximum >= HAVE_FUTURE_DATA && m_readyState < HAVE_FUTURE_DATA; + return (pausedToBuffer || m_readyState >= HAVE_FUTURE_DATA) && couldPlayIfEnoughData() && !isBlockedOnMediaController(); +} + +bool HTMLMediaElement::couldPlayIfEnoughData() const +{ + return !paused() && !endedPlayback() && !stoppedDueToErrors() && !pausedForUserInteraction(); +} + +bool HTMLMediaElement::endedPlayback() const +{ + float dur = duration(); + if (!m_player || isnan(dur)) + 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. + float now = currentTime(); + if (m_playbackRate > 0) + return dur > 0 && now >= dur && (!loop() || m_mediaController); + + // or the current playback position is the earliest possible position and the direction + // of playback is backwards + if (m_playbackRate < 0) + return now <= 0; + + return false; +} + +bool HTMLMediaElement::stoppedDueToErrors() const +{ + if (m_readyState >= HAVE_METADATA && m_error) { + RefPtr seekableRanges = seekable(); + if (!seekableRanges->contain(currentTime())) + return true; + } + + return false; +} + +bool HTMLMediaElement::pausedForUserInteraction() const +{ +// return !paused() && m_readyState >= HAVE_FUTURE_DATA && [UA requires a decitions from the user] + return false; +} + +float HTMLMediaElement::minTimeSeekable() const +{ + return 0; +} + +float HTMLMediaElement::maxTimeSeekable() const +{ + return m_player ? m_player->maxTimeSeekable() : 0; +} + +void HTMLMediaElement::updateVolume() +{ + if (!m_player) + return; + + // Avoid recursion when the player reports volume changes. + if (!processingMediaPlayerCallback()) { + Page* page = document()->page(); + float volumeMultiplier = page ? page->mediaVolume() : 1; + bool shouldMute = m_muted; + + if (m_mediaController) { + volumeMultiplier *= m_mediaController->volume(); + shouldMute = m_mediaController->muted(); + } + + m_player->setMuted(shouldMute); + m_player->setVolume(m_volume * volumeMultiplier); + } + + if (hasMediaControls()) + mediaControls()->changedVolume(); +} + +void HTMLMediaElement::updatePlayState() +{ + 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 - shouldBePlaying = %s, playerPaused = %s", + boolString(shouldBePlaying), boolString(playerPaused)); + + if (shouldBePlaying) { + setDisplayMode(Video); + invalidateCachedTime(); + + if (playerPaused) { + if (!m_isFullscreen && isVideo() && document() && document()->page() && document()->page()->chrome()->requiresFullscreenForVideoPlayback()) + 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(m_playbackRate); + m_player->setMuted(m_muted); + + m_player->play(); + } + + if (hasMediaControls()) + mediaControls()->playbackStarted(); + startPlaybackProgressTimer(); + m_playing = true; + + } else { // Should not be playing right now + if (!playerPaused) + m_player->pause(); + refreshCachedTime(); + + m_playbackProgressTimer.stop(); + m_playing = false; + float time = currentTime(); + if (time > m_lastSeekTime) + addPlayedRange(m_lastSeekTime, time); + + if (couldPlayIfEnoughData()) + prepareToPlay(); + + if (hasMediaControls()) + mediaControls()->playbackStopped(); + } + + updateMediaController(); + + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::setPausedInternal(bool b) +{ + m_pausedInternal = b; + updatePlayState(); +} + +void HTMLMediaElement::stopPeriodicTimers() +{ + m_progressEventTimer.stop(); + m_playbackProgressTimer.stop(); +} + +void HTMLMediaElement::userCancelledLoad() +{ + LOG(Media, "HTMLMediaElement::userCancelledLoad"); + + if (m_networkState == NETWORK_EMPTY || m_completelyLoaded) + return; + + // If the media data fetching process is aborted by the user: + + // 1 - The user agent should cancel the fetching process. +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + m_player.clear(); +#endif + stopPeriodicTimers(); + m_loadTimer.stop(); + m_loadState = WaitingForSource; + + // 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) + if (m_sourceState != SOURCE_CLOSED) + setSourceState(SOURCE_CLOSED); +#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 = 0; + + // Reset m_readyState since m_player is gone. + m_readyState = HAVE_NOTHING; + updateMediaController(); +} + +bool HTMLMediaElement::canSuspend() const +{ + return true; +} + +void HTMLMediaElement::stop() +{ + LOG(Media, "HTMLMediaElement::stop"); + if (m_isFullscreen) + exitFullscreen(); + + m_inActiveDocument = false; + userCancelledLoad(); + + // Stop the playback without generating events + setPausedInternal(true); + + if (renderer()) + renderer()->updateFromElement(); + + stopPeriodicTimers(); + cancelPendingEventsAndCallbacks(); +} + +void HTMLMediaElement::suspend(ReasonForSuspension why) +{ + LOG(Media, "HTMLMediaElement::suspend"); + + switch (why) + { + case DocumentWillBecomeInactive: + stop(); + break; + case JavaScriptDebuggerPaused: + case WillShowDialog: + // Do nothing, we don't pause media playback in these cases. + break; + } +} + +void HTMLMediaElement::resume() +{ + LOG(Media, "HTMLMediaElement::resume"); + + m_inActiveDocument = true; + setPausedInternal(false); + + 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. + ExceptionCode ec; + load(ec); + } + + if (renderer()) + renderer()->updateFromElement(); +} + +bool HTMLMediaElement::hasPendingActivity() const +{ + // Return true when we have pending events so we can't fire events after the JS + // object gets collected. + bool pending = m_pendingEvents.size(); + LOG(Media, "HTMLMediaElement::hasPendingActivity -> %s", boolString(pending)); + return pending; +} + +void HTMLMediaElement::mediaVolumeDidChange() +{ + LOG(Media, "HTMLMediaElement::mediaVolumeDidChange"); + updateVolume(); +} + +void HTMLMediaElement::defaultEventHandler(Event* event) +{ +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + RenderObject* r = renderer(); + if (!r || !r->isWidget()) + return; + + Widget* widget = toRenderWidget(r)->widget(); + if (widget) + widget->handleEvent(event); +#else + HTMLElement::defaultEventHandler(event); +#endif +} + +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + +void HTMLMediaElement::ensureMediaPlayer() +{ + if (!m_player) + createMediaPlayer(); +} + +void HTMLMediaElement::deliverNotification(MediaPlayerProxyNotificationType notification) +{ + if (notification == MediaPlayerNotificationPlayPauseButtonPressed) { + togglePlayState(); + return; + } + + if (m_player) + m_player->deliverNotification(notification); +} + +void HTMLMediaElement::setMediaPlayerProxy(WebMediaPlayerProxy* proxy) +{ + ensureMediaPlayer(); + m_player->setMediaPlayerProxy(proxy); +} + +void HTMLMediaElement::getPluginProxyParams(KURL& url, Vector& names, Vector& values) +{ + Frame* frame = document()->frame(); + + if (isVideo()) { + KURL posterURL = getNonEmptyURLAttribute(posterAttr); + if (!posterURL.isEmpty() && frame && frame->loader()->willLoadMediaElementURL(posterURL)) { + names.append("_media_element_poster_"); + values.append(posterURL.string()); + } + } + + if (controls()) { + names.append("_media_element_controls_"); + values.append("true"); + } + + url = src(); + if (!isSafeToLoadURL(url, Complain)) + url = selectNextSourceChild(0, DoNothing); + + m_currentSrc = url; + if (url.isValid() && frame && frame->loader()->willLoadMediaElementURL(url)) { + names.append("_media_element_src_"); + values.append(m_currentSrc.string()); + } +} + +void HTMLMediaElement::createMediaPlayerProxy() +{ + ensureMediaPlayer(); + + if (m_proxyWidget || (inDocument() && !m_needWidgetUpdate)) + return; + + Frame* frame = document()->frame(); + if (!frame) + return; + + LOG(Media, "HTMLMediaElement::createMediaPlayerProxy"); + + KURL url; + Vector paramNames; + Vector paramValues; + + getPluginProxyParams(url, paramNames, paramValues); + + // Hang onto the proxy widget so it won't be destroyed if the plug-in is set to + // display:none + m_proxyWidget = frame->loader()->subframeLoader()->loadMediaPlayerProxyPlugin(this, url, paramNames, paramValues); + if (m_proxyWidget) + m_needWidgetUpdate = false; +} + +void HTMLMediaElement::updateWidget(PluginCreationOption) +{ + mediaElement->setNeedWidgetUpdate(false); + + Vector paramNames; + Vector paramValues; + // FIXME: Rename kurl to something more sensible. + KURL kurl; + + mediaElement->getPluginProxyParams(kurl, paramNames, paramValues); + // FIXME: What if document()->frame() is 0? + SubframeLoader* loader = document()->frame()->loader()->subframeLoader(); + loader->loadMediaPlayerProxyPlugin(mediaElement, kurl, paramNames, paramValues); +} + +#endif // ENABLE(PLUGIN_PROXY_FOR_VIDEO) + +bool HTMLMediaElement::isFullscreen() const +{ + if (m_isFullscreen) + return true; + +#if ENABLE(FULLSCREEN_API) + if (document()->webkitIsFullScreen() && document()->webkitCurrentFullScreenElement() == this) + return true; +#endif + + return false; +} + +void HTMLMediaElement::enterFullscreen() +{ + LOG(Media, "HTMLMediaElement::enterFullscreen"); + +#if ENABLE(FULLSCREEN_API) + if (document() && document()->settings() && document()->settings()->fullScreenEnabled()) { + document()->requestFullScreenForElement(this, 0, Document::ExemptIFrameAllowFulScreenRequirement); + return; + } +#endif + ASSERT(!m_isFullscreen); + m_isFullscreen = true; + if (hasMediaControls()) + mediaControls()->enteredFullscreen(); + if (document() && document()->page()) { + document()->page()->chrome()->client()->enterFullscreenForNode(this); + scheduleEvent(eventNames().webkitbeginfullscreenEvent); + } +} + +void HTMLMediaElement::exitFullscreen() +{ + LOG(Media, "HTMLMediaElement::exitFullscreen"); + +#if ENABLE(FULLSCREEN_API) + if (document() && document()->settings() && document()->settings()->fullScreenEnabled()) { + if (document()->webkitIsFullScreen() && document()->webkitCurrentFullScreenElement() == this) + document()->webkitCancelFullScreen(); + return; + } +#endif + ASSERT(m_isFullscreen); + m_isFullscreen = false; + if (hasMediaControls()) + mediaControls()->exitedFullscreen(); + if (document() && document()->page()) { + if (document()->page()->chrome()->requiresFullscreenForVideoPlayback()) + pauseInternal(); + document()->page()->chrome()->client()->exitFullscreenForNode(this); + scheduleEvent(eventNames().webkitendfullscreenEvent); + } +} + +void HTMLMediaElement::didBecomeFullscreenElement() +{ + if (hasMediaControls()) + mediaControls()->enteredFullscreen(); +} + +void HTMLMediaElement::willStopBeingFullscreenElement() +{ + if (hasMediaControls()) + mediaControls()->exitedFullscreen(); +} + +PlatformMedia HTMLMediaElement::platformMedia() const +{ + return m_player ? m_player->platformMedia() : NoPlatformMedia; +} + +#if USE(ACCELERATED_COMPOSITING) +PlatformLayer* HTMLMediaElement::platformLayer() const +{ + return m_player ? m_player->platformLayer() : 0; +} +#endif + +bool HTMLMediaElement::hasClosedCaptions() const +{ + return m_player && m_player->hasClosedCaptions(); +} + +bool HTMLMediaElement::closedCaptionsVisible() const +{ + return m_closedCaptionsVisible; +} + +void HTMLMediaElement::setClosedCaptionsVisible(bool closedCaptionVisible) +{ + LOG(Media, "HTMLMediaElement::setClosedCaptionsVisible(%s)", boolString(closedCaptionVisible)); + + if (!m_player ||!hasClosedCaptions()) + return; + + m_closedCaptionsVisible = closedCaptionVisible; + m_player->setClosedCaptionsVisible(closedCaptionVisible); + if (hasMediaControls()) + mediaControls()->changedClosedCaptionsVisibility(); +} + +void HTMLMediaElement::setWebkitClosedCaptionsVisible(bool visible) +{ + setClosedCaptionsVisible(visible); +} + +bool HTMLMediaElement::webkitClosedCaptionsVisible() const +{ + return 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() +{ + LOG(Media, "HTMLMediaElement::mediaCanStart"); + + ASSERT(m_isWaitingUntilMediaCanStart); + m_isWaitingUntilMediaCanStart = false; + loadInternal(); +} + +bool HTMLMediaElement::isURLAttribute(Attribute* attribute) const +{ + return attribute->name() == srcAttr || HTMLElement::isURLAttribute(attribute); +} + +void HTMLMediaElement::setShouldDelayLoadEvent(bool shouldDelay) +{ + if (m_shouldDelayLoadEvent == shouldDelay) + return; + + LOG(Media, "HTMLMediaElement::setShouldDelayLoadEvent(%s)", boolString(shouldDelay)); + + m_shouldDelayLoadEvent = shouldDelay; + if (shouldDelay) + document()->incrementLoadEventDelayCount(); + else + document()->decrementLoadEventDelayCount(); +} + + +void HTMLMediaElement::getSitesInMediaCache(Vector& sites) +{ + MediaPlayer::getSitesInMediaCache(sites); +} + +void HTMLMediaElement::clearMediaCache() +{ + MediaPlayer::clearMediaCache(); +} + +void HTMLMediaElement::clearMediaCacheForSite(const String& site) +{ + MediaPlayer::clearMediaCacheForSite(site); +} + +void HTMLMediaElement::privateBrowsingStateDidChange() +{ + if (!m_player) + return; + + Settings* settings = document()->settings(); + bool privateMode = !settings || settings->privateBrowsingEnabled(); + LOG(Media, "HTMLMediaElement::privateBrowsingStateDidChange(%s)", boolString(privateMode)); + m_player->setPrivateBrowsingMode(privateMode); +} + +MediaControls* HTMLMediaElement::mediaControls() +{ + return toMediaControls(shadowRoot()->firstChild()); +} + +bool HTMLMediaElement::hasMediaControls() +{ + if (!shadowRoot()) + return false; + + Node* node = shadowRoot()->firstChild(); + return node && node->isMediaControls(); +} + +bool HTMLMediaElement::createMediaControls() +{ + if (hasMediaControls()) + return true; + + ExceptionCode ec; + RefPtr controls = MediaControls::create(document()); + if (!controls) + return false; + + controls->setMediaController(m_mediaController ? m_mediaController.get() : static_cast(this)); + controls->reset(); + + ensureShadowRoot()->appendChild(controls, ec); + return true; +} + +void HTMLMediaElement::configureMediaControls() +{ +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + if (!controls()) { + if (hasMediaControls()) + mediaControls()->hide(); + return; + } + + if (!hasMediaControls() && !createMediaControls()) + return; + + mediaControls()->show(); +#else + if (m_player) + m_player->setControls(controls()); +#endif +} + +#if ENABLE(VIDEO_TRACK) +void HTMLMediaElement::configureTextTrackDisplay() +{ + ASSERT(m_textTracks); + + bool haveVisibleTextTrack = false; + for (unsigned i = 0; i < m_textTracks->length(); ++i) { + if (m_textTracks->item(i)->mode() == TextTrack::SHOWING) { + haveVisibleTextTrack = true; + break; + } + } + + if (m_haveVisibleTextTrack == haveVisibleTextTrack) + return; + m_haveVisibleTextTrack = haveVisibleTextTrack; + + if (!m_haveVisibleTextTrack && !hasMediaControls()) + return; + if (!hasMediaControls() && !createMediaControls()) + return; + mediaControls()->updateTextTrackDisplay(); +} +#endif + +void* HTMLMediaElement::preDispatchEventHandler(Event* event) +{ + if (event && event->type() == eventNames().webkitfullscreenchangeEvent) + configureMediaControls(); + + return 0; +} + +void HTMLMediaElement::createMediaPlayer() +{ +#if ENABLE(WEB_AUDIO) + if (m_audioSourceNode) + m_audioSourceNode->lock(); +#endif + + m_player = MediaPlayer::create(this); + +#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(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 + +#if ENABLE(MICRODATA) +String HTMLMediaElement::itemValueText() const +{ + return getURLAttribute(srcAttr); +} + +void HTMLMediaElement::setItemValueText(const String& value, ExceptionCode& ec) +{ + setAttribute(srcAttr, value, ec); +} +#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(0); + + // 3. If m's mediagroup attribute is being removed, then abort these steps. + if (group.isNull() || 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 elements = documentToElementSetMap().get(document()); + for (HashSet::iterator i = elements.begin(); i != elements.end(); ++i) { + if (*i == 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 ((*i)->mediaGroup() == group) { + // then let controller be that media element's current media controller. + setController((*i)->controller()); + return; + } + } + + // Otherwise, let controller be a newly created MediaController. + setController(MediaController::create(Node::scriptExecutionContext())); +} + +MediaController* HTMLMediaElement::controller() const +{ + return m_mediaController.get(); +} + +void HTMLMediaElement::setController(PassRefPtr controller) +{ + if (m_mediaController) + m_mediaController->removeMediaElement(this); + + m_mediaController = controller; + + if (m_mediaController) + m_mediaController->addMediaElement(this); + + if (hasMediaControls()) + mediaControls()->setMediaController(m_mediaController ? m_mediaController.get() : static_cast(this)); +} + +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. + float mediaControllerPosition = m_mediaController->currentTime(); + if (mediaControllerPosition < startTime() || mediaControllerPosition > startTime() + duration()) + return true; + + return false; +} + +void HTMLMediaElement::prepareMediaFragmentURI() +{ + MediaFragmentURIParser fragmentParser(m_currentSrc); + float dur = duration(); + + double start = fragmentParser.startTime(); + if (start != MediaFragmentURIParser::invalidTimeValue() && start > 0) { + m_fragmentStartTime = start; + if (m_fragmentStartTime > dur) + m_fragmentStartTime = dur; + } else + m_fragmentStartTime = invalidMediaTime; + + double end = fragmentParser.endTime(); + if (end != MediaFragmentURIParser::invalidTimeValue() && end > 0 && end > m_fragmentStartTime) { + m_fragmentEndTime = end; + if (m_fragmentEndTime > dur) + m_fragmentEndTime = dur; + } else + m_fragmentEndTime = invalidMediaTime; + + if (m_fragmentStartTime != invalidMediaTime && m_readyState < HAVE_FUTURE_DATA) + prepareToPlay(); +} + +void HTMLMediaElement::applyMediaFragmentURI() +{ + if (m_fragmentStartTime != invalidMediaTime) { + ExceptionCode ignoredException; + m_sentEndEvent = false; + seek(m_fragmentStartTime, ignoredException); + } +} + +} + +#endif -- cgit v1.2.1