🕐 커밋 시간: 2025. 11. 18. 09:44:39 📊 변경 통계: • 총 파일: 7개 • 추가: +23줄 • 삭제: -23줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/playActions.js ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js ~ com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.bak.js ~ com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.original.js ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • UI 컴포넌트 아키텍처 개선 • 소규모 기능 개선 • 코드 정리 및 최적화 • 모듈 구조 개선
2712 lines
81 KiB
JavaScript
2712 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;
|
|
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 ? (
|
|
<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 ||
|
|
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 };
|