/* * 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