diff -r 000000000000 -r 4f2f89ce4247 WebCore/html/HTMLMediaElement.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebCore/html/HTMLMediaElement.cpp Fri Sep 17 09:02:29 2010 +0300 @@ -0,0 +1,2107 @@ +/* + * Copyright (C) 2007, 2008, 2009, 2010 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 "Attribute.h" +#include "CSSHelper.h" +#include "CSSPropertyNames.h" +#include "CSSValueKeywords.h" +#include "Chrome.h" +#include "ChromeClient.h" +#include "ClientRect.h" +#include "ClientRectList.h" +#include "ContentType.h" +#include "DocLoader.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 "MIMETypeRegistry.h" +#include "MediaDocument.h" +#include "MediaError.h" +#include "MediaList.h" +#include "MediaPlayer.h" +#include "MediaQueryEvaluator.h" +#include "Page.h" +#include "RenderVideo.h" +#include "RenderView.h" +#include "ScriptEventListener.h" +#include "Settings.h" +#include "TimeRanges.h" +#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 + +using namespace std; + +namespace WebCore { + +using namespace HTMLNames; + +HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document* doc) + : HTMLElement(tagName, doc) + , 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_player(0) +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + , m_proxyWidget(0) +#endif + , m_restrictions(NoRestrictions) + , m_preload(MediaPlayer::Auto) + , m_playing(false) + , m_processingMediaPlayerCallback(0) + , m_isWaitingUntilMediaCanStart(false) + , m_processingLoad(false) + , m_delayingTheLoadEvent(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) +{ + document()->registerForDocumentActivationCallbacks(this); + document()->registerForMediaVolumeCallbacks(this); +} + +HTMLMediaElement::~HTMLMediaElement() +{ + if (m_isWaitingUntilMediaCanStart) + document()->removeMediaCanStartListener(this); + document()->unregisterForDocumentActivationCallbacks(this); + document()->unregisterForMediaVolumeCallbacks(this); +} + +void HTMLMediaElement::willMoveToNewOwnerDocument() +{ + if (m_isWaitingUntilMediaCanStart) + document()->removeMediaCanStartListener(this); + document()->unregisterForDocumentActivationCallbacks(this); + document()->unregisterForMediaVolumeCallbacks(this); + HTMLElement::willMoveToNewOwnerDocument(); +} + +void HTMLMediaElement::didMoveToNewOwnerDocument() +{ + if (m_isWaitingUntilMediaCanStart) + document()->addMediaCanStartListener(this); + document()->registerForDocumentActivationCallbacks(this); + document()->registerForMediaVolumeCallbacks(this); + HTMLElement::didMoveToNewOwnerDocument(); +} + + +bool HTMLMediaElement::checkDTD(const Node* newChild) +{ + return newChild->hasTagName(sourceTag) || HTMLElement::checkDTD(newChild); +} + +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(); + } + else if (attrName == controlsAttr) { +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + if (!isVideo() && attached() && (controls() != (renderer() != 0))) { + detach(); + attach(); + } + if (renderer()) + renderer()->updateFromElement(); +#else + if (m_player) + m_player->setControls(controls()); +#endif + } +} + +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 == 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); +} + +bool HTMLMediaElement::rendererIsNeeded(RenderStyle* style) +{ +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + UNUSED_PARAM(style); + Frame* frame = document()->frame(); + if (!frame) + return false; + + return true; +#else + return controls() ? HTMLElement::rendererIsNeeded(style) : 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); + + Frame* frame = document()->frame(); + FrameLoader* loader = frame ? frame->loader() : 0; + if (loader) + loader->showMediaPlayerProxyPlugin(m_proxyWidget.get()); + } + return mediaRenderer; +#else + return new (arena) RenderMedia(this); +#endif +} + +void HTMLMediaElement::insertedIntoDocument() +{ + HTMLElement::insertedIntoDocument(); + if (!src().isEmpty() && m_networkState == NETWORK_EMPTY) + scheduleLoad(); +} + +void HTMLMediaElement::removedFromDocument() +{ + if (m_networkState > NETWORK_EMPTY) + pause(processingUserGesture()); + 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) { + Frame* frame = document()->frame(); + FrameLoader* loader = frame ? frame->loader() : 0; + if (loader) + loader->hideMediaPlayerProxyPlugin(m_proxyWidget.get()); + } +#endif +} + +void HTMLMediaElement::recalcStyle(StyleChange change) +{ + HTMLElement::recalcStyle(change); + + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::scheduleLoad() +{ +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + createMediaPlayerProxy(); +#endif + + if (m_loadTimer.isActive()) + return; + prepareForLoad(); + m_loadTimer.startOneShot(0); +} + +void HTMLMediaElement::scheduleNextSourceChild() +{ + // Schedule the timer to try the next element WITHOUT resetting state ala prepareForLoad. + m_loadTimer.startOneShot(0); +} + +void HTMLMediaElement::scheduleEvent(const AtomicString& eventName) +{ + 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 (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_loadState == LoadingFromSourceElement) + loadNextSourceChild(); + else + loadInternal(); +} + +static String serializeTimeOffset(float time) +{ + String timeString = String::number(time); + // FIXME serialize time offset values properly (format not specified yet) + timeString.append("s"); + return timeString; +} + +static float parseTimeOffset(const String& timeString, bool* ok = 0) +{ + const UChar* characters = timeString.characters(); + unsigned length = timeString.length(); + + if (length && characters[length - 1] == 's') + length--; + + // FIXME parse time offset values (format not specified yet) + float val = charactersToFloat(characters, length, ok); + return val; +} + +float HTMLMediaElement::getTimeOffsetAttribute(const QualifiedName& name, float valueOnError) const +{ + bool ok; + String timeString = getAttribute(name); + float result = parseTimeOffset(timeString, &ok); + if (ok) + return result; + return valueOnError; +} + +void HTMLMediaElement::setTimeOffsetAttribute(const QualifiedName& name, float value) +{ + setAttribute(name, serializeTimeOffset(value)); +} + +PassRefPtr HTMLMediaElement::error() const +{ + return m_error; +} + +KURL HTMLMediaElement::src() const +{ + return getNonEmptyURLAttribute(srcAttr); +} + +void HTMLMediaElement::setSrc(const String& url) +{ + setAttribute(srcAttr, url); +} + +String HTMLMediaElement::currentSrc() const +{ + return m_currentSrc; +} + +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; + } + + return canPlay; +} + +void HTMLMediaElement::load(bool isUserGesture, ExceptionCode& ec) +{ + if (m_restrictions & RequireUserGestureForLoadRestriction && !isUserGesture) + ec = INVALID_STATE_ERR; + else { + m_loadInitiatedByUserGesture = isUserGesture; + prepareForLoad(); + loadInternal(); + } +} + +void HTMLMediaElement::prepareForLoad() +{ + // Perform the cleanup required for the resource load algorithm to run. + stopPeriodicTimers(); + m_loadTimer.stop(); + m_sentStalledEvent = false; + m_haveFiredLoadedData = false; + m_completelyLoaded = false; + + // 1 - Abort any already-running instance of the resource selection algorithm for this element. + 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) + m_player = MediaPlayer::create(this); +#else + createMediaPlayerProxy(); +#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; + m_paused = true; + m_seeking = false; + scheduleEvent(eventNames().emptiedEvent); + } + + // 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; + + m_playedTimeRanges = TimeRanges::create(); + m_lastSeekTime = 0; + m_closedCaptionsVisible = false; + +} + +void HTMLMediaElement::loadInternal() +{ + // If we can't start a load right away, start it later. + Page* page = document()->page(); + if (page && !page->canStartMedia()) { + if (m_isWaitingUntilMediaCanStart) + return; + document()->addMediaCanStartListener(this); + m_isWaitingUntilMediaCanStart = true; + return; + } + + // Steps 1 - 6 were done in prepareForLoad + + // 7 - Invoke the media element's resource selection algorithm. + selectMediaResource(); + m_processingLoad = false; +} + +void HTMLMediaElement::selectMediaResource() +{ + enum Mode { attribute, children }; + Mode mode = attribute; + + // 1 - Set the networkState to NETWORK_NO_SOURCE + m_networkState = NETWORK_NO_SOURCE; + + // 2 - Asynchronously await a stable state. + + // 3 - ... the media element has neither a src attribute ... + if (!hasAttribute(srcAttr)) { + // ... nor a source element child: ... + Node* node; + for (node = firstChild(); node; node = node->nextSibling()) { + if (node->hasTagName(sourceTag)) + break; + } + + if (!node) { + m_loadState = WaitingForSource; + + // ... set the networkState to NETWORK_EMPTY, and abort these steps + m_networkState = NETWORK_EMPTY; + ASSERT(!m_delayingTheLoadEvent); + return; + } + + mode = children; + } + + // 4 + m_delayingTheLoadEvent = true; + m_networkState = NETWORK_LOADING; + + // 5 + scheduleEvent(eventNames().loadstartEvent); + + // 6 - If mode is attribute, then run these substeps + if (mode == attribute) { + // If the src attribute's value is the empty string ... jump down to the failed step below + KURL mediaURL = getNonEmptyURLAttribute(srcAttr); + if (mediaURL.isEmpty()) { + noneSupported(); + return; + } + + if (isSafeToLoadURL(mediaURL, Complain) && dispatchBeforeLoadEvent(mediaURL.string())) { + ContentType contentType(""); + m_loadState = LoadingFromSrcAttr; + loadResource(mediaURL, contentType); + } else + noneSupported(); + + return; + } + + // Otherwise, the source elements will be used + m_currentSourceNode = 0; + 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 + m_player = MediaPlayer::create(this); +#endif + + m_loadState = LoadingFromSourceElement; + loadResource(mediaURL, contentType); +} + +void HTMLMediaElement::loadResource(const KURL& initialURL, ContentType& contentType) +{ + ASSERT(isSafeToLoadURL(initialURL, Complain)); + + Frame* frame = document()->frame(); + if (!frame) + return; + FrameLoader* loader = frame->loader(); + if (!loader) + return; + + KURL url(initialURL); + if (!loader->willLoadMediaElementURL(url)) + return; + + // The resource fetch algorithm + m_networkState = NETWORK_LOADING; + + m_currentSrc = url; + + if (m_sendProgressEvents) + startProgressEventTimer(); + + if (!autoplay()) + m_player->setPreload(m_preload); + m_player->setPreservesPitch(m_webkitPreservesPitch); + updateVolume(); + + m_player->load(m_currentSrc, contentType); + + if (isVideo() && m_player->canLoadPoster()) { + KURL posterUrl = poster(); + if (!posterUrl.isEmpty()) + m_player->setPoster(posterUrl); + } + + if (renderer()) + renderer()->updateFromElement(); +} + +bool HTMLMediaElement::isSafeToLoadURL(const KURL& url, InvalidSourceAction actionIfInvalid) +{ + if (!url.isValid()) + return false; + + Frame* frame = document()->frame(); + FrameLoader* loader = frame ? frame->loader() : 0; + + // don't allow remote to local urls, and check with the frame loader client. + if (!loader || !SecurityOrigin::canLoad(url, String(), document())) { + if (actionIfInvalid == Complain) + FrameLoader::reportLocalLoadFailed(frame, url.string()); + 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() +{ + 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. + m_delayingTheLoadEvent = false; +} + +void HTMLMediaElement::noneSupported() +{ + stopPeriodicTimers(); + m_loadState = WaitingForSource; + m_currentSourceNode = 0; + + // 5 - Reaching this step indicates that either the URL failed to resolve, or the media + // resource failed to load. 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 - Set the element's networkState attribute to the NETWORK_NO_SOURCE value. + m_networkState = NETWORK_NO_SOURCE; + + // 7 - Queue a task to fire a progress event called error at the media element, in + // the context of the fetching process that was used to try to obtain the media + // resource in the resource fetch algorithm. + scheduleEvent(eventNames().errorEvent); + + // 8 - Set the element's delaying-the-load-event flag to false. This stops delaying the load event. + m_delayingTheLoadEvent = false; + + // 9 -Abort these steps. Until the load() method is invoked, the element won't attempt to load another resource. + + updatePosterImage(); + + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::mediaEngineError(PassRefPtr err) +{ + // 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 progress event called error at the media element, in + // the context of the fetching process started by this instance of this algorithm. + scheduleEvent(eventNames().errorEvent); + + // 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. + m_delayingTheLoadEvent = false; + + // 6 - Abort the overall resource selection algorithm. + m_currentSourceNode = 0; +} + +void 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::setNetworkState(MediaPlayer::NetworkState state) +{ + 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) { + 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) { + m_currentSourceNode->scheduleErrorEvent(); + if (havePotentialSourceChild()) + scheduleNextSourceChild(); + else + waitForSourceChange(); + + return; + } + + if (state == MediaPlayer::NetworkError) + mediaEngineError(MediaError::create(MediaError::MEDIA_ERR_NETWORK)); + else if (state == MediaPlayer::DecodeError) + mediaEngineError(MediaError::create(MediaError::MEDIA_ERR_DECODE)); + else if (state == MediaPlayer::FormatError && m_loadState == LoadingFromSrcAttr) + noneSupported(); + + updatePosterImage(); + return; + } + + if (state == MediaPlayer::Idle) { + if (m_networkState > NETWORK_IDLE) { + m_progressEventTimer.stop(); + scheduleEvent(eventNames().suspendEvent); + } + 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; + } +} + +void HTMLMediaElement::mediaPlayerReadyStateChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + + setReadyState(m_player->readyState()); + + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::setReadyState(MediaPlayer::ReadyState state) +{ + // Set "wasPotentiallyPlaying" BEFORE updating m_readyState, potentiallyPlaying() uses it + bool wasPotentiallyPlaying = potentiallyPlaying(); + + ReadyState oldState = m_readyState; + m_readyState = static_cast(state); + + if (m_readyState == oldState) + return; + + if (oldState > m_readyStateMaximum) + m_readyStateMaximum = oldState; + + if (m_networkState == NETWORK_EMPTY) + return; + + if (m_seeking) { + // 4.8.10.10, step 8 + if (wasPotentiallyPlaying && m_readyState < HAVE_FUTURE_DATA) + scheduleEvent(eventNames().waitingEvent); + + // 4.8.10.10, step 9 + if (m_readyState < HAVE_CURRENT_DATA) { + if (oldState >= HAVE_CURRENT_DATA) + scheduleEvent(eventNames().seekingEvent); + } else { + // 4.8.10.10 step 12 & 13. + finishSeek(); + } + + } else { + if (wasPotentiallyPlaying && m_readyState < HAVE_FUTURE_DATA) { + // 4.8.10.9 + scheduleTimeupdateEvent(false); + scheduleEvent(eventNames().waitingEvent); + } + } + + if (m_readyState >= HAVE_METADATA && oldState < HAVE_METADATA) { + scheduleEvent(eventNames().durationchangeEvent); + scheduleEvent(eventNames().loadedmetadataEvent); + +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + if (renderer() && renderer()->isVideo()) { + toRenderVideo(renderer())->videoSizeChanged(); + } +#endif + m_delayingTheLoadEvent = false; + m_player->seek(0); + } + + bool shouldUpdatePosterImage = false; + + // 4.8.10.7 says loadeddata is sent only when the new state *is* HAVE_CURRENT_DATA: "If the + // previous ready state was HAVE_METADATA and the new ready state is HAVE_CURRENT_DATA", + // but the event table at the end of the spec says it is sent when: "readyState newly + // increased to HAVE_CURRENT_DATA or greater for the first time" + // We go with the later because it seems useful to count on getting this event + if (m_readyState >= HAVE_CURRENT_DATA && oldState < HAVE_CURRENT_DATA && !m_haveFiredLoadedData) { + m_haveFiredLoadedData = true; + shouldUpdatePosterImage = true; + scheduleEvent(eventNames().loadeddataEvent); + } + + bool isPotentiallyPlaying = potentiallyPlaying(); + if (m_readyState == HAVE_FUTURE_DATA && oldState <= HAVE_CURRENT_DATA) { + scheduleEvent(eventNames().canplayEvent); + if (isPotentiallyPlaying) + scheduleEvent(eventNames().playingEvent); + shouldUpdatePosterImage = true; + } + + if (m_readyState == HAVE_ENOUGH_DATA && oldState < HAVE_ENOUGH_DATA) { + 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; + scheduleEvent(eventNames().playEvent); + scheduleEvent(eventNames().playingEvent); + } + + shouldUpdatePosterImage = true; + } + + if (shouldUpdatePosterImage) + updatePosterImage(); + + updatePlayState(); +} + +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; + } + } else { + scheduleEvent(eventNames().progressEvent); + m_previousProgress = progress; + m_previousProgressTime = time; + m_sentStalledEvent = false; + if (renderer()) + renderer()->updateFromElement(); + } +} + +void HTMLMediaElement::rewind(float timeDelta) +{ + ExceptionCode e; + setCurrentTime(max(currentTime() - timeDelta, minTimeSeekable()), e); +} + +void HTMLMediaElement::returnToRealtime() +{ + ExceptionCode e; + setCurrentTime(maxTimeSeekable(), e); +} + +void HTMLMediaElement::addPlayedRange(float start, float end) +{ + if (!m_playedTimeRanges) + m_playedTimeRanges = TimeRanges::create(); + m_playedTimeRanges->add(start, end); +} + +bool HTMLMediaElement::supportsSave() const +{ + return m_player ? m_player->supportsSave() : false; +} + +void HTMLMediaElement::seek(float time, ExceptionCode& ec) +{ + // 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; + } + + // Get the current time before setting m_seeking, m_lastSeekTime is returned once it is set. + float now = currentTime(); + + // 3 - Set the seeking IDL attribute to true. + // The flag will be cleared when the engine tells is the time has actually changed + m_seeking = true; + + // 4 - Queue a task to fire a simple event named timeupdate at the element. + scheduleTimeupdateEvent(false); + + // 6 - If the new playback position is later than the end of the media resource, then let it be the end + // of the media resource instead. + time = min(time, duration()); + + // 7 - 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); + + // 8 - If the (possibly now changed) new playback position is not in one of the ranges given in the + // seekable attribute, then let it be the position in one of the ranges given in the seekable attribute + // that is the nearest to the new playback position. ... If there are no ranges given in the seekable + // attribute then set the seeking IDL attribute to false and abort these steps. + RefPtr seekableRanges = seekable(); + if (!seekableRanges->length() || time == now) { + 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; + + // 9 - Set the current playback position to the given new playback position + m_player->seek(time); + + // 10-15 are handled, if necessary, when the engine signals a readystate change. + +} + +void HTMLMediaElement::finishSeek() +{ + // 4.8.10.10 Seeking step 12 + m_seeking = false; + + // 4.8.10.10 Seeking step 13 + scheduleEvent(eventNames().seekedEvent); +} + +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; +} + +// playback state +float HTMLMediaElement::currentTime() const +{ + if (!m_player) + return 0; + if (m_seeking) + return m_lastSeekTime; + return m_player->currentTime(); +} + +void HTMLMediaElement::setCurrentTime(float time, ExceptionCode& ec) +{ + seek(time, ec); +} + +float HTMLMediaElement::startTime() const +{ + if (!m_player) + return 0; + return m_player->startTime(); +} + +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_player ? m_player->rate() : 0; +} + +void HTMLMediaElement::setPlaybackRate(float rate) +{ + if (m_playbackRate != rate) { + m_playbackRate = rate; + scheduleEvent(eventNames().ratechangeEvent); + } + if (m_player && potentiallyPlaying() && m_player->rate() != rate) + m_player->setRate(rate); +} + +bool HTMLMediaElement::webkitPreservesPitch() const +{ + return m_webkitPreservesPitch; +} + +void HTMLMediaElement::setWebkitPreservesPitch(bool 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 hasAttribute(autoplayAttr); +} + +void HTMLMediaElement::setAutoplay(bool 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) +{ + setAttribute(preloadAttr, preload); +} + +void HTMLMediaElement::play(bool isUserGesture) +{ + if (m_restrictions & RequireUserGestureForRateChangeRestriction && !isUserGesture) + return; + + Document* doc = document(); + Settings* settings = doc->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(!isUserGesture); + String host = doc->baseURL().host(); + if (host.endsWith(".npr.org", false) || equalIgnoringCase(host, "npr.org")) + return; + } + + playInternal(); +} + +void HTMLMediaElement::playInternal() +{ + // 4.8.10.9. Playing the media resource + if (!m_player || m_networkState == NETWORK_EMPTY) + scheduleLoad(); + + if (endedPlayback()) { + ExceptionCode unused; + seek(0, unused); + } + + setPlaybackRate(defaultPlaybackRate()); + + if (m_paused) { + m_paused = false; + 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(); +} + +void HTMLMediaElement::pause(bool isUserGesture) +{ + if (m_restrictions & RequireUserGestureForRateChangeRestriction && !isUserGesture) + return; + + pauseInternal(); +} + + +void HTMLMediaElement::pauseInternal() +{ + // 4.8.10.9. Playing the media resource + if (!m_player || m_networkState == NETWORK_EMPTY) + scheduleLoad(); + + m_autoplaying = false; + + if (!m_paused) { + m_paused = true; + scheduleTimeupdateEvent(false); + scheduleEvent(eventNames().pauseEvent); + } + + updatePlayState(); +} + +bool HTMLMediaElement::loop() const +{ + return hasAttribute(loopAttr); +} + +void HTMLMediaElement::setLoop(bool b) +{ + setBooleanAttribute(loopAttr, b); +} + +bool HTMLMediaElement::controls() const +{ + Frame* frame = document()->frame(); + + // always show controls when scripting is disabled + if (frame && !frame->script()->canExecuteScripts(NotAboutToExecuteScript)) + return true; + + return hasAttribute(controlsAttr); +} + +void HTMLMediaElement::setControls(bool b) +{ + setBooleanAttribute(controlsAttr, b); +} + +float HTMLMediaElement::volume() const +{ + return m_volume; +} + +void HTMLMediaElement::setVolume(float vol, ExceptionCode& ec) +{ + 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) +{ + 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 (renderer()) + renderer()->updateFromElement(); + } else + updateVolume(); + } + scheduleEvent(eventNames().volumechangeEvent); + } +} + +void HTMLMediaElement::togglePlayState() +{ + // 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()) + playInternal(); + else + pauseInternal(); +} + +void HTMLMediaElement::beginScrubbing() +{ + 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(processingUserGesture()); + } 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() +{ + 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; + +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_playbackRate) + return; + + scheduleTimeupdateEvent(true); + + // FIXME: deal with cue ranges here +} + +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 = m_player ? m_player->currentTime() : 0; + 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; +} + +bool HTMLMediaElement::havePotentialSourceChild() +{ + // Stash the current node so we can restore it after checking + // to see there is another potential + HTMLSourceElement* currentSourceNode = m_currentSourceNode; + KURL nextURL = selectNextSourceChild(0, DoNothing); + m_currentSourceNode = currentSourceNode; + + return nextURL.isValid(); +} + +KURL HTMLMediaElement::selectNextSourceChild(ContentType *contentType, InvalidSourceAction actionIfInvalid) +{ + KURL mediaURL; + Node* node; + bool lookingForPreviousNode = m_currentSourceNode; + bool canUse = false; + + for (node = firstChild(); !canUse && node; node = node->nextSibling()) { + if (!node->hasTagName(sourceTag)) + continue; + + if (lookingForPreviousNode) { + if (m_currentSourceNode == static_cast(node)) + lookingForPreviousNode = false; + continue; + } + + HTMLSourceElement* 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 (mediaURL.isEmpty()) + goto check_again; + + if (source->hasAttribute(mediaAttr)) { + MediaQueryEvaluator screenEval("screen", document()->frame(), renderer() ? renderer()->style() : 0); + RefPtr media = MediaList::createAllowingDescriptionSyntax(source->media()); + if (!screenEval.eval(media.get())) + goto check_again; + } + + if (source->hasAttribute(typeAttr)) { + 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; + if (contentType) + *contentType = ContentType(source->type()); + +check_again: + if (!canUse && actionIfInvalid == Complain) + source->scheduleErrorEvent(); + m_currentSourceNode = static_cast(node); + } + + if (!canUse) + m_currentSourceNode = 0; + return canUse ? mediaURL : KURL(); +} + +void HTMLMediaElement::mediaPlayerTimeChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + + // 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); + + // 4.8.10.10 step 12 & 13. Needed if no ReadyState change is associated with the seek. + if (m_readyState >= HAVE_CURRENT_DATA && m_seeking) + finishSeek(); + + float now = currentTime(); + float dur = duration(); + if (!isnan(dur) && dur && now >= dur) { + if (loop()) { + ExceptionCode ignoredException; + m_sentEndEvent = false; + seek(0, ignoredException); + } else { + if (!m_sentEndEvent) { + m_sentEndEvent = true; + scheduleEvent(eventNames().endedEvent); + } + } + } + else + m_sentEndEvent = false; + + updatePlayState(); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerVolumeChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + if (m_player) + m_volume = m_player->volume(); + updateVolume(); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerMuteChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + if (m_player) + setMuted(m_player->muted()); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerDurationChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + scheduleEvent(eventNames().durationchangeEvent); +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + if (renderer()) { + renderer()->updateFromElement(); + if (renderer()->isVideo()) + toRenderVideo(renderer())->videoSizeChanged(); + } +#endif + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerRateChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); + // Stash the rate in case the one we tried to set isn't what the engine is + // using (eg. it can't handle the rate we set) + m_playbackRate = m_player->rate(); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerSawUnsupportedTracks(MediaPlayer*) +{ + // 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(); + if (renderer()) + renderer()->repaint(); + + updatePosterImage(); + endProcessingMediaPlayerCallback(); +} + +void HTMLMediaElement::mediaPlayerSizeChanged(MediaPlayer*) +{ + beginProcessingMediaPlayerCallback(); +#if !ENABLE(PLUGIN_PROXY_FOR_VIDEO) + if (renderer() && renderer()->isVideo()) + toRenderVideo(renderer())->videoSizeChanged(); +#endif + 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*) +{ + // Kick off a fake recalcStyle that will update the compositing tree. + setNeedsStyleRecalc(SyntheticStyleChange); +} +#endif + +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 +{ + // FIXME real ranges support + if (!maxTimeSeekable()) + return TimeRanges::create(); + return TimeRanges::create(minTimeSeekable(), maxTimeSeekable()); +} + +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(); +} + +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 and the media element does not have a loop attribute specified, + float now = currentTime(); + if (m_playbackRate > 0) + return now >= dur && !loop(); + + // 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; + + m_player->setMuted(m_muted); + m_player->setVolume(m_volume * volumeMultiplier); + } + + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::updatePlayState() +{ + if (!m_player) + return; + + if (m_pausedInternal) { + if (!m_player->paused()) + m_player->pause(); + m_playbackProgressTimer.stop(); + return; + } + + bool shouldBePlaying = potentiallyPlaying(); + bool playerPaused = m_player->paused(); + if (shouldBePlaying && playerPaused) { + // Set rate before calling play in case the rate was set before the media engine wasn't setup. + // The media engine should just stash the rate since it isn't already playing. + m_player->setRate(m_playbackRate); + m_player->play(); + startPlaybackProgressTimer(); + m_playing = true; + } else if (!shouldBePlaying && !playerPaused) { + m_player->pause(); + m_playbackProgressTimer.stop(); + m_playing = false; + float time = currentTime(); + if (time > m_lastSeekTime) + addPlayedRange(m_lastSeekTime, time); + } else if (couldPlayIfEnoughData() && playerPaused) + m_player->prepareToPlay(); + + 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() +{ + 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(); + + // 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 progress event called abort at the media element, in the context + // of the fetching process started by this instance of this algorithm. + scheduleEvent(eventNames().abortEvent); + + // 5 - 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 called emptied at the element. Otherwise, set 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; + + // 6 - Set the element's delaying-the-load-event flag to false. This stops delaying the load event. + m_delayingTheLoadEvent = false; + + // 7 - Abort the overall resource selection algorithm. + m_currentSourceNode = 0; + + // Reset m_readyState since m_player is gone. + m_readyState = HAVE_NOTHING; +} + +void HTMLMediaElement::documentWillBecomeInactive() +{ + if (m_isFullscreen) + exitFullscreen(); + + m_inActiveDocument = false; + userCancelledLoad(); + + // Stop the playback without generating events + setPausedInternal(true); + + if (renderer()) + renderer()->updateFromElement(); + + stopPeriodicTimers(); + cancelPendingEventsAndCallbacks(); +} + +void HTMLMediaElement::documentDidBecomeActive() +{ + 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(processingUserGesture(), ec); + } + + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::mediaVolumeDidChange() +{ + updateVolume(); +} + +IntRect HTMLMediaElement::screenRect() +{ + if (!renderer()) + return IntRect(); + return renderer()->view()->frameView()->contentsToScreen(renderer()->absoluteBoundingBoxRect()); +} + +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 + if (renderer() && renderer()->isMedia()) + toRenderMedia(renderer())->forwardEvent(event); + if (event->defaultHandled()) + return; + HTMLElement::defaultEventHandler(event); +#endif +} + +bool HTMLMediaElement::processingUserGesture() const +{ + Frame* frame = document()->frame(); + FrameLoader* loader = frame ? frame->loader() : 0; + + // return 'true' for safety if we don't know the answer + return loader ? loader->isProcessingUserGesture() : true; +} + +#if ENABLE(PLUGIN_PROXY_FOR_VIDEO) + +void HTMLMediaElement::ensureMediaPlayer() +{ + if (!m_player) + m_player = MediaPlayer::create(this); +} + +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(); + FrameLoader* loader = frame ? frame->loader() : 0; + + if (isVideo()) { + String poster = poster(); + if (!poster.isEmpty() && loader) { + KURL posterURL = loader->completeURL(poster); + if (posterURL.isValid() && 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.string(); + if (url.isValid() && loader && loader->willLoadMediaElementURL(url)) { + names.append("_media_element_src_"); + values.append(m_currentSrc); + } +} + +void HTMLMediaElement::finishParsingChildren() +{ + HTMLElement::finishParsingChildren(); + document()->updateStyleIfNeeded(); + createMediaPlayerProxy(); +} + +void HTMLMediaElement::createMediaPlayerProxy() +{ + ensureMediaPlayer(); + + if (m_proxyWidget || (inDocument() && !m_needWidgetUpdate)) + return; + + Frame* frame = document()->frame(); + FrameLoader* loader = frame ? frame->loader() : 0; + if (!loader) + return; + + 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 = loader->subframeLoader()->loadMediaPlayerProxyPlugin(this, url, paramNames, paramValues); + if (m_proxyWidget) + m_needWidgetUpdate = false; +} +#endif // ENABLE(PLUGIN_PROXY_FOR_VIDEO) + +void HTMLMediaElement::enterFullscreen() +{ + ASSERT(!m_isFullscreen); + m_isFullscreen = true; + if (document() && document()->page()) { + document()->page()->chrome()->client()->enterFullscreenForNode(this); + scheduleEvent(eventNames().webkitbeginfullscreenEvent); + } +} + +void HTMLMediaElement::exitFullscreen() +{ + ASSERT(m_isFullscreen); + m_isFullscreen = false; + if (document() && document()->page()) { + document()->page()->chrome()->client()->exitFullscreenForNode(this); + scheduleEvent(eventNames().webkitendfullscreenEvent); + } +} + +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) +{ + if (!m_player ||!hasClosedCaptions()) + return; + + m_closedCaptionsVisible = closedCaptionVisible; + m_player->setClosedCaptionsVisible(closedCaptionVisible); + if (renderer()) + renderer()->updateFromElement(); +} + +void HTMLMediaElement::setWebkitClosedCaptionsVisible(bool visible) +{ + setClosedCaptionsVisible(visible); +} + +bool HTMLMediaElement::webkitClosedCaptionsVisible() const +{ + return closedCaptionsVisible(); +} + + +bool HTMLMediaElement::webkitHasClosedCaptions() const +{ + return hasClosedCaptions(); +} + +void HTMLMediaElement::mediaCanStart() +{ + ASSERT(m_isWaitingUntilMediaCanStart); + m_isWaitingUntilMediaCanStart = false; + loadInternal(); +} + +bool HTMLMediaElement::isURLAttribute(Attribute* attribute) const +{ + return attribute->name() == srcAttr; +} + +} + +#endif