text: /*
   * 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"
  
  #if ENABLE(VIDEO)
  #include "MediaController.h"
  
  #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
  