| /* |
| * 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 <pal/system/Clock.h> |
| #include <wtf/IsoMallocInlines.h> |
| #include <wtf/NeverDestroyed.h> |
| #include <wtf/StdLibExtras.h> |
| #include <wtf/text/AtomString.h> |
| |
| namespace WebCore { |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(MediaController); |
| |
| Ref<MediaController> 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<TimeRanges> 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<TimeRanges> bufferedRanges = m_mediaElements.first()->buffered(); |
| for (size_t index = 1; index < m_mediaElements.size(); ++index) |
| bufferedRanges->intersectWith(m_mediaElements[index]->buffered()); |
| return bufferedRanges; |
| } |
| |
| Ref<TimeRanges> 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<TimeRanges> seekableRanges = m_mediaElements.first()->seekable(); |
| for (size_t index = 1; index < m_mediaElements.size(); ++index) |
| seekableRanges->intersectWith(m_mediaElements[index]->seekable()); |
| return seekableRanges; |
| } |
| |
| Ref<TimeRanges> 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<TimeRanges> 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<double>(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<void> 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<const AtomString> waiting("waiting", AtomString::ConstructFromLiteral); |
| return waiting; |
| } |
| |
| static const AtomString& playbackStatePlaying() |
| { |
| static MainThreadNeverDestroyed<const AtomString> playing("playing", AtomString::ConstructFromLiteral); |
| return playing; |
| } |
| |
| static const AtomString& playbackStateEnded() |
| { |
| static MainThreadNeverDestroyed<const AtomString> 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<ReadyState>(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<Ref<Event>> 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 |