Files
shoptime/com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js
optrader 4ecb03002f [251217] fix: VideoPlayer.v3.jsx times 전체화면에만 표시
🕐 커밋 시간: 2025. 12. 17. 18:19:37

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +2줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화
2025-12-17 18:19:37 +09:00

2716 lines
81 KiB
JavaScript

/**
* 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.v3.module.less';
import { updateVideoPlayState } from '../../actions/playActions';
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,
/**
* Disables Spotlight focus on the media slider while keeping it visible.
*
* @type {Boolean}
* @default false
* @public
*/
disableSliderFocus: 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,
setBelowContentsVisible: PropTypes.func,
belowContentsVisible: PropTypes.bool,
tabContainerVersion: PropTypes.number,
tabIndexV2: PropTypes.number,
overlayContentsComponent: PropTypes.elementType,
dispatch: PropTypes.func,
};
static contextType = FloatingLayerContext;
static defaultProps = {
autoCloseTimeout: 3000,
disableSliderFocus: false,
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();
}
}
// TabContainerV2와 mediaControls 동기화
if (
this.props.tabContainerVersion === 2 &&
this.props.belowContentsVisible !== undefined &&
prevProps.belowContentsVisible !== this.props.belowContentsVisible
) {
if (this.props.belowContentsVisible && !this.state.mediaControlsVisible) {
// TabContainerV2가 표시될 때 controls도 표시
this.showControls();
} else if (!this.props.belowContentsVisible && this.state.mediaControlsVisible) {
// TabContainerV2가 숨겨질 때 controls도 숨김
this.hideControls();
}
}
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.
// TabContainerV2가 표시 중이면 자동으로 닫지 않음
if (this.props.tabContainerVersion === 2 && this.props.belowContentsVisible) {
return;
}
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;
}
Spotlight.setPointerMode(false);
// 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,
};
});
};
onSpotlightFocus = () => {
this.showControls();
if (this.state.lastFocusedTarget) {
setTimeout(() => {
Spotlight.focus(this.state.lastFocusedTarget);
});
} else {
setTimeout(() => {
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
});
}
};
/**
* 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
);
onVideoClickCapture = (ev) => {
if (!this.state.mediaControlsVisible && !this.props.panelInfo?.modal) {
this.activityDetected();
this.onVideoClick();
ev.stopPropagation();
ev.preventDefault();
}
};
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);
// Redux에 비디오 재생 상태 업데이트
if (this.props.dispatch) {
this.props.dispatch(
updateVideoPlayState({
isPlaying: !updatedState.paused,
isPaused: updatedState.paused,
currentTime: updatedState.currentTime,
duration: updatedState.duration,
playbackRate: updatedState.playbackRate,
})
);
}
};
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 = () => {
console.log('[VideoPlayer] onVideoClick', {
controlsVisible: this.state.mediaControlsVisible,
panelInfoModal: this.props.panelInfo?.modal,
pointerMode: Spotlight.getPointerMode(),
});
// tabContainerVersion === 2일 때 belowContentsVisible도 함께 토글
if (this.props.tabContainerVersion === 2 && this.props.setBelowContentsVisible) {
const willShowControls = !this.state.mediaControlsVisible;
// belowContentsVisible을 먼저 변경 (componentDidUpdate에서 mediaControls 동기화됨)
this.props.setBelowContentsVisible(willShowControls);
return;
}
// 외부 onClick 핸들러가 있으면 호출
if (this.props.onClick) {
this.props.onClick();
return;
}
const willShowControls = !this.state.mediaControlsVisible;
this.toggleControls();
if (willShowControls && !this.props.panelInfo?.modal) {
this.restoreOverlayFocus();
}
};
restoreOverlayFocus = () => {
setTimeout(() => {
Spotlight.setPointerMode(false);
Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
});
};
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);
console.log('<<<<<<<<<<<<<<<<<<<<<< down key in slider >>>>>>>>>>>>>>>>');
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,
disableSliderFocus,
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,
belowContentsVisible,
tabContainerVersion,
overlayContentsComponent,
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;
delete mediaProps.setBelowContentsVisible;
delete mediaProps.belowContentsVisible;
delete mediaProps.tabContainerVersion;
delete mediaProps.tabIndexV2;
delete mediaProps.overlayContentsComponent;
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;
if (!panelInfo.modal) {
mediaProps.tabIndex = -1;
}
//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;
// 웹/TV 전체화면에서 ReactPlayer(YouTube) iframe이 자체 컨트롤을 활성화하지 않도록 포인터 이벤트 차단
if (!panelInfo.modal) {
mediaProps.style = { ...(mediaProps.style || {}), pointerEvents: 'none' };
}
delete mediaProps.mediaComponent;
delete mediaProps.ref;
// mediaProps.isYoutube = isYoutube;
}
const controlsAriaProps = this.getControlsAriaProps();
const OverlayContents = overlayContentsComponent || PlayerOverlayContents;
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;
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}
onClickCapture={this.onVideoClickCapture}
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 : '')
}
>
<OverlayContents
playListInfo={playListInfo && playListInfo}
selectedIndex={selectedIndex}
onClick={onBackButton}
panelInfo={panelInfo}
setIsSubtitleActive={setIsSubtitleActive}
sideContentsVisible={sideContentsVisible}
setSideContentsVisible={setSideContentsVisible}
belowContentsVisible={belowContentsVisible}
tabContainerVersion={tabContainerVersion}
tabIndexV2={this.props.tabIndexV2}
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 && !panelInfo.modal ? (
<Times
noCurrentTime
total={type === 'LIVE' ? liveTotalTime : this.state.duration}
formatter={durFmt}
type={type}
/>
) : null}
{this.state.mediaSliderVisible && type && !panelInfo.modal ? (
<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 ||
disableSliderFocus ||
!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={this.onSpotlightFocus}
onSpotlightUp={this.onSpotlightFocus}
onSpotlightRight={this.onSpotlightFocus}
onSpotlightLeft={this.onSpotlightFocus}
onClick={this.onSpotlightFocus}
selectionKeys={controlsHandleAboveSelectionKeys}
spotlightDisabled={
this.state.mediaControlsVisible || spotlightDisabled || !panelInfo.modal
}
/>
<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',
'onVideoClick',
],
},
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 };