/**
 * Provides Sandstone-themed video player components.
 *
 * @module sandstone/VideoPlayer
 * @exports Video
 * @exports VideoPlayer
 * @exports VideoPlayerBase
 */

import React from "react";
import ReactDOM from "react-dom";

import classNames from "classnames";
import DurationFmt from "ilib/lib/DurationFmt";
import PropTypes from "prop-types";
import shallowEqual from "recompose/shallowEqual";

import { off, on } from "@enact/core/dispatcher";
import {
  adaptEvent,
  call,
  forKey,
  forward,
  forwardWithPrevent,
  handle,
  preventDefault,
  returnsTrue,
  stopImmediate,
} from "@enact/core/handle";
import ApiDecorator from "@enact/core/internal/ApiDecorator";
import EnactPropTypes from "@enact/core/internal/prop-types";
import { is } from "@enact/core/keymap";
import { platform } from "@enact/core/platform";
import { Job, memoize, perfNow } from "@enact/core/util";
import { I18nContextDecorator } from "@enact/i18n/I18nDecorator";
import { toUpperCase } from "@enact/i18n/util";
import Skinnable from "@enact/sandstone/Skinnable";
import { getDirection, Spotlight } from "@enact/spotlight";
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
import { Spottable } from "@enact/spotlight/Spottable";
import Announce from "@enact/ui/AnnounceDecorator/Announce";
import ComponentOverride from "@enact/ui/ComponentOverride";
import { FloatingLayerDecorator } from "@enact/ui/FloatingLayer";
import { FloatingLayerContext } from "@enact/ui/FloatingLayer/FloatingLayerDecorator";
import Marquee from "@enact/ui/Marquee";
import Slottable from "@enact/ui/Slottable";
import Touchable from "@enact/ui/Touchable";

import { panel_names } from "../../utils/Config";
import { $L } from "../../utils/helperMethods";
import { SpotlightIds } from "../../utils/SpotlightIds";
import ThemeIndicator from "../../views/DetailPanel/components/indicator/ThemeIndicator";
import ThemeIndicatorArrow from "../../views/DetailPanel/components/indicator/ThemeIndicatorArrow";
import PlayerOverlayContents from "../../views/PlayerPanel/PlayerOverlay/PlayerOverlayContents";
import PlayerOverlayQRCode from "../../views/PlayerPanel/PlayerOverlay/PlayerOverlayQRCode";
import Loader from "../Loader/Loader";
import {
  MediaControls,
  MediaSlider,
  secondsToTime,
  Times,
} from "../MediaPlayer";
import VideoOverlayWithPhoneNumber from "../VideoOverlayWithPhoneNumber/VideoOverlayWithPhoneNumber";
import FeedbackContent from "./FeedbackContent";
import FeedbackTooltip from "./FeedbackTooltip";
import Media from "./Media";
import MediaTitle from "./MediaTitle";
import Overlay from "./Overlay";
import TReactPlayer from "./TReactPlayer";
import Video from "./Video";
import css from "./VideoPlayer.module.less";

const isEnter = is("enter");
const isLeft = is("left");
const isRight = is("right");

const jumpBackKeyCode = 37;
const jumpForwardKeyCode = 39;
const controlsHandleAboveSelectionKeys = [
  13,
  16777221,
  jumpBackKeyCode,
  jumpForwardKeyCode,
];
const getControlsHandleAboveHoldConfig = ({ frequency, time }) => ({
  events: [{ name: "hold", time }],
  frequency,
});
const shouldJump = (
  { disabled, no5WayJump },
  { mediaControlsVisible, sourceUnavailable }
) => !no5WayJump && !mediaControlsVisible && !(disabled || sourceUnavailable);
const calcNumberValueOfPlaybackRate = (rate) => {
  const pbArray = String(rate).split("/");
  return pbArray.length > 1
    ? parseInt(pbArray[0]) / parseInt(pbArray[1])
    : parseInt(rate);
};
const SpottableBtn = Spottable("button");
const SpottableDiv = Touchable(Spottable("div"));
const RootContainer = SpotlightContainerDecorator(
  {
    enterTo: "default-element",
    defaultElement: [`.${css.controlsHandleAbove}`, `.${css.controlsFrame}`],
  },
  "div"
);

const ControlsContainer = SpotlightContainerDecorator(
  {
    enterTo: "",
    straightOnly: true,
  },
  "div"
);

const memoGetDurFmt = memoize(
  (/* locale */) =>
    new DurationFmt({
      length: "medium",
      style: "clock",
      useNative: false,
    })
);

const getDurFmt = (locale) => {
  if (typeof window === "undefined") return null;

  return memoGetDurFmt(locale);
};

const forwardWithState = (type) =>
  adaptEvent(call("addStateToEvent"), forwardWithPrevent(type));

const forwardToggleMore = forward("onToggleMore");

// provide forwarding of events on media controls
const forwardControlsAvailable = forward("onControlsAvailable");
const forwardPlay = forwardWithState("onPlay");
const forwardPause = forwardWithState("onPause");
const forwardRewind = forwardWithState("onRewind");
const forwardFastForward = forwardWithState("onFastForward");
const forwardJumpBackward = forwardWithState("onJumpBackward");
const forwardJumpForward = forwardWithState("onJumpForward");

const AnnounceState = {
  // Video is loaded but additional announcements have not been made
  READY: 0,

  // The title should be announced
  TITLE: 1,

  // The title has been announce
  TITLE_READ: 2,

  // The infoComponents should be announce
  INFO: 3,

  // All announcements have been made
  DONE: 4,
};

/**
 * Every callback sent by [VideoPlayer]{@link sandstone/VideoPlayer} receives a status package,
 * which includes an object with the following key/value pairs as the first argument:
 *
 * @typedef {Object} videoStatus
 * @memberof sandstone/VideoPlayer
 * @property {String} type - Type of event that triggered this callback
 * @property {Number} currentTime - Playback index of the media in seconds
 * @property {Number} duration - Media's entire duration in seconds
 * @property {Boolean} paused - Playing vs paused state. `true` means the media is paused
 * @property {Number} playbackRate - Current playback rate, as a number
 * @property {Number} proportionLoaded - A value between `0` and `1` representing the proportion of the media that has loaded
 * @property {Number} proportionPlayed - A value between `0` and `1` representing the proportion of the media that has already been shown
 *
 * @public
 */

/**
 * A set of playback rates when media fast forwards, rewinds, slow-forwards, or slow-rewinds.
 *
 * The number used for each operation is proportional to the normal playing speed, 1. If the rate
 * is less than 1, it will play slower than normal speed, and, if it is larger than 1, it will play
 * faster. If it is negative, it will play backward.
 *
 * The order of numbers represents the incremental order of rates that will be used for each
 * operation. Note that all rates are expressed as strings and fractions are used rather than decimals
 * (e.g.: `'1/2'`, not `'0.5'`).
 *
 * @typedef {Object} playbackRateHash
 * @memberof sandstone/VideoPlayer
 * @property {String[]} fastForward - An array of playback rates when media fast forwards
 * @property {String[]} rewind - An array of playback rates when media rewinds
 * @property {String[]} slowForward - An array of playback rates when media slow-forwards
 * @property {String[]} slowRewind - An array of playback rates when media slow-rewinds
 *
 * @public
 */

/**
 * A player for video {@link sandstone/VideoPlayer.VideoPlayerBase}.
 *
 * @class VideoPlayerBase
 * @memberof sandstone/VideoPlayer
 * @ui
 * @public
 */
const VideoPlayerBase = class extends React.Component {
  static displayName = "VideoPlayerBase";

  static propTypes =
    /** @lends sandstone/VideoPlayer.VideoPlayerBase.prototype */ {
      /**
       * passed by AnnounceDecorator for accessibility
       *
       * @type {Function}
       * @private
       */
      announce: PropTypes.func,

      /**
       * The time (in milliseconds) before the control buttons will hide.
       *
       * Setting this to 0 or `null` disables closing, requiring user input to open and close.
       *
       * @type {Number}
       * @default 5000
       * @public
       */
      autoCloseTimeout: PropTypes.number,

      /**
       * Removes interactive capability from this component. This includes, but is not limited to,
       * key-press events, most clickable buttons, and prevents the showing of the controls.
       *
       * @type {Boolean}
       * @public
       */
      disabled: PropTypes.bool,

      /**
       * Amount of time (in milliseconds) after which the feedback text/icon part of the slider's
       * tooltip will automatically hidden after the last action.
       * Setting this to 0 or `null` disables feedbackHideDelay; feedback will always be present.
       *
       * @type {Number}
       * @default 3000
       * @public
       */
      feedbackHideDelay: PropTypes.number,

      /**
       * Components placed below the title.
       *
       * Typically these will be media descriptor icons, like how many audio channels, what codec
       * the video uses, but can also be a description for the video or anything else that seems
       * appropriate to provide information about the video to the user.
       *
       * @type {Node}
       * @public
       */
      infoComponents: PropTypes.node,
      backButton: PropTypes.node,
      promotionTitle: PropTypes.string,
      /**
       * The number of milliseconds that the player will pause before firing the
       * first jump event on a right or left pulse.
       *
       * @type {Number}
       * @default 400
       * @public
       */
      initialJumpDelay: PropTypes.number,

      /**
       * The number of seconds the player should skip forward or backward when a "jump" button is
       * pressed.
       *
       * @type {Number}
       * @default 30
       * @public
       */
      jumpBy: PropTypes.number,

      /**
       * The number of milliseconds that the player will throttle before firing a
       * jump event on a right or left pulse.
       *
       * @type {Number}
       * @default 200
       * @public
       */
      jumpDelay: PropTypes.number,

      /**
       * Manually set the loading state of the media, in case you have information that
       * `VideoPlayer` does not have.
       *
       * @type {Boolean}
       * @public
       */
      loading: PropTypes.bool,

      /**
       * The current locale as a
       * {@link https://tools.ietf.org/html/rfc5646|BCP 47 language tag}.
       *
       * @type {String}
       * @public
       */
      locale: PropTypes.string,

      /**
       * Overrides the default media control component to support customized behaviors.
       *
       * The provided component will receive the following props from `VideoPlayer`:
       *
       * * `initialJumpDelay` - Time (in ms) to wait before starting a jump
       * * `jumpDelay` -  - Time (in ms) to wait between jumps
       * * `mediaDisabled` - `true` when the media controls are not interactive
       * * `no5WayJump` - `true` when 5-way jumping is disabled
       * * `onClose` - Called when cancel key is pressed when the media controls are visible
       * * `onFastForward` - Called when the media is fast forwarded via a key event
       * * `onJump` - Called when the media jumps either forward or backward
       * * `onJumpBackwardButtonClick` - Called when the jump backward button is pressed
       * * `onJumpForwardButtonClick` - Called when the jump forward button is pressed
       * * `onKeyDown` - Called when a key is pressed
       * * `onPause` - Called when the media is paused via a key event
       * * `onPlay` - Called when the media is played via a key event
       * * `onRewind` - Called when the media is rewound via a key event
       * * `onToggleMore` - Called when the more components are hidden or shown
       * * `paused` - `true` when the media is paused
       * * `spotlightId` - The spotlight container Id for the media controls
       * * `spotlightDisabled` - `true` when spotlight is disabled for the media controls
       * * `visible` - `true` when the media controls should be displayed
       *
       * @type {Component|Element}
       * @default sandstone/MediaPlayer.MediaControls
       * @public
       */
      mediaControlsComponent: EnactPropTypes.componentOverride,

      /**
       * Amount of time (in milliseconds), after the last user action, that the `miniFeedback`
       * will automatically hide.
       * Setting this to 0 or `null` disables `miniFeedbackHideDelay`; `miniFeedback` will always
       * be present.
       *
       * @type {Number}
       * @default 2000
       * @public
       */
      miniFeedbackHideDelay: PropTypes.number,

      /**
       * Disable audio for this video.
       *
       * In a TV context, this is handled by the remote control, not programmatically in the
       * VideoPlayer API.
       *
       * @type {Boolean}
       * @default false
       * @public
       */
      muted: PropTypes.bool,

      /**
       * Prevents the default behavior of using left and right keys for seeking.
       *
       * @type {Boolean}
       * @public
       */
      no5WayJump: PropTypes.bool,

      /**
       * Prevents the default behavior of playing a video immediately after it's loaded.
       *
       * @type {Boolean}
       * @default false
       * @public
       */
      noAutoPlay: PropTypes.bool,

      /**
       * Prevents the default behavior of showing media controls immediately after it's loaded.
       *
       * @type {Boolean}
       * @default false
       * @public
       */
      noAutoShowMediaControls: PropTypes.bool,

      /**
       * Hides media slider feedback when fast forward or rewind while media controls are hidden.
       *
       * @type {Boolean}
       * @default false
       * @public
       */
      noMediaSliderFeedback: PropTypes.bool,

      /**
       * Removes the mini feedback.
       *
       * @type {Boolean}
       * @default false
       * @public
       */
      noMiniFeedback: PropTypes.bool,

      /**
       * Removes the media slider.
       *
       * @type {Boolean}
       * @default false
       * @public
       */
      noSlider: PropTypes.bool,

      /**
       * Removes spinner while loading.
       *
       * @type {Boolean}
       * @public
       */
      noSpinner: PropTypes.bool,

      /**
       * Called when the player's controls change availability, whether they are shown
       * or hidden.
       *
       * The current status is sent as the first argument in an object with a key `available`
       * which will be either `true` or `false`. (e.g.: `onControlsAvailable({available: true})`)
       *
       * @type {Function}
       * @public
       */
      onControlsAvailable: PropTypes.func,

      /**
       * Called when the video is fast forwarded.
       *
       * @type {Function}
       * @public
       */
      onFastForward: PropTypes.func,

      /**
       * Called when the user clicks the JumpBackward button.
       *
       * Is passed a {@link sandstone/VideoPlayer.videoStatus} as the first argument.
       *
       * @type {Function}
       * @public
       */
      onJumpBackward: PropTypes.func,

      /**
       * Called when the user clicks the JumpForward button.
       *
       * Is passed a {@link sandstone/VideoPlayer.videoStatus} as the first argument.
       *
       * @type {Function}
       * @public
       */
      onJumpForward: PropTypes.func,

      /**
       * Called when video is paused
       *
       * @type {Function}
       * @public
       */
      onPause: PropTypes.func,

      /**
       * Called when video is played
       *
       * @type {Function}
       * @public
       */
      onPlay: PropTypes.func,

      /**
       * Called when video is rewound.
       *
       * @type {Function}
       * @public
       */
      onRewind: PropTypes.func,

      /**
       * Called when the user is moving the VideoPlayer's Slider knob independently of
       * the current playback position.
       *
       * It is passed an object with a `seconds` key (float value) to indicate the current time
       * index. It can be used to update the `thumbnailSrc` to the reflect the current scrub
       * position.
       *
       * @type {Function}
       * @public
       */
      onScrub: PropTypes.func,

      /**
       * Called when seek is attempted while `seekDisabled` is true.
       *
       * @type {Function}
       */
      onSeekFailed: PropTypes.func,

      /**
       * Called when seeking outside of the current `selection` range.
       *
       * By default, the seek will still be performed. Calling `preventDefault()` on the event
       * will prevent the seek operation.
       *
       * @type {Function}
       * @public
       */
      onSeekOutsideSelection: PropTypes.func,

      /**
       * Called when the visibility of more components is changed
       *
       * Event payload includes:
       *
       * * `type` - Type of event, `'onToggleMore'`
       * * `showMoreComponents` - `true` when the components are visible`
       * * `liftDistance` - The distance, in pixels, the component animates
       *`
       * @type {Function}
       * @public
       */
      onToggleMore: PropTypes.func,

      /**
       * Pauses the video when it reaches either the start or the end of the video during rewind,
       * slow rewind, fast forward, or slow forward.
       *
       * @type {Boolean}
       * @default false
       * @public
       */
      pauseAtEnd: PropTypes.bool,

      /**
       * Mapping of playback rate names to playback rate values that may be set.
       *
       * @type {sandstone/VideoPlayer.playbackRateHash}
       * @default {
       *	fastForward: ['2', '4', '8', '16'],
       *	rewind: ['-2', '-4', '-8', '-16'],
       *	slowForward: ['1/4', '1/2'],
       *	slowRewind: ['-1/2', '-1']
       * }
       * @public
       */
      playbackRateHash: PropTypes.shape({
        fastForward: PropTypes.arrayOf(PropTypes.string),
        rewind: PropTypes.arrayOf(PropTypes.string),
        slowForward: PropTypes.arrayOf(PropTypes.string),
        slowRewind: PropTypes.arrayOf(PropTypes.string),
      }),

      /**
       * Disables seek function.
       *
       * Note that jump by arrow keys will also be disabled when `true`.
       *
       * @type {Boolean}
       * @public
       */
      seekDisabled: PropTypes.bool,

      /**
       * A range of the video to display as selected.
       *
       * The value of `selection` may either be:
       * * `null` or `undefined` for no selection,
       * * a single-element array with the start time of the selection
       * * a two-element array containing both the start and end time of the selection in seconds
       *
       * When the start time is specified, the media slider will show filled starting at that
       * time to the current time.
       *
       * When the end time is specified, the slider's background will be filled between the two
       * times.
       *
       * @type {Number[]}
       * @public
       */
      selection: PropTypes.arrayOf(PropTypes.number),

      /**
       * Registers the VideoPlayer component with an
       * {@link core/internal/ApiDecorator.ApiDecorator}.
       *
       * @type {Function}
       * @private
       */
      setApiProvider: PropTypes.func,

      /**
       * The video source.
       *
       * Any children `<source>` tag elements of [VideoPlayer]{@link sandstone/VideoPlayer} will
       * be sent directly to the `videoComponent` as video sources.
       *
       * @type {Node}
       * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source
       * @public
       */
      source: PropTypes.node,
      track: PropTypes.node,

      /**
       * Disables spotlight navigation into the component.
       *
       * @type {Boolean}
       * @public
       */
      spotlightDisabled: PropTypes.bool,

      /**
       * The spotlight container ID for the player.
       *
       * @type {String}
       * @public
       * @default 'videoPlayer'
       */
      spotlightId: PropTypes.string,

      /**
       * The thumbnail component to be used instead of the built-in version.
       *
       * The internal thumbnail style will not be applied to this component. This component
       * follows the same rules as the built-in version.
       *
       * @type {String|Component|Element}
       * @public
       */
      thumbnailComponent: EnactPropTypes.renderableOverride,

      /**
       * Thumbnail image source to show on the slider knob.
       *
       * This is a standard {@link sandstone/Image} component so it supports all of the same
       * options for the `src` property. If no `thumbnailComponent` and no `thumbnailSrc` is set,
       * no tooltip will display.
       *
       * @type {String|Object}
       * @public
       */
      thumbnailSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),

      /**
       * Enables the thumbnail transition from opaque to translucent.
       *
       * @type {Boolean}
       * @public
       */
      thumbnailUnavailable: PropTypes.bool,

      /**
       * Title for the video being played.
       *
       * @type {String|Node}
       * @public
       */
      title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),

      /**
       * The time (in milliseconds) before the title disappears from the controls.
       *
       * Setting this to `0` disables hiding.
       *
       * @type {Number}
       * @default 5000
       * @public
       */
      titleHideDelay: PropTypes.number,

      /**
       * Video component to use.
       *
       * The default renders an `HTMLVideoElement`. Custom video components must have a similar
       * API structure, exposing the following APIs:
       *
       * Properties:
       * * `currentTime` {Number} - Playback index of the media in seconds
       * * `duration` {Number} - Media's entire duration in seconds
       * * `error` {Boolean} - `true` if video playback has errored.
       * * `loading` {Boolean} - `true` if video playback is loading.
       * * `paused` {Boolean} - Playing vs paused state. `true` means the media is paused
       * * `playbackRate` {Number} - Current playback rate, as a number
       * * `proportionLoaded` {Number} - A value between `0` and `1`
       *	representing the proportion of the media that has loaded
       * * `proportionPlayed` {Number} - A value between `0` and `1` representing the
       *	proportion of the media that has already been shown
       *
       * Events:
       * * `onLoadStart` - Called when the video starts to load
       * * `onUpdate` - Sent when any of the properties were updated
       *
       * Methods:
       * * `play()` - play video
       * * `pause()` - pause video
       * * `load()` - load video
       *
       * The [`source`]{@link sandstone/VideoPlayer.Video.source} property is passed to
       * the video component as a child node.
       *
       * @type {Component|Element}
       * @default {@link ui/Media.Media}
       * @public
       */
      videoComponent: EnactPropTypes.componentOverride,

      introTime: PropTypes.number,
      onClickSkipIntro: PropTypes.func,
      onIntroDisabled: PropTypes.func,
      modalClassName: PropTypes.any,
      src: PropTypes.string, //for ReactPlayer
      reactPlayerConfig: PropTypes.any, //for ReactPlayer
      qrCurrentItem: PropTypes.any,
      modalScale: PropTypes.number,
    };

  static contextType = FloatingLayerContext;

  static defaultProps = {
    autoCloseTimeout: 3000,
    feedbackHideDelay: 3000,
    initialJumpDelay: 400,
    jumpBy: 30,
    jumpDelay: 200,
    mediaControlsComponent: MediaControls,
    miniFeedbackHideDelay: 2000,
    playbackRateHash: {
      fastForward: ["2", "4", "8", "16"],
      rewind: ["-2", "-4", "-8", "-16"],
      slowForward: ["1/4", "1/2"],
      slowRewind: ["-1/2", "-1"],
    },
    spotlightId: "videoPlayer",
    titleHideDelay: 5000,
    //ReactPlayer
    videoComponent:
      typeof window === "object" && !window.PalmSystem ? TReactPlayer : Media,
    width: "100%", //for ReactPlayer
    height: "100%", //for ReactPlayer
  };

  constructor(props) {
    super(props);

    // Internal State
    this.video = null;
    this.currentLiveTimeSeconds = props.currentLiveTimeSeconds;
    this.liveTotalTime = props.liveTotalTime;
    this.type = props.type;
    this.pulsedPlaybackRate = null;
    this.pulsedPlaybackState = null;
    this.prevCommand = props.noAutoPlay ? "pause" : "play";
    this.showMiniFeedback = false;
    this.speedIndex = 0;
    this.id = this.generateId();
    this.selectPlaybackRates("fastForward");
    this.sliderKnobProportion = 0;
    this.mediaControlsSpotlightId = props.spotlightId + "_mediaControls";
    this.jumpButtonPressed = null;

    // Re-render-necessary State
    this.state = {
      currentLiveTimeSeconds: props.currentLiveTimeSeconds,
      liveTotalTime: props.liveTotalTime,
      announce: AnnounceState.READY,
      currentTime: 0,
      duration: 0,
      error: false,
      loading: true,
      paused: props.noAutoPlay,
      playbackRate: 1,
      titleOffsetHeight: 0,
      bottomOffsetHeight: 0,
      lastFocusedTarget: "",
      // Non-standard state computed from properties
      bottomControlsRendered: false,
      feedbackAction: "idle",
      feedbackVisible: false,
      infoVisible: false,
      mediaControlsVisible: false,
      mediaSliderVisible: false,
      miniFeedbackVisible: false,
      proportionLoaded: 0,
      proportionPlayed: 0,
      sourceUnavailable: true,
      titleVisible: true,
      thumbnailUrl: null,
    };

    if (props.setApiProvider) {
      props.setApiProvider(this);
    }
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    if (
      nextProps.currentLiveTimeSeconds !== prevState.currentLiveTimeSeconds ||
      nextProps.liveTotalTime !== prevState.liveTotalTime
    ) {
      return {
        currentLiveTimeSeconds: nextProps.currentLiveTimeSeconds,
        liveTotalTime: nextProps.liveTotalTime,
      };
    }
    return null;
  }

  componentDidMount() {
    on("mousemove", this.activityDetected);
    if (platform.touch) {
      on("touchmove", this.activityDetected);
    }
    if (typeof document !== "undefined") {
      document.addEventListener("keydown", this.handleGlobalKeyDown, {
        capture: true,
      });
      document.addEventListener("wheel", this.activityDetected, {
        capture: true,
      });
    }
    this.startDelayedFeedbackHide();
    if (this.context && typeof this.context === "function") {
      this.floatingLayerController = this.context(() => {});
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (
      // Use shallow props compare instead of source comparison to support possible changes
      // from mediaComponent.
      shallowEqual(this.props, nextProps) &&
      !this.state.miniFeedbackVisible &&
      this.state.miniFeedbackVisible === nextState.miniFeedbackVisible &&
      !this.state.mediaSliderVisible &&
      this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
      this.state.loading === nextState.loading &&
      this.props.loading === nextProps.loading &&
      (this.state.currentTime !== nextState.currentTime ||
        this.state.proportionPlayed !== nextState.proportionPlayed ||
        this.state.sliderTooltipTime !== nextState.sliderTooltipTime)
    ) {
      return false;
    }

    return true;
  }

  componentDidUpdate(prevProps, prevState) {
    if (
      prevProps.currentLiveTimeSeconds !== this.props.currentLiveTimeSeconds
    ) {
      this.currentLiveTimeSeconds = this.props.currentLiveTimeSeconds;
    }
    if (prevProps.liveTotalTime !== this.props.liveTotalTime) {
      this.liveTotalTime = this.props.liveTotalTime;
    }
    if (prevProps.panelInfo.modal !== this.props.panelInfo.modal) {
      if (!this.props.panelInfo.modal) {
        this.showControls();
      }
    }
    if (
      (!this.state.mediaControlsVisible &&
        prevState.mediaControlsVisible !== this.state.mediaControlsVisible) ||
      (!this.state.mediaSliderVisible &&
        prevState.mediaSliderVisible !== this.state.mediaSliderVisible)
    ) {
      this.floatingLayerController.notify({ action: "closeAll" });
    }
    if (this.props.spotlightId !== prevProps.spotlightId) {
      this.mediaControlsSpotlightId = this.props.spotlightId + "_mediaControls";
    }
    if (this.state.mediaSliderVisible && !prevState.mediaSliderVisible) {
      forwardControlsAvailable({ available: true }, this.props);
    } else if (!this.state.mediaSliderVisible && prevState.mediaSliderVisible) {
      forwardControlsAvailable({ available: false }, this.props);
    }
    if (!this.state.mediaControlsVisible && prevState.mediaControlsVisible) {
      forwardControlsAvailable({ available: false }, this.props);
      this.stopAutoCloseTimeout();

      if (!this.props.spotlightDisabled) {
        // If last focused item were in the media controls or slider, we need to explicitly
        // blur the element when MediaControls hide. See ENYO-5648
        const current = Spotlight.getCurrent();
        const bottomControls =
          document && document.querySelector(`.${css.bottom}`);
        if (current && bottomControls && bottomControls.contains(current)) {
          current.blur();
        }

        // when in pointer mode, the focus call below will only update the last focused for
        // the video player and not set the active container to the video player which will
        // cause focus to land back on the media controls button when spotlight restores
        // focus.
        if (Spotlight.getPointerMode()) {
          Spotlight.setActiveContainer(this.props.spotlightId);
        }

        // Set focus to the hidden spottable control - maintaining focus on available spottable
        // controls, which prevents an additional 5-way attempt in order to re-show media controls
        Spotlight.focus(`.${css.controlsHandleAbove}`);
      }
    } else if (
      this.state.mediaControlsVisible &&
      !prevState.mediaControlsVisible
    ) {
      forwardControlsAvailable({ available: true }, this.props);
      this.startAutoCloseTimeout();

      if (!this.props.spotlightDisabled) {
        const current = Spotlight.getCurrent();
        if (!current || this.player.contains(current)) {
          // Set focus within media controls when they become visible.
          if (
            Spotlight.focus(SpotlightIds.PLAYER_SKIPINTRO) &&
            this.jumpButtonPressed === 0
          ) {
            this.jumpButtonPressed = null;
          } else if (
            !Spotlight.focus(SpotlightIds.PLAYER_SKIPINTRO) &&
            Spotlight.focus(this.mediaControlsSpotlightId) &&
            this.jumpButtonPressed === 0
          ) {
            this.jumpButtonPressed = null;
          }
        }
      }
    }
    if (
      !this.props.panelInfo.modal &&
      prevProps.sideContentsVisible !== this.props.sideContentsVisible
    ) {
      if (this.props.sideContentsVisible) {
        forwardControlsAvailable({ available: false }, this.props);
        this.stopAutoCloseTimeout();
      } else {
        forwardControlsAvailable({ available: true }, this.props);
        this.startAutoCloseTimeout();
      }
    }
    // Once video starts loading it queues bottom control render until idle
    if (
      this.state.bottomControlsRendered &&
      !prevState.bottomControlsRendered &&
      !this.state.mediaControlsVisible
    ) {
      this.showControls();
    }
    if (
      (prevProps.isSubtitleActive !== this.props.isSubtitleActive ||
        prevProps.captionEnable !== this.props.captionEnable) &&
      this.props.isYoutube &&
      this.video &&
      this.props.captionEnable
    ) {
      if (this.video.getOptions("captions").length > 0) {
        if (this.props.isSubtitleActive) {
          this.video?.loadModule("captions");
          this.video?.setOption("captions", "track", { languageCode: "en" });
        } else {
          this.video?.unloadModule("captions");
        }
      }
    }
    if (prevProps.thumbnailUrl !== this.props.thumbnailUrl) {
      this.setState({ thumbnailUrl: this.props.thumbnailUrl });
    }
    if (prevProps.src !== this.props.src && this.props.src) {
      this.setState({ loading: true });
    }
    //for old browser
    const progressbarNode = document.querySelector(`[role="progressbar"]`);
    if (progressbarNode) {
      const pBGNode = progressbarNode.childNodes?.[0]?.childNodes?.[0];
      const pProgressNode = progressbarNode.childNodes?.[0]?.childNodes?.[1];
      if (pBGNode && pProgressNode) {
        pBGNode.style.width = this.state.proportionLoaded * 100 + "%";
        pProgressNode.style.width = this.state.proportionPlayed * 100 + "%";
      }
    }
  }

  componentWillUnmount() {
    off("mousemove", this.activityDetected);
    if (platform.touch) {
      off("touchmove", this.activityDetected);
    }
    if (typeof document !== "undefined") {
      document.removeEventListener("keydown", this.handleGlobalKeyDown, {
        capture: true,
      });
      document.removeEventListener("wheel", this.activityDetected, {
        capture: true,
      });
    }
    this.stopRewindJob();
    this.stopAutoCloseTimeout();
    this.stopDelayedTitleHide();
    this.stopDelayedFeedbackHide();
    this.stopDelayedMiniFeedbackHide();
    this.announceJob.stop();
    this.renderBottomControl.stop();
    this.slider5WayPressJob.stop();
    if (this.floatingLayerController) {
      this.floatingLayerController.unregister();
    }
  }

  //
  // Internal Methods
  //

  announceJob = new Job(
    (msg, clear) => this.announceRef && this.announceRef.announce(msg, clear),
    200
  );

  announce = (msg, clear) => {
    this.announceJob.start(msg, clear);
  };

  activityDetected = () => {
    this.startAutoCloseTimeout();
  };

  startAutoCloseTimeout = () => {
    // If this.state.more is used as a reference for when this function should fire, timing for
    // detection of when "more" is pressed vs when the state is updated is mismatched. Using an
    // instance variable that's only set and used for this express purpose seems cleanest.
    if (this.props.autoCloseTimeout && !this.props.sideContentsVisible) {
      this.autoCloseJob.startAfter(this.props.autoCloseTimeout);
    }
  };

  stopAutoCloseTimeout = () => {
    this.autoCloseJob.stop();
  };

  generateId = () => {
    return Math.random().toString(36).substr(2, 8);
  };

  isTimeBeyondSelection(time) {
    const { selection } = this.props;

    // if selection isn't set or only contains the starting value, there isn't a valid selection
    // with which to test the time
    if (selection != null && selection.length >= 2) {
      const [start, end] = selection;

      return time > end || time < start;
    }

    return false;
  }

  preventTimeChange(time) {
    return (
      this.isTimeBeyondSelection(time) &&
      !forwardWithPrevent(
        "onSeekOutsideSelection",
        { type: "onSeekOutsideSelection", time },
        this.props
      )
    );
  }

  /**
   * If the announce state is either ready to read the title or ready to read info, advance the
   * state to "read".
   *
   * @returns {Boolean} Returns true to be used in event handlers
   * @private
   */
  markAnnounceRead = () => {
    if (this.state.announce === AnnounceState.TITLE) {
      this.setState({ announce: AnnounceState.TITLE_READ });
    } else if (this.state.announce === AnnounceState.INFO) {
      this.setState({ announce: AnnounceState.DONE });
    }

    return true;
  };

  /**
   * Shows media controls.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  showControls = () => {
    if (this.props.disabled) {
      return;
    }

    // this.startDelayedFeedbackHide();
    // this.startDelayedTitleHide();

    this.setState(({ announce }) => {
      if (announce === AnnounceState.READY) {
        // if we haven't read the title yet, do so this time
        announce = AnnounceState.TITLE;
      } else if (announce === AnnounceState.TITLE) {
        // if we have read the title, advance to INFO so title isn't read again
        announce = AnnounceState.TITLE_READ;
      }

      return {
        announce,
        bottomControlsRendered: true,
        // feedbackAction: "idle",
        feedbackVisible: true,
        mediaControlsVisible: true,
        mediaSliderVisible: true,
        miniFeedbackVisible: false,
        titleVisible: true,
      };
    });
  };

  /**
   * Hides media controls.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  hideControls = () => {
    if (this.state.mediaControlsVisible) {
      const current = Spotlight.getCurrent();

      if (current) {
        const lastFocusedTarget = current.getAttribute("data-spotlight-id");

        if (lastFocusedTarget !== this.state.lastFocusedTarget) {
          this.setState({ lastFocusedTarget: lastFocusedTarget });
        }
      }
    }

    this.stopDelayedFeedbackHide();
    this.stopDelayedMiniFeedbackHide();
    this.stopDelayedTitleHide();
    this.stopAutoCloseTimeout();
    this.setState({
      feedbackAction: "idle",
      feedbackVisible: false,
      mediaControlsVisible: false,
      mediaSliderVisible: false,
      miniFeedbackVisible: false,
      infoVisible: false,
    });
    this.markAnnounceRead();
  };

  /**
   * Toggles the media controls.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  toggleControls = () => {
    if (this.state.mediaControlsVisible) {
      this.hideControls();
    } else {
      this.showControls();
    }
  };

  doAutoClose = () => {
    this.stopDelayedFeedbackHide();
    this.stopDelayedTitleHide();
    this.setState(({ mediaSliderVisible, miniFeedbackVisible }) => ({
      feedbackVisible: false,
      mediaControlsVisible: false,
      mediaSliderVisible: mediaSliderVisible && miniFeedbackVisible,
      infoVisible: false,
    }));
    this.markAnnounceRead();
  };

  autoCloseJob = new Job(this.doAutoClose);

  startDelayedTitleHide = () => {
    if (this.props.titleHideDelay) {
      this.hideTitleJob.startAfter(this.props.titleHideDelay);
    }
  };

  stopDelayedTitleHide = () => {
    this.hideTitleJob.stop();
  };

  hideTitle = () => {
    this.setState({ titleVisible: false });
  };

  hideTitleJob = new Job(this.hideTitle);

  startDelayedFeedbackHide = () => {
    if (this.props.feedbackHideDelay) {
      this.hideFeedbackJob.startAfter(this.props.feedbackHideDelay);
    }
  };

  stopDelayedFeedbackHide = () => {
    this.hideFeedbackJob.stop();
  };

  showFeedback = () => {
    if (this.state.mediaControlsVisible) {
      this.setState({
        feedbackVisible: true,
      });
    } else {
      const shouldShowSlider =
        this.pulsedPlaybackState !== null ||
        calcNumberValueOfPlaybackRate(this.playbackRate) !== 1;

      if (
        this.showMiniFeedback &&
        (!this.state.miniFeedbackVisible ||
          this.state.mediaSliderVisible !== shouldShowSlider)
      ) {
        this.setState(({ loading, duration, error }) => ({
          mediaSliderVisible:
            shouldShowSlider && !this.props.noMediaSliderFeedback,
          miniFeedbackVisible: !(loading || !duration || error),
        }));
      }
    }
  };

  hideFeedback = () => {
    if (this.state.feedbackVisible && this.state.feedbackAction !== "focus") {
      this.setState({
        feedbackVisible: false,
        feedbackAction: "idle",
      });
    }
  };

  hideFeedbackJob = new Job(this.hideFeedback);

  startDelayedMiniFeedbackHide = (delay = this.props.miniFeedbackHideDelay) => {
    if (delay) {
      this.hideMiniFeedbackJob.startAfter(delay);
    }
  };

  stopDelayedMiniFeedbackHide = () => {
    this.hideMiniFeedbackJob.stop();
  };

  hideMiniFeedback = () => {
    if (this.state.miniFeedbackVisible) {
      this.showMiniFeedback = false;
      this.setState({
        mediaSliderVisible: false,
        miniFeedbackVisible: false,
      });
    }
  };

  hideMiniFeedbackJob = new Job(this.hideMiniFeedback);

  handle = handle.bind(this);

  showControlsFromPointer = () => {
    Spotlight.setPointerMode(false);
    this.showControls();
  };

  clearPulsedPlayback = () => {
    this.pulsedPlaybackRate = null;
    this.pulsedPlaybackState = null;
  };

  // only show mini feedback if playback controls are invoked by a key event
  shouldShowMiniFeedback = (ev) => {
    if (ev.type === "keyup") {
      this.showMiniFeedback = true;
    }
    return true;
  };

  handleLoadStart = () => {
    this.firstPlayReadFlag = true;
    this.prevCommand = this.props.noAutoPlay ? "pause" : "play";
    this.speedIndex = 0;
    this.setState({
      announce: AnnounceState.READY,
      currentTime: 0,
      sourceUnavailable: true,
      proportionPlayed: 0,
      proportionLoaded: 0,
    });

    if (!this.props.noAutoShowMediaControls) {
      if (!this.state.bottomControlsRendered) {
        this.renderBottomControl.idle();
      } else {
        this.showControls();
      }
    }
  };

  handlePlay = this.handle(forwardPlay, this.shouldShowMiniFeedback, () =>
    this.play()
  );

  handlePause = this.handle(forwardPause, this.shouldShowMiniFeedback, () =>
    this.pause()
  );

  handleRewind = this.handle(forwardRewind, this.shouldShowMiniFeedback, () =>
    this.rewind()
  );

  handleFastForward = this.handle(
    forwardFastForward,
    this.shouldShowMiniFeedback,
    () => this.fastForward()
  );

  handleJump = ({ keyCode, withControl }) => {
    if (this.props.seekDisabled) {
      forward("onSeekFailed", {}, this.props);
    } else {
      const jumpBy = (is("left", keyCode) ? -1 : 1) * this.props.jumpBy;
      const time = Math.min(
        this.state.duration,
        Math.max(0, this.state.currentTime + jumpBy)
      );

      if (this.preventTimeChange(time)) return;

      if (withControl) {
        this.jump(jumpBy);
      } else {
        this.showMiniFeedback = true;
        this.jump(jumpBy);
        this.announceJob.startAfter(
          500,
          secondsToTime(this.video.currentTime, getDurFmt(this.props.locale), {
            includeHour: true,
          })
        );
      }
    }
  };

  // new function
  // onClickLeftJump = (ev) => {
  //   ev.stopPropagation();
  //   this.handleJump({ keyCode: 37, withControl: true });
  // };

  // // new function
  // onClickRightJump = (ev) => {
  //   ev.stopPropagation();
  //   this.handleJump({ keyCode: 39, withControl: true });
  // };

  // // new function
  // SpotToRight = (ev) => {
  //   Spotlight.focus("jumpright");
  //   ev.stopPropagation();
  // };

  // // new function
  // SpotToLeft = (ev) => {
  //   Spotlight.focus("jumpleft");
  //   ev.stopPropagation();
  // };
  // new function
  //   SpotToTitle = (ev) => {
  //     Spotlight.focus(SpotlightIds.PLAYER_TITLE_LAYER);
  //     ev.stopPropagation();
  //   };
  //   SpotToTitle2 = (ev) => {
  //     Spotlight.focus(SpotlightIds.PLAYER_TITLE_LAYER);
  //     Spotlight.focus(SpotlightIds.PLAYER_SKIPINTRO);
  //     ev.stopPropagation();
  //   };

  handleGlobalKeyDown = this.handle(
    returnsTrue(this.activityDetected),
    forKey("down"),
    () =>
      !this.state.mediaControlsVisible &&
      !Spotlight.getCurrent() &&
      Spotlight.getPointerMode() &&
      !this.props.spotlightDisabled,
    preventDefault,
    stopImmediate,
    this.showControlsFromPointer
  );

  handleControlsHandleAboveHoldPulse = () => {
    if (shouldJump(this.props, this.state)) {
      this.handleJump({
        keyCode:
          this.jumpButtonPressed === -1 ? jumpBackKeyCode : jumpForwardKeyCode,
      });
    }
  };

  handleControlsHandleAboveKeyDown = ({ keyCode }) => {
    if (isEnter(keyCode)) {
      this.jumpButtonPressed = 0;
    } else if (isLeft(keyCode)) {
      this.jumpButtonPressed = -1;
    } else if (isRight(keyCode)) {
      this.jumpButtonPressed = 1;
    }
  };

  handleControlsHandleAboveKeyUp = ({ keyCode }) => {
    if (isEnter(keyCode) || isLeft(keyCode) || isRight(keyCode)) {
      this.jumpButtonPressed = null;
    }
  };

  handleControlsHandleAboveDown = () => {
    if (this.jumpButtonPressed === 0) {
      this.showControls();
    } else if (this.jumpButtonPressed === -1 || this.jumpButtonPressed === 1) {
      const keyCode =
        this.jumpButtonPressed === -1 ? jumpBackKeyCode : jumpForwardKeyCode;

      if (shouldJump(this.props, this.state)) {
        this.handleJump({ keyCode });
      } else {
        Spotlight.move(getDirection(keyCode));
      }
    }
  };

  //
  // Media Interaction Methods
  //
  handleEvent = (ev) => {
    const el = this.video;

    const updatedState = {
      // Standard media properties
      currentTime: 0,
      duration: 0,
      paused: false,
      playbackRate: 0,
      //  lastFocusedTarget: "",
      // Non-standard state computed from properties
      error: false,
      loading: true,
      // 지나온 바
      proportionLoaded: false,
      //벨류( 노브 )
      proportionPlayed: 0,
      sliderTooltipTime: 0,
      // note: `el.loading && this.state.sourceUnavailable == false` is equivalent to `oncanplaythrough`
      sourceUnavailable: false,
    };
    if (!el) {
      console.log("yhcho VideoPlayer no el ");
      updatedState.error = true;
      this.setState(updatedState);
      return;
    }

    this.type = this.props.type;

    updatedState.currentTime =
      this.type === "LIVE" ? this.state.currentLiveTimeSeconds : el.currentTime;

    updatedState.duration =
      this.type === "LIVE" ? this.state.liveTotalTime : el.duration;

    //last time error : for youtube
    if (this.props.isYoutube && ev.type === "onProgress") {
      if (updatedState.duration - updatedState.currentTime < 1.0) {
        updatedState.currentTime = updatedState.duration;
      }
      this.props.setCurrentTime(updatedState.currentTime);
    }
    const proportionPlayed =
      updatedState.currentTime / updatedState.duration || 0;
    updatedState.paused = el.playbackRate !== 1 || el.paused;
    updatedState.playbackRate = el.playbackRate;
    updatedState.error = el.error;
    updatedState.loading = el.loading;
    updatedState.proportionLoaded = el.proportionLoaded;
    updatedState.proportionPlayed =
      proportionPlayed || el.proportionPlayed || 0;
    updatedState.sliderTooltipTime = el.currentTime;
    updatedState.sourceUnavailable =
      (el.loading && this.state.sourceUnavailable) || el.error;

    // If there's an error, we're obviously not loading, no matter what the readyState is.

    if (updatedState.error) updatedState.loading = false;

    const isRewind =
      this.prevCommand === "rewind" || this.prevCommand === "slowRewind";
    const isForward =
      this.prevCommand === "fastForward" || this.prevCommand === "slowForward";
    if (
      this.props.pauseAtEnd &&
      ((el.currentTime === 0 && isRewind) ||
        (el.currentTime === el.duration && isForward))
    ) {
      this.pause();
    }
    if (
      this.props.isYoutube &&
      this.props.captionEnable &&
      this.props.isSubtitleActive &&
      ev?.type === "onStart" &&
      el.getOptions("captions").length > 0
    ) {
      el.loadModule("captions");
      el.setOption("captions", "track", { languageCode: "en" });
    }
    if (updatedState.loading) {
      updatedState.thumbnailUrl = this.props.thumbnailUrl;
    } else {
      updatedState.thumbnailUrl = null;
    }
    this.setState(updatedState);
  };

  renderBottomControl = new Job(() => {
    if (!this.state.bottomControlsRendered) {
      this.setState({ bottomControlsRendered: true });
    }
  });

  /**
   * Returns an object with the current state of the media including `currentTime`, `duration`,
   * `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @returns {Object}
   * @public
   */
  getMediaState = () => {
    return {
      currentTime: this.state.currentTime,
      duration: this.state.duration,
      paused: this.state.paused,
      playbackRate: this.video?.playbackRate,
      proportionLoaded: this.state.proportionLoaded,
      proportionPlayed: this.state.proportionPlayed,
    };
  };

  /**
   * The primary means of interacting with the `<video>` element.
   *
   * @param  {String} action The method to preform.
   * @param  {Multiple} props  The arguments, in the format that the action method requires.
   *
   * @private
   */
  send = (action, props) => {
    this.clearPulsedPlayback();
    this.showFeedback();
    this.startDelayedFeedbackHide();
    if (this.video) {
      this.video[action](props);
    }
  };

  /**
   * Programmatically plays the current media.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  play = () => {
    if (this.state.sourceUnavailable) {
      return;
    }

    this.speedIndex = 0;
    // must happen before send() to ensure feedback uses the right value
    // TODO: refactor into this.state member
    this.prevCommand = "play";
    this.setPlaybackRate(1);
    this.send("play");
    this.announce($L("Play"));
    this.startDelayedMiniFeedbackHide(5000);
  };

  /**
   * Programmatically plays the current media.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  pause = () => {
    if (this.state.sourceUnavailable) {
      return;
    }

    this.speedIndex = 0;
    // must happen before send() to ensure feedback uses the right value
    // TODO: refactor into this.state member
    this.prevCommand = "pause";
    this.setPlaybackRate(1);
    this.send("pause");
    this.announce($L("Pause"));
    this.stopDelayedMiniFeedbackHide();
  };

  /**
   * Set the media playback time index
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @param {Number} timeIndex - Time index to seek
   * @public
   */
  seek = (timeIndex) => {
    if (this.video) {
      if (
        !this.props.seekDisabled &&
        !isNaN(this.video.duration) &&
        !this.state.sourceUnavailable
      ) {
        // last time error
        if (timeIndex >= this.video.duration) {
          this.video.currentTime = this.video.duration - 1;
        } else {
          this.video.currentTime = timeIndex;
        }
      } else {
        forward("onSeekFailed", {}, this.props);
      }
    }
  };

  /**
   * Step a given amount of time away from the current playback position.
   * Like [seek]{@link sandstone/VideoPlayer.VideoPlayer#seek} but relative.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @param {Number} distance - Time value to jump
   * @public
   */
  jump = (distance) => {
    if (this.state.sourceUnavailable) {
      return;
    }

    this.pulsedPlaybackRate = toUpperCase(
      new DurationFmt({ length: "long" }).format({ second: this.props.jumpBy })
    );
    this.pulsedPlaybackState = distance > 0 ? "jumpForward" : "jumpBackward";
    this.showFeedback();
    this.startDelayedFeedbackHide();
    this.seek(this.state.currentTime + distance);
    this.startDelayedMiniFeedbackHide();
  };

  /**
   * Changes the playback speed via [selectPlaybackRate()]{@link sandstone/VideoPlayer.VideoPlayer#selectPlaybackRate}.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  fastForward = () => {
    if (this.state.sourceUnavailable) {
      return;
    }

    let shouldResumePlayback = false;

    switch (this.prevCommand) {
      case "slowForward":
        if (this.speedIndex === this.playbackRates.length - 1) {
          // reached to the end of array => fastforward
          this.selectPlaybackRates("fastForward");
          this.speedIndex = 0;
          this.prevCommand = "fastForward";
        } else {
          this.speedIndex = this.clampPlaybackRate(this.speedIndex + 1);
        }
        break;
      case "pause":
        this.selectPlaybackRates("slowForward");
        if (this.state.paused) {
          shouldResumePlayback = true;
        }
        this.speedIndex = 0;
        this.prevCommand = "slowForward";
        break;
      case "fastForward":
        this.speedIndex = this.clampPlaybackRate(this.speedIndex + 1);
        this.prevCommand = "fastForward";
        break;
      default:
        this.selectPlaybackRates("fastForward");
        this.speedIndex = 0;
        this.prevCommand = "fastForward";
        if (this.state.paused) {
          shouldResumePlayback = true;
        }
        break;
    }

    this.setPlaybackRate(this.selectPlaybackRate(this.speedIndex));

    if (shouldResumePlayback) this.send("play");

    this.stopDelayedFeedbackHide();
    this.stopDelayedMiniFeedbackHide();
    this.clearPulsedPlayback();
    this.showFeedback();
  };

  /**
   * Changes the playback speed via [selectPlaybackRate()]{@link sandstone/VideoPlayer.VideoPlayer#selectPlaybackRate}.
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  rewind = () => {
    if (this.state.sourceUnavailable) {
      return;
    }

    const rateForSlowRewind = this.props.playbackRateHash["slowRewind"];
    let shouldResumePlayback = false,
      command = "rewind";

    if (this.video.currentTime === 0) {
      // Do not rewind if currentTime is 0. We're already at the beginning.
      return;
    }
    switch (this.prevCommand) {
      case "slowRewind":
        if (this.speedIndex === this.playbackRates.length - 1) {
          // reached to the end of array => go to rewind
          this.selectPlaybackRates(command);
          this.speedIndex = 0;
          this.prevCommand = command;
        } else {
          this.speedIndex = this.clampPlaybackRate(this.speedIndex + 1);
        }
        break;
      case "pause":
        // If it's possible to slowRewind, do it, otherwise just leave it as normal rewind : QEVENTSEVT-17386
        if (rateForSlowRewind && rateForSlowRewind.length >= 0) {
          command = "slowRewind";
        }
        this.selectPlaybackRates(command);
        if (this.state.paused && this.state.duration > this.state.currentTime) {
          shouldResumePlayback = true;
        }
        this.speedIndex = 0;
        this.prevCommand = command;
        break;
      case "rewind":
        this.speedIndex = this.clampPlaybackRate(this.speedIndex + 1);
        this.prevCommand = command;
        break;
      default:
        this.selectPlaybackRates(command);
        this.speedIndex = 0;
        this.prevCommand = command;
        break;
    }

    this.setPlaybackRate(this.selectPlaybackRate(this.speedIndex));

    if (shouldResumePlayback) this.send("play");

    this.stopDelayedFeedbackHide();
    this.stopDelayedMiniFeedbackHide();
    this.clearPulsedPlayback();
    this.showFeedback();
  };

  // Creates a proxy to the video node if Proxy is supported
  videoProxy =
    typeof Proxy !== "function"
      ? null
      : new Proxy(
          {},
          {
            get: (target, name) => {
              let value = this.video[name];

              if (typeof value === "function") {
                value = value.bind(this.video);
              }

              return value;
            },
            set: (target, name, value) => {
              return (this.video[name] = value);
            },
          }
        );

  /**
   * Returns a proxy to the underlying `<video>` node currently used by the VideoPlayer
   *
   * @function
   * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
   * @public
   */
  getVideoNode = () => {
    return this.videoProxy || this.video;
  };

  areControlsVisible = () => {
    return this.state.mediaControlsVisible;
  };

  /**
   * Sets the playback rate type (from the keys of [playbackRateHash]{@link sandstone/VideoPlayer.VideoPlayer#playbackRateHash}).
   *
   * @param {String} cmd - Key of the playback rate type.
   * @private
   */
  selectPlaybackRates = (cmd) => {
    this.playbackRates = this.props.playbackRateHash[cmd];
  };

  /**
   * Changes [playbackRate]{@link sandstone/VideoPlayer.VideoPlayer#playbackRate} to a valid value
   * when initiating fast forward or rewind.
   *
   * @param {Number} idx - The index of the desired playback rate.
   * @private
   */
  clampPlaybackRate = (idx) => {
    if (!this.playbackRates) {
      return;
    }

    return idx % this.playbackRates.length;
  };

  /**
   * Retrieves the playback rate name.
   *
   * @param {Number} idx - The index of the desired playback rate.
   * @returns {String} The playback rate name.
   * @private
   */
  selectPlaybackRate = (idx) => {
    return this.playbackRates[idx];
  };

  /**
   * Sets [playbackRate]{@link sandstone/VideoPlayer.VideoPlayer#playbackRate}.
   *
   * @param {String} rate - The desired playback rate.
   * @private
   */
  setPlaybackRate = (rate) => {
    // Stop rewind (if happening)
    this.stopRewindJob();

    // Make sure rate is a string
    this.playbackRate = rate = String(rate);
    const pbNumber = calcNumberValueOfPlaybackRate(rate);

    if (!platform.webos) {
      // ReactDOM throws error for setting negative value for playbackRate
      if (this.video) {
        this.video.playbackRate = pbNumber < 0 ? 0 : pbNumber;
      }

      // For supporting cross browser behavior
      if (pbNumber < 0) {
        this.beginRewind();
      }
    } else {
      // Set native playback rate
      if (this.video) {
        this.video.playbackRate = pbNumber;
      }
    }
  };

  /**
   * Calculates the time that has elapsed since. This is necessary for browsers until negative
   * playback rate is directly supported.
   *
   * @private
   */
  rewindManually = () => {
    const now = perfNow(),
      distance = now - this.rewindBeginTime,
      pbRate = calcNumberValueOfPlaybackRate(this.playbackRate),
      adjustedDistance = (distance * pbRate) / 1000;

    this.jump(adjustedDistance);
    this.stopDelayedMiniFeedbackHide();
    this.clearPulsedPlayback();
    this.startRewindJob(); // Issue another rewind tick
  };

  rewindJob = new Job(this.rewindManually, 100);

  /**
   * Starts rewind job.
   *
   * @private
   */
  startRewindJob = () => {
    this.rewindBeginTime = perfNow();
    this.rewindJob.start();
  };

  /**
   * Stops rewind job.
   *
   * @private
   */
  stopRewindJob = () => {
    this.rewindJob.stop();
  };

  /**
   * Implements custom rewind functionality (until browsers support negative playback rate).
   *
   * @private
   */
  beginRewind = () => {
    this.send("pause");
    this.startRewindJob();
  };

  //
  // Handled Media events
  //
  addStateToEvent = (ev) => {
    return {
      // More props from `ev` may be added here as needed, but a full copy via `...ev`
      // overloads Storybook's Action Logger and likely has other perf fallout.
      type: ev.type,
      // Specific state variables are included in the outgoing calback payload, not all of them
      ...this.getMediaState(),
    };
  };

  disablePointerMode = () => {
    Spotlight.setPointerMode(false);
    return true;
  };

  //
  // Player Interaction events
  //
  onVideoClick = () => {
    this.toggleControls();
  };

  onSliderChange = ({ value }) => {
    const time = value * this.state.duration;

    if (this.preventTimeChange(time)) return;

    this.seek(time);
    this.sliderScrubbing = false;
  };

  handleKnobMove = (ev) => {
    this.sliderScrubbing = true;

    // prevent announcing repeatedly when the knob is detached from the progress.
    // TODO: fix Slider to not send onKnobMove when the knob hasn't, in fact, moved
    if (this.sliderKnobProportion !== ev.proportion && this.video) {
      this.sliderKnobProportion = ev.proportion;
      const seconds = Math.floor(
        this.sliderKnobProportion * this.video.duration
      );

      if (!isNaN(seconds)) {
        const knobTime = secondsToTime(seconds, getDurFmt(this.props.locale), {
          includeHour: true,
        });

        forward("onScrub", { ...ev, seconds }, this.props);

        this.announce(`${$L("jump to")} ${knobTime}`, true);
      }
    }
  };

  handleSliderFocus = () => {
    if (!this.video) {
      return;
    }
    const seconds = Math.floor(this.sliderKnobProportion * this.video.duration);
    this.sliderScrubbing = true;

    this.setState({
      feedbackAction: "focus",
      feedbackVisible: true,
    });
    this.stopDelayedFeedbackHide();

    if (!isNaN(seconds)) {
      const knobTime = secondsToTime(seconds, getDurFmt(this.props.locale), {
        includeHour: true,
      });

      forward(
        "onScrub",
        {
          detached: this.sliderScrubbing,
          proportion: this.sliderKnobProportion,
          seconds,
        },
        this.props
      );

      this.announce(`${$L("jump to")} ${knobTime}`, true);
    }
  };

  handleSliderBlur = () => {
    this.sliderScrubbing = false;
    this.startDelayedFeedbackHide();
    this.setState(() => ({
      feedbackAction: "blur",
      feedbackVisible: true,
    }));
  };

  slider5WayPressJob = new Job(() => {
    this.setState({ slider5WayPressed: false });
  }, 200);

  handleSliderKeyDown = (ev) => {
    const { keyCode } = ev;

    if (is("enter", keyCode)) {
      this.setState(
        {
          slider5WayPressed: true,
        },
        this.slider5WayPressJob.start()
      );
    } else if (is("down", keyCode)) {
      Spotlight.setPointerMode(false);

      if (Spotlight.focus(this.mediaControlsSpotlightId)) {
        preventDefault(ev);
        stopImmediate(ev);
      }
    } else if (is("up", keyCode)) {
      Spotlight.setPointerMode(false);
      let keyHandled = false;
      if (Spotlight.focus(SpotlightIds.PLAYER_TITLE_LAYER)) {
        keyHandled = true;
      }
      if (Spotlight.focus(SpotlightIds.PLAYER_SKIPINTRO)) {
        keyHandled = true;
      }
      if (keyHandled) {
        preventDefault(ev);
        stopImmediate(ev);
      }
    }
  };

  onJumpBackward = this.handle(forwardJumpBackward, () =>
    this.jump(-1 * this.props.jumpBy)
  );
  onJumpForward = this.handle(forwardJumpForward, () =>
    this.jump(this.props.jumpBy)
  );

  handleMediaControlsClose = (ev) => {
    this.hideControls();
    ev.stopPropagation();
  };

  setPlayerRef = (node) => {
    // TODO: We've moved SpotlightContainerDecorator up to allow VP to be spottable but also
    // need a ref to the root node to query for children and set CSS variables.
    // eslint-disable-next-line react/no-find-dom-node
    this.player = ReactDOM.findDOMNode(node);
  };

  setVideoRef = (video) => {
    this.video = video;
  };

  setTitleRef = (node) => {
    this.titleRef = node;
  };

  setAnnounceRef = (node) => {
    this.announceRef = node;
  };

  getControlsAriaProps() {
    if (this.state.announce === AnnounceState.TITLE) {
      return {
        "aria-labelledby": `${this.id}_mediaTitle_title ${this.id}_mediaControls_actionGuide`,
        "aria-live": "off",
        role: "alert",
      };
    } else if (this.state.announce === AnnounceState.INFO) {
      return {
        "aria-labelledby": `${this.id}_mediaTitle_info`,
        role: "region",
      };
    }

    return null;
  }

  render() {
    const {
      className,
      modalClassName,
      disabled,
      infoComponents,
      backButton,
      promotionTitle,
      initialJumpDelay,
      jumpDelay,

      locale,
      mediaControlsComponent,
      no5WayJump,
      noAutoPlay,
      noMiniFeedback,
      playListInfo,
      noSlider,
      noSpinner,
      selection,
      spotlightDisabled,
      spotlightId,
      style,
      thumbnailComponent,
      thumbnailSrc,
      title,
      introTime,
      onClickSkipIntro,
      onIntroDisabled,
      videoComponent: VideoComponent,
      cameraSettingsButton,
      onBackButton,
      panelInfo,
      selectedIndex,
      src,
      width,
      height,
      reactPlayerConfig,
      setIsSubtitleActive,
      thumbnailUrl,
      type,
      isYoutube,
      sideContentsVisible,
      setSideContentsVisible,
      disclaimer,
      liveTotalTime,
      currentLiveTimeSeconds,
      themeProductInfos,
      detailThemeProductImageLength,
      videoVerticalVisible,
      handleIndicatorDownClick,
      handleIndicatorUpClick,
      orderPhnNo,
      captionEnable,
      countryCode,
      setCurrentTime,
      setIsVODPaused,
      qrCurrentItem,
      modalScale,
      ...mediaProps
    } = this.props;

    delete mediaProps.announce;
    delete mediaProps.autoCloseTimeout;
    delete mediaProps.children;
    delete mediaProps.feedbackHideDelay;
    delete mediaProps.jumpBy;
    delete mediaProps.miniFeedbackHideDelay;
    delete mediaProps.noAutoShowMediaControls;
    delete mediaProps.noMediaSliderFeedback;
    delete mediaProps.onControlsAvailable;
    delete mediaProps.onFastForward;
    delete mediaProps.onJumpBackward;
    delete mediaProps.onJumpForward;
    delete mediaProps.onPause;
    delete mediaProps.onPlay;
    delete mediaProps.onRewind;
    delete mediaProps.onScrub;
    delete mediaProps.onSeekFailed;
    delete mediaProps.onSeekOutsideSelection;
    delete mediaProps.onToggleMore;
    delete mediaProps.pauseAtEnd;
    delete mediaProps.playbackRateHash;
    delete mediaProps.seekDisabled;
    delete mediaProps.setApiProvider;
    delete mediaProps.thumbnailUnavailable;
    delete mediaProps.titleHideDelay;
    delete mediaProps.videoPath;

    mediaProps.autoPlay = !noAutoPlay;
    mediaProps.className = type !== "MEDIA" ? css.video : css.media;
    mediaProps.controls = false;
    mediaProps.mediaComponent = "video";
    mediaProps.onLoadStart = this.handleLoadStart;
    mediaProps.onUpdate = this.handleEvent;
    mediaProps.ref = this.setVideoRef;

    //yhcho ReactPlayer
    if ((typeof window === "object" && !window.PalmSystem) || isYoutube) {
      mediaProps.url = src;
      mediaProps.playing = !noAutoPlay;
      mediaProps.width = width;
      mediaProps.height = height;
      mediaProps.videoRef = this.setVideoRef;
      mediaProps.config = reactPlayerConfig;
      delete mediaProps.mediaComponent;
      delete mediaProps.ref;
      // mediaProps.isYoutube = isYoutube;
    }

    const controlsAriaProps = this.getControlsAriaProps();

    let proportionSelection = selection;
    if (proportionSelection != null && this.state.duration) {
      proportionSelection = selection.map((t) => t / this.state.duration);
    }

    const durFmt = getDurFmt(locale);
    const controlsHandleAboveHoldConfig = getControlsHandleAboveHoldConfig({
      frequency: jumpDelay,
      time: initialJumpDelay,
    });
    let introPer = Math.round((this.state.currentTime / introTime) * 100);
    if (introTime <= 0) {
      introPer = -1;
    }
    if (introPer > 100) introPer = 100;

    //check is youtube support captions
    let _captionEnable = captionEnable;
    if (isYoutube) {
      if (
        !this.video ||
        (typeof this.video.getOptions === "function" &&
          this.video.getOptions("captions").length <= 0)
      ) {
        _captionEnable = false;
      }
    }

    const isQRCodeVisible =
      playListInfo && qrCurrentItem && !thumbnailUrl && !panelInfo.modal;

    const onSpotlightFocus = () => {
      this.showControls();

      if (this.state.lastFocusedTarget) {
        setTimeout(() => {
          Spotlight.focus(this.state.lastFocusedTarget);
        });
      } else {
        setTimeout(() => {
          Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
        });
      }
    };

    if (
      panelInfo?.shptmBanrTpNm === "VOD" ||
      panelInfo?.shptmBanrTpNm === "MEDIA"
    ) {
      setIsVODPaused(this.state.paused);
    }

    const getVideoPhoneNumberClassNames = () => {
      const isQVC =
        panelInfo?.chanId === "USQVC" ||
        panelInfo?.patnrNm === "QVC" ||
        panelInfo?.patncNm === "QVC";
      const isHSN =
        panelInfo?.chanId === "USHSN" ||
        panelInfo?.patnrNm === "HSN" ||
        panelInfo?.patncNm === "HSN";
      const showCallToOrderHide =
        panelInfo?.modal &&
        panelInfo?.hotPicksType &&
        ["TCFV_2", "TCFV_4"].includes(panelInfo.hotPicksType);
      const isVerticalModal = panelInfo?.modal && panelInfo?.isVerticalModal;

      return classNames(
        type === "MEDIA"
          ? css.videoOverlayMedia
          : css.videoOverlayWithPhoneNumber,
        countryCode === "RU" && css.ru,
        countryCode === "US" && css.us,
        videoVerticalVisible === "true" ? css.vertical : css.horizontal,
        isQVC && css.qvc,
        isHSN && css.hsn,
        isVerticalModal && css.verticalModal,
        showCallToOrderHide && css.callToOrderHide
      );
    };

    return (
      <RootContainer
        className={classNames(
          css.videoPlayer + " enact-fit" + (className ? " " + className : ""),
          modalClassName
        )}
        onClick={this.activityDetected}
        ref={this.setPlayerRef}
        spotlightDisabled={spotlightDisabled}
        spotlightId={spotlightId}
        style={style}
      >
        {/* Video Section */}
        {panelInfo.modal &&
          type === "MEDIA" &&
          themeProductInfos &&
          themeProductInfos.length > 0 && (
            <ThemeIndicatorArrow
              themeProductInfos={themeProductInfos}
              imageLength={detailThemeProductImageLength}
            />
          )}
        {panelInfo.modal && disclaimer && (
          <>
            <div className={css.disclaimer}>
              <span className={css.icon} />
              <h3 aria-label={disclaimer}>
                <Marquee className={css.marquee} marqueeOn="render">
                  {disclaimer}
                </Marquee>
              </h3>
            </div>
          </>
        )}
        {
          // Duplicating logic from <ComponentOverride /> until enzyme supports forwardRef
          (VideoComponent &&
            (((typeof VideoComponent === "function" ||
              typeof VideoComponent === "string") && (
              <VideoComponent {...mediaProps} />
            )) ||
              (React.isValidElement(VideoComponent) &&
                React.cloneElement(VideoComponent, mediaProps)))) ||
            null
        }
        {orderPhnNo && (
          <VideoOverlayWithPhoneNumber
            className={
              panelInfo?.modal
                ? getVideoPhoneNumberClassNames()
                : css.videoOverlayWithPhoneNumberFull
            }
            patnerName={
              panelInfo?.modal ? null : playListInfo?.[selectedIndex]?.patncNm
            }
            chanId={panelInfo?.modal ? null : panelInfo?.chanId}
            orderPhnNo={orderPhnNo}
            modal={panelInfo.modal}
          />
        )}
        {isQRCodeVisible && (
          <PlayerOverlayQRCode
            qrCurrentItem={qrCurrentItem}
            type={type}
            modalScale={modalScale}
          />
        )}
        <Overlay
          bottomControlsVisible={this.state.mediaControlsVisible}
          onClick={this.onVideoClick}
        >
          {!noSpinner && this.state.loading ? (
            <>
              {
                <p
                  className={classNames(
                    css.thumbnail,
                    videoVerticalVisible && css.verticalThumbnail,
                    !panelInfo.modal && css.noRadiusThumbnail,
                    panelInfo.modal && type !== "MEDIA" && css.smallThumbnail
                  )}
                >
                  <img src={thumbnailUrl} alt="" />
                </p>
              }

              <div className={css.loaderWrap}>
                <Loader />
              </div>
            </>
          ) : null}
          {this.state.mediaControlsVisible && (
            <div
              className={
                css.controlFeedbackBtnLayer +
                (this.state.infoVisible ? " " + css.lift : "")
              }
            >
              <PlayerOverlayContents
                playListInfo={playListInfo && playListInfo}
                selectedIndex={selectedIndex}
                onClick={onBackButton}
                panelInfo={panelInfo}
                setIsSubtitleActive={setIsSubtitleActive}
                sideContentsVisible={sideContentsVisible}
                setSideContentsVisible={setSideContentsVisible}
                videoVerticalVisible={videoVerticalVisible}
                handleIndicatorUpClick={handleIndicatorUpClick}
                handleIndicatorDownClick={handleIndicatorDownClick}
                captionEnable={_captionEnable}
                disclaimer={disclaimer}
                type={type}
              />
            </div>
          )}
        </Overlay>

        {this.state.bottomControlsRendered ? (
          <div
            className={classNames(
              css.fullscreen,
              type === "LIVE" && css.liveFullScreen
            )}
            {...controlsAriaProps}
          >
            <FeedbackContent
              className={css.miniFeedback}
              playbackRate={
                this.pulsedPlaybackRate ||
                this.selectPlaybackRate(this.speedIndex)
              }
              playbackState={this.pulsedPlaybackState || this.prevCommand}
              visible={this.state.miniFeedbackVisible && !noMiniFeedback}
            ></FeedbackContent>

            <ControlsContainer
              className={classNames(
                css.bottom +
                  (this.state.mediaControlsVisible ? "" : " " + css.hidden) +
                  (this.state.infoVisible ? " " + css.lift : ""),
                videoVerticalVisible && css.videoVerticalBottom
              )}
              spotlightDisabled={
                spotlightDisabled || !this.state.mediaControlsVisible
              }
            >
              {/*
								Info Section: Title, Description, Times
								Only render when `this.state.mediaControlsVisible` is true in order for `Marquee`
								to make calculations correctly in `MediaTitle`.
							*/}

              {noSlider ? null : (
                <div
                  className={classNames(
                    css.sliderContainer,
                    videoVerticalVisible && css.videoVertical
                  )}
                >
                  {this.state.mediaSliderVisible && type ? (
                    <Times
                      noCurrentTime
                      total={
                        type === "LIVE" ? liveTotalTime : this.state.duration
                      }
                      formatter={durFmt}
                      type={type}
                    />
                  ) : null}
                  {this.state.mediaSliderVisible && type ? (
                    <Times
                      noTotalTime
                      current={
                        type === "LIVE"
                          ? currentLiveTimeSeconds
                          : this.state.currentTime
                      }
                      formatter={durFmt}
                    />
                  ) : null}

                  {!panelInfo.modal && (
                    <MediaSlider
                      //노브
                      backgroundProgress={this.state.proportionLoaded}
                      disabled={disabled || this.state.sourceUnavailable}
                      forcePressed={this.state.slider5WayPressed}
                      //  onBlur={this.handleSliderBlur}
                      onChange={this.onSliderChange}
                      // onFocus={this.handleSliderFocus}
                      onKeyDown={this.handleSliderKeyDown}
                      onKnobMove={this.handleKnobMove}
                      spotlightId={SpotlightIds.PLAYER_SLIDER}
                      videoVerticalVisible={videoVerticalVisible}
                      selection={proportionSelection}
                      spotlightDisabled={
                        spotlightDisabled ||
                        !this.state.mediaControlsVisible ||
                        type === "LIVE"
                      }
                      value={this.state.proportionPlayed}
                      visible={this.state.mediaSliderVisible}
                      type={type}
                    ></MediaSlider>
                  )}
                </div>
              )}
              <ComponentOverride
                component={mediaControlsComponent}
                videoVerticalVisible={videoVerticalVisible}
                id={`${this.id}_mediaControls`}
                initialJumpDelay={initialJumpDelay}
                jumpDelay={jumpDelay}
                mediaDisabled={disabled || this.state.sourceUnavailable}
                no5WayJump={no5WayJump}
                onClose={!sideContentsVisible && this.handleMediaControlsClose}
                onPause={this.handlePause}
                onPlay={this.handlePlay}
                paused={type !== "LIVE" && this.state.paused}
                spotlightId={this.mediaControlsSpotlightId}
                countryCode={countryCode}
                spotlightDisabled={
                  !this.state.mediaControlsVisible || spotlightDisabled
                }
                visible={this.state.mediaControlsVisible}
                type={type}
              />
            </ControlsContainer>
          </div>
        ) : null}
        <SpottableDiv
          className={css.controlsHandleAbove}
          holdConfig={controlsHandleAboveHoldConfig}
          onDown={this.handleControlsHandleAboveDown}
          onKeyUp={this.handleControlsHandleAboveKeyUp}
          onSpotlightDown={onSpotlightFocus}
          onSpotlightUp={onSpotlightFocus}
          onSpotlightRight={onSpotlightFocus}
          onSpotlightLeft={onSpotlightFocus}
          onClick={onSpotlightFocus}
          selectionKeys={controlsHandleAboveSelectionKeys}
          spotlightDisabled={
            this.state.mediaControlsVisible || spotlightDisabled
          }
        />
        <Announce ref={this.setAnnounceRef} />
      </RootContainer>
    );
  }
};

/**
 * A standard HTML5 video player for Sandstone. It behaves, responds to, and operates like a
 * `<video>` tag in its support for `<source>`.  It also accepts custom tags such as
 * `<infoComponents>` for displaying additional information in the title area and `<MediaControls>`
 * for handling media playback controls and adding more controls.
 *
 * Example usage:
 * ```
 *	<VideoPlayer title="Hilarious Cat Video" poster="http://my.cat.videos/boots-poster.jpg">
 *		<source src="http://my.cat.videos/boots.mp4" type="video/mp4" />
 *		<infoComponents>A video about my cat Boots, wearing boots.</infoComponents>
 *		<MediaControls>
 *			<leftComponents><Button backgroundOpacity="translucent" icon="star" /></leftComponents>
 *			<rightComponents><Button backgroundOpacity="translucent" icon="notification" /></rightComponents>
 *
 *			<Button backgroundOpacity="translucent">Add To Favorites</Button>
 *			<Button backgroundOpacity="translucent" icon="search" />
 *		</MediaControls>
 *	</VideoPlayer>
 * ```
 *
 * To invoke methods (e.g.: `fastForward()`) or get the current state (`getMediaState()`), store a
 * ref to the `VideoPlayer` within your component:
 *
 * ```
 * 	...
 *
 * 	setVideoPlayer = (node) => {
 * 		this.videoPlayer = node;
 * 	}
 *
 * 	play () {
 * 		this.videoPlayer.play();
 * 	}
 *
 * 	render () {
 * 		return (
 * 			<VideoPlayer ref={this.setVideoPlayer} />
 * 		);
 * 	}
 * ```
 *
 * @class VideoPlayer
 * @memberof sandstone/VideoPlayer
 * @mixes ui/Slottable.Slottable
 * @ui
 * @public
 */
const VideoPlayer = ApiDecorator(
  {
    api: [
      "areControlsVisible",
      "fastForward",
      "getMediaState",
      "getVideoNode",
      "hideControls",
      "jump",
      "pause",
      "play",
      "rewind",
      "seek",
      "showControls",
      "showFeedback",
      "toggleControls",
    ],
  },
  I18nContextDecorator(
    { localeProp: "locale" },

    Slottable(
      {
        slots: [
          "infoComponents",
          "backButton",
          "promotionTitle",
          "cameraSettingsButton",
          "mediaControlsComponent",
          "source",
          "track",
          "thumbnailComponent",
          "videoComponent",
        ],
      },
      FloatingLayerDecorator(
        { floatLayerId: "videoPlayerFloatingLayer" },
        Skinnable(VideoPlayerBase)
      )
    )
  )
);

export default VideoPlayer;
export { Video, VideoPlayer, VideoPlayerBase };
