/* * Copyright (C) 2011 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "MediaController.h" #if ENABLE(VIDEO) #include "EventNames.h" #include "HTMLMediaElement.h" #include "TimeRanges.h" #include #include #include #include #include namespace WebCore { WTF_MAKE_ISO_ALLOCATED_IMPL(MediaController); Ref MediaController::create(ScriptExecutionContext& context) { return adoptRef(*new MediaController(context)); } MediaController::MediaController(ScriptExecutionContext& context) : m_paused(false) , m_defaultPlaybackRate(1) , m_volume(1) , m_position(MediaPlayer::invalidTime()) , m_muted(false) , m_readyState(HAVE_NOTHING) , m_playbackState(WAITING) , m_asyncEventTimer(*this, &MediaController::asyncEventTimerFired) , m_clearPositionTimer(*this, &MediaController::clearPositionTimerFired) , m_closedCaptionsVisible(false) , m_clock(PAL::Clock::create()) , m_scriptExecutionContext(context) , m_timeupdateTimer(*this, &MediaController::scheduleTimeupdateEvent) { } MediaController::~MediaController() = default; void MediaController::addMediaElement(HTMLMediaElement& element) { ASSERT(!m_mediaElements.contains(&element)); m_mediaElements.append(&element); bringElementUpToSpeed(element); } void MediaController::removeMediaElement(HTMLMediaElement& element) { ASSERT(m_mediaElements.contains(&element)); m_mediaElements.remove(m_mediaElements.find(&element)); } bool MediaController::containsMediaElement(HTMLMediaElement& element) const { return m_mediaElements.contains(&element); } Ref MediaController::buffered() const { if (m_mediaElements.isEmpty()) return TimeRanges::create(); // The buffered attribute must return a new static normalized TimeRanges object that represents // the intersection of the ranges of the media resources of the mediagroup elements that the // user agent has buffered, at the time the attribute is evaluated. Ref bufferedRanges = m_mediaElements.first()->buffered(); for (size_t index = 1; index < m_mediaElements.size(); ++index) bufferedRanges->intersectWith(m_mediaElements[index]->buffered()); return bufferedRanges; } Ref MediaController::seekable() const { if (m_mediaElements.isEmpty()) return TimeRanges::create(); // The seekable attribute must return a new static normalized TimeRanges object that represents // the intersection of the ranges of the media resources of the mediagroup elements that the // user agent is able to seek to, at the time the attribute is evaluated. Ref seekableRanges = m_mediaElements.first()->seekable(); for (size_t index = 1; index < m_mediaElements.size(); ++index) seekableRanges->intersectWith(m_mediaElements[index]->seekable()); return seekableRanges; } Ref MediaController::played() { if (m_mediaElements.isEmpty()) return TimeRanges::create(); // The played attribute must return a new static normalized TimeRanges object that represents // the union of the ranges of the media resources of the mediagroup elements that the // user agent has so far rendered, at the time the attribute is evaluated. Ref playedRanges = m_mediaElements.first()->played(); for (size_t index = 1; index < m_mediaElements.size(); ++index) playedRanges->unionWith(m_mediaElements[index]->played()); return playedRanges; } double MediaController::duration() const { // FIXME: Investigate caching the maximum duration and only updating the cached value // when the mediagroup elements' durations change. double maxDuration = 0; for (auto& mediaElement : m_mediaElements) { double duration = mediaElement->duration(); if (std::isnan(duration)) continue; maxDuration = std::max(maxDuration, duration); } return maxDuration; } double MediaController::currentTime() const { if (m_mediaElements.isEmpty()) return 0; if (m_position == MediaPlayer::invalidTime()) { // Some clocks may return times outside the range of [0..duration]. m_position = std::max(0, std::min(duration(), m_clock->currentTime())); m_clearPositionTimer.startOneShot(0_s); } return m_position; } void MediaController::setCurrentTime(double time) { // When the user agent is to seek the media controller to a particular new playback position, // it must follow these steps: // If the new playback position is less than zero, then set it to zero. time = std::max(0.0, time); // If the new playback position is greater than the media controller duration, then set it // to the media controller duration. time = std::min(time, duration()); // Set the media controller position to the new playback position. m_clock->setCurrentTime(time); // Seek each mediagroup element to the new playback position relative to the media element timeline. for (auto& mediaElement : m_mediaElements) mediaElement->seek(MediaTime::createWithDouble(time)); scheduleTimeupdateEvent(); m_resetCurrentTimeInNextPlay = false; } void MediaController::unpause() { // When the unpause() method is invoked, if the MediaController is a paused media controller, if (!m_paused) return; // the user agent must change the MediaController into a playing media controller, m_paused = false; // queue a task to fire a simple event named play at the MediaController, scheduleEvent(eventNames().playEvent); // and then report the controller state of the MediaController. reportControllerState(); } void MediaController::play() { // When the play() method is invoked, the user agent must invoke the play method of each // mediagroup element in turn, for (auto& mediaElement : m_mediaElements) mediaElement->play(); // and then invoke the unpause method of the MediaController. unpause(); } void MediaController::pause() { // When the pause() method is invoked, if the MediaController is a playing media controller, if (m_paused) return; // then the user agent must change the MediaController into a paused media controller, m_paused = true; // queue a task to fire a simple event named pause at the MediaController, scheduleEvent(eventNames().pauseEvent); // and then report the controller state of the MediaController. reportControllerState(); } void MediaController::setDefaultPlaybackRate(double rate) { if (m_defaultPlaybackRate == rate) return; // The defaultPlaybackRate attribute, on setting, must set the MediaController's media controller // default playback rate to the new value, m_defaultPlaybackRate = rate; // then queue a task to fire a simple event named ratechange at the MediaController. scheduleEvent(eventNames().ratechangeEvent); } double MediaController::playbackRate() const { return m_clock->playRate(); } void MediaController::setPlaybackRate(double rate) { if (m_clock->playRate() == rate) return; // The playbackRate attribute, on setting, must set the MediaController's media controller // playback rate to the new value, m_clock->setPlayRate(rate); for (auto& mediaElement : m_mediaElements) mediaElement->updatePlaybackRate(); // then queue a task to fire a simple event named ratechange at the MediaController. scheduleEvent(eventNames().ratechangeEvent); } ExceptionOr MediaController::setVolume(double level) { if (m_volume == level) return { }; // If the new value is outside the range 0.0 to 1.0 inclusive, then, on setting, an // IndexSizeError exception must be raised instead. if (!(level >= 0 && level <= 1)) return Exception { IndexSizeError }; // The volume attribute, on setting, if the new value is in the range 0.0 to 1.0 inclusive, // must set the MediaController's media controller volume multiplier to the new value m_volume = level; // and queue a task to fire a simple event named volumechange at the MediaController. scheduleEvent(eventNames().volumechangeEvent); for (auto& mediaElement : m_mediaElements) mediaElement->updateVolume(); return { }; } void MediaController::setMuted(bool flag) { if (m_muted == flag) return; // The muted attribute, on setting, must set the MediaController's media controller mute override // to the new value m_muted = flag; // and queue a task to fire a simple event named volumechange at the MediaController. scheduleEvent(eventNames().volumechangeEvent); for (auto& mediaElement : m_mediaElements) mediaElement->updateVolume(); } static const AtomString& playbackStateWaiting() { static MainThreadNeverDestroyed waiting("waiting", AtomString::ConstructFromLiteral); return waiting; } static const AtomString& playbackStatePlaying() { static MainThreadNeverDestroyed playing("playing", AtomString::ConstructFromLiteral); return playing; } static const AtomString& playbackStateEnded() { static MainThreadNeverDestroyed ended("ended", AtomString::ConstructFromLiteral); return ended; } const AtomString& MediaController::playbackState() const { switch (m_playbackState) { case WAITING: return playbackStateWaiting(); case PLAYING: return playbackStatePlaying(); case ENDED: return playbackStateEnded(); default: ASSERT_NOT_REACHED(); return nullAtom(); } } void MediaController::reportControllerState() { updateReadyState(); updatePlaybackState(); } static AtomString eventNameForReadyState(MediaControllerInterface::ReadyState state) { switch (state) { case MediaControllerInterface::HAVE_NOTHING: return eventNames().emptiedEvent; case MediaControllerInterface::HAVE_METADATA: return eventNames().loadedmetadataEvent; case MediaControllerInterface::HAVE_CURRENT_DATA: return eventNames().loadeddataEvent; case MediaControllerInterface::HAVE_FUTURE_DATA: return eventNames().canplayEvent; case MediaControllerInterface::HAVE_ENOUGH_DATA: return eventNames().canplaythroughEvent; default: ASSERT_NOT_REACHED(); return nullAtom(); } } void MediaController::updateReadyState() { ReadyState oldReadyState = m_readyState; ReadyState newReadyState; if (m_mediaElements.isEmpty()) { // If the MediaController has no mediagroup elements, let new readiness state be 0. newReadyState = HAVE_NOTHING; } else { // Otherwise, let it have the lowest value of the readyState IDL attributes of all of its // mediagroup elements. newReadyState = m_mediaElements.first()->readyState(); for (size_t index = 1; index < m_mediaElements.size(); ++index) newReadyState = std::min(newReadyState, m_mediaElements[index]->readyState()); } if (newReadyState == oldReadyState) return; // If the MediaController's most recently reported readiness state is greater than new readiness // state then queue a task to fire a simple event at the MediaController object, whose name is the // event name corresponding to the value of new readiness state given in the table below. [omitted] if (oldReadyState > newReadyState) { scheduleEvent(eventNameForReadyState(newReadyState)); return; } // If the MediaController's most recently reported readiness state is less than the new readiness // state, then run these substeps: // 1. Let next state be the MediaController's most recently reported readiness state. ReadyState nextState = oldReadyState; do { // 2. Loop: Increment next state by one. nextState = static_cast(nextState + 1); // 3. Queue a task to fire a simple event at the MediaController object, whose name is the // event name corresponding to the value of next state given in the table below. [omitted] scheduleEvent(eventNameForReadyState(nextState)); // If next state is less than new readiness state, then return to the step labeled loop } while (nextState < newReadyState); // Let the MediaController's most recently reported readiness state be new readiness state. m_readyState = newReadyState; } void MediaController::updatePlaybackState() { PlaybackState oldPlaybackState = m_playbackState; PlaybackState newPlaybackState; // Initialize new playback state by setting it to the state given for the first matching // condition from the following list: if (m_mediaElements.isEmpty()) { // If the MediaController has no mediagroup elements // Let new playback state be waiting. newPlaybackState = WAITING; } else if (hasEnded()) { // If all of the MediaController's mediagroup elements have ended playback and the media // controller playback rate is positive or zero // Let new playback state be ended. newPlaybackState = ENDED; } else if (isBlocked()) { // If the MediaController is a blocked media controller // Let new playback state be waiting. newPlaybackState = WAITING; } else { // Otherwise // Let new playback state be playing. newPlaybackState = PLAYING; } // If the MediaController's most recently reported playback state is not equal to new playback state if (newPlaybackState == oldPlaybackState) return; // and the new playback state is ended, if (newPlaybackState == ENDED) { // then queue a task that, if the MediaController object is a playing media controller, and // all of the MediaController's mediagroup elements have still ended playback, and the // media controller playback rate is still positive or zero, if (!m_paused && hasEnded()) { // changes the MediaController object to a paused media controller m_paused = true; // and then fires a simple event named pause at the MediaController object. scheduleEvent(eventNames().pauseEvent); } } // If the MediaController's most recently reported playback state is not equal to new playback state // then queue a task to fire a simple event at the MediaController object, whose name is playing // if new playback state is playing, ended if new playback state is ended, and waiting otherwise. AtomString eventName; switch (newPlaybackState) { case WAITING: eventName = eventNames().waitingEvent; m_clock->stop(); m_timeupdateTimer.stop(); break; case ENDED: eventName = eventNames().endedEvent; m_resetCurrentTimeInNextPlay = true; m_clock->stop(); m_timeupdateTimer.stop(); break; case PLAYING: if (m_resetCurrentTimeInNextPlay) { m_resetCurrentTimeInNextPlay = false; m_clock->setCurrentTime(0); } eventName = eventNames().playingEvent; m_clock->start(); startTimeupdateTimer(); break; default: ASSERT_NOT_REACHED(); } scheduleEvent(eventName); // Let the MediaController's most recently reported playback state be new playback state. m_playbackState = newPlaybackState; updateMediaElements(); } void MediaController::updateMediaElements() { for (auto& mediaElement : m_mediaElements) mediaElement->updatePlayState(); } void MediaController::bringElementUpToSpeed(HTMLMediaElement& element) { ASSERT(m_mediaElements.contains(&element)); // When the user agent is to bring a media element up to speed with its new media controller, // it must seek that media element to the MediaController's media controller position relative // to the media element's timeline. element.seekInternal(MediaTime::createWithDouble(currentTime())); } bool MediaController::isBlocked() const { // A MediaController is a blocked media controller if the MediaController is a paused media // controller, if (m_paused) return true; if (m_mediaElements.isEmpty()) return false; bool allPaused = true; for (auto& element : m_mediaElements) { // or if any of its mediagroup elements are blocked media elements, if (element->isBlocked()) return true; // or if any of its mediagroup elements whose autoplaying flag is true still have their // paused attribute set to true, if (element->isAutoplaying() && element->paused()) return true; if (!element->paused()) allPaused = false; } // or if all of its mediagroup elements have their paused attribute set to true. return allPaused; } bool MediaController::hasEnded() const { // If the ... media controller playback rate is positive or zero if (m_clock->playRate() < 0) return false; // [and] all of the MediaController's mediagroup elements have ended playback ... let new // playback state be ended. if (m_mediaElements.isEmpty()) return false; bool allHaveEnded = true; for (auto& mediaElement : m_mediaElements) { if (!mediaElement->ended()) allHaveEnded = false; } return allHaveEnded; } void MediaController::scheduleEvent(const AtomString& eventName) { m_pendingEvents.append(Event::create(eventName, Event::CanBubble::No, Event::IsCancelable::Yes)); if (!m_asyncEventTimer.isActive()) m_asyncEventTimer.startOneShot(0_s); } void MediaController::asyncEventTimerFired() { Vector> pendingEvents; m_pendingEvents.swap(pendingEvents); for (auto& pendingEvent : pendingEvents) dispatchEvent(pendingEvent); } void MediaController::clearPositionTimerFired() { m_position = MediaPlayer::invalidTime(); } bool MediaController::hasAudio() const { for (auto& mediaElement : m_mediaElements) { if (mediaElement->hasAudio()) return true; } return false; } bool MediaController::hasVideo() const { for (auto& mediaElement : m_mediaElements) { if (mediaElement->hasVideo()) return true; } return false; } bool MediaController::hasClosedCaptions() const { for (auto& mediaElement : m_mediaElements) { if (mediaElement->hasClosedCaptions()) return true; } return false; } void MediaController::setClosedCaptionsVisible(bool visible) { m_closedCaptionsVisible = visible; for (auto& mediaElement : m_mediaElements) mediaElement->setClosedCaptionsVisible(visible); } bool MediaController::supportsScanning() const { for (auto& mediaElement : m_mediaElements) { if (!mediaElement->supportsScanning()) return false; } return true; } void MediaController::beginScrubbing() { for (auto& mediaElement : m_mediaElements) mediaElement->beginScrubbing(); if (m_playbackState == PLAYING) m_clock->stop(); } void MediaController::endScrubbing() { for (auto& mediaElement : m_mediaElements) mediaElement->endScrubbing(); if (m_playbackState == PLAYING) m_clock->start(); } void MediaController::beginScanning(ScanDirection direction) { for (auto& mediaElement : m_mediaElements) mediaElement->beginScanning(direction); } void MediaController::endScanning() { for (auto& mediaElement : m_mediaElements) mediaElement->endScanning(); } bool MediaController::canPlay() const { if (m_paused) return true; for (auto& mediaElement : m_mediaElements) { if (!mediaElement->canPlay()) return false; } return true; } bool MediaController::isLiveStream() const { for (auto& mediaElement : m_mediaElements) { if (!mediaElement->isLiveStream()) return false; } return true; } bool MediaController::hasCurrentSrc() const { for (auto& mediaElement : m_mediaElements) { if (!mediaElement->hasCurrentSrc()) return false; } return true; } void MediaController::returnToRealtime() { for (auto& mediaElement : m_mediaElements) mediaElement->returnToRealtime(); } // The spec says to fire periodic timeupdate events (those sent while playing) every // "15 to 250ms", we choose the slowest frequency static const Seconds maxTimeupdateEventFrequency { 250_ms }; void MediaController::startTimeupdateTimer() { if (m_timeupdateTimer.isActive()) return; m_timeupdateTimer.startRepeating(maxTimeupdateEventFrequency); } void MediaController::scheduleTimeupdateEvent() { MonotonicTime now = MonotonicTime::now(); Seconds timedelta = now - m_previousTimeupdateTime; if (timedelta < maxTimeupdateEventFrequency) return; scheduleEvent(eventNames().timeupdateEvent); m_previousTimeupdateTime = now; } } // namespace WebCore #endif