Files
shoptime/com.twin.app.shoptime/src/components/VideoPlayer/Video.js
2024-03-05 10:05:34 +09:00

325 lines
8.6 KiB
JavaScript

import {forward} from '@enact/core/handle';
import ForwardRef from '@enact/ui/ForwardRef';
import {Media, getKeyFromSource} from '@enact/ui/Media';
import EnactPropTypes from '@enact/core/internal/prop-types';
import Slottable from '@enact/ui/Slottable';
import compose from 'ramda/src/compose';
import React from 'react';
import css from './VideoPlayer.module.less';
import PropTypes from 'prop-types';
/**
* Adds support for preloading a video source for `VideoPlayer`.
*
* @class VideoBase
* @memberof sandstone/VideoPlayer
* @ui
* @private
*/
const VideoBase = class extends React.Component {
static displayName = 'Video';
static propTypes = /** @lends sandstone/VideoPlayer.Video.prototype */ {
/**
* Video plays automatically.
*
* @type {Boolean}
* @default false
* @public
*/
autoPlay: PropTypes.bool,
/**
* Video component to use.
*
* The default (`'video'`) 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
* * `onPlay` - Sent when playback of the media starts after having been paused
* * `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 {String|Component|Element}
* @default 'video'
* @public
*/
mediaComponent: EnactPropTypes.renderableOverride,
/**
* The video source to be preloaded. Expects a `<source>` node.
*
* @type {Node}
* @public
*/
preloadSource: PropTypes.node,
/**
* Called with a reference to the active [Media]{@link ui/Media.Media} component.
*
* @type {Function}
* @private
*/
setMedia: PropTypes.func,
/**
* The video source to be played.
*
* Any children `<source>` elements will be sent directly to the `mediaComponent` as video
* sources.
*
* See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source
*
* @type {String|Node}
* @public
*/
source: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
track: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
};
static defaultProps = {
mediaComponent: 'video'
};
componentDidUpdate (prevProps) {
const {source, preloadSource} = this.props;
const {source: prevSource, preloadSource: prevPreloadSource} = prevProps;
const key = getKeyFromSource(source);
const prevKey = getKeyFromSource(prevSource);
const preloadKey = getKeyFromSource(preloadSource);
const prevPreloadKey = getKeyFromSource(prevPreloadSource);
if (this.props.setMedia !== prevProps.setMedia) {
this.clearMedia(prevProps);
this.setMedia();
}
if (source) {
if (key === prevPreloadKey && preloadKey !== prevPreloadKey) {
// if there's source and it was the preload source
// if the preloaded video didn't error, notify VideoPlayer it is ready to reset
if (this.preloadLoadStart) {
forward('onLoadStart', this.preloadLoadStart, this.props);
}
// emit onUpdate to give VideoPlayer an opportunity to updates its internal state
// since it won't receive the onLoadStart or onError event
forward('onUpdate', {type: 'onUpdate'}, this.props);
this.autoPlay();
} else if (key !== prevKey) {
// if there's source and it has changed.
this.autoPlay();
}
}
if (preloadSource && preloadKey !== prevPreloadKey) {
this.preloadLoadStart = null;
// In the case that the previous source equalled the previous preload (causing the
// preload video node to not be created) and then the preload source was changed, we
// need to guard against accessing the preloadVideo node.
if (this.preloadVideo) {
this.preloadVideo.load();
}
}
}
componentWillUnmount () {
this.clearMedia();
}
keys = ['media-1', 'media-2'];
prevSourceKey = null;
prevPreloadKey = null;
handlePreloadLoadStart = (ev) => {
// persist the event so we can cache it to re-emit when the preload becomes active
ev.persist();
this.preloadLoadStart = ev;
// prevent the from bubbling to upstream handlers
ev.stopPropagation();
};
clearMedia ({setMedia} = this.props) {
if (setMedia) {
setMedia(null);
}
}
setMedia ({setMedia} = this.props) {
if (setMedia) {
setMedia(this.video);
}
}
autoPlay () {
if (!this.props.autoPlay) return;
this.video.play();
}
setVideoRef = (node) => {
this.video = node;
this.setMedia();
};
setPreloadRef = (node) => {
if (node) {
node.load();
}
this.preloadVideo = node;
};
getKeys () {
const {source, preloadSource} = this.props;
const sourceKey = source && getKeyFromSource(source);
let preloadKey = preloadSource && getKeyFromSource(preloadSource);
// If the same source is used for both, clear the preload key to avoid rendering duplicate
// video elements.
if (sourceKey === preloadKey) {
preloadKey = null;
}
// if either the source or preload existed previously in the other "slot", swap the keys so
// the preload video becomes the active video and vice versa
if (
(sourceKey === this.prevPreloadKey && this.prevPreloadKey) ||
(preloadKey === this.prevSourceKey && this.prevSourceKey)
) {
this.keys.reverse();
}
// cache the previous keys so we know if the sources change the next time
this.prevSourceKey = sourceKey;
this.prevPreloadKey = preloadKey;
// if preload is unset, clear the key so we don't render that media node at all
return preloadKey ? this.keys : this.keys.slice(0, 1);
}
render () {
const {
preloadSource,
source,
track,
mediaComponent,
...rest
} = this.props;
delete rest.setMedia;
const [sourceKey, preloadKey] = this.getKeys();
return (
<React.Fragment>
{sourceKey ? (
<Media
{...rest}
className={css.video}
controls={false}
key={sourceKey}
mediaComponent={mediaComponent}
preload="none"
ref={this.setVideoRef}
track={React.isValidElement(track) ? track : (
<track src={track} />
)}
source={React.isValidElement(source) ? source : (
<source src={source} />
)}
/>
) : null}
{preloadKey ? (
<Media
autoPlay={false}
className={css.preloadVideo}
controls={false}
key={preloadKey}
mediaComponent={mediaComponent}
onLoadStart={this.handlePreloadLoadStart}
preload="none"
ref={this.setPreloadRef}
track={React.isValidElement(track) ? track : (
<track src={track} />
)}
source={React.isValidElement(preloadSource) ? preloadSource : (
<source src={preloadSource} />
)}
/>
) : null}
</React.Fragment>
);
}
};
const VideoDecorator = compose(
ForwardRef({prop: 'setMedia'}),
Slottable({slots: ['source', 'track','preloadSource']})
);
/**
* Provides support for more advanced video configurations for `VideoPlayer`.
*
* Custom Video Tag
*
* ```
* <VideoPlayer>
* <Video mediaComponent="custom-video-element">
* <source src="path/to/source.mp4" />
* </Video>
* </VideoPlayer>
* ```
*
* Preload Video Source
*
* ```
* <VideoPlayer>
* <Video>
* <source src="path/to/source.mp4" />
* <source src="path/to/preload-source.mp4" slot="preloadSource" />
* </Video>
* </VideoPlayer>
* ```
*
* @class Video
* @mixes ui/Slottable.Slottable
* @memberof sandstone/VideoPlayer
* @ui
* @public
*/
const Video = VideoDecorator(VideoBase);
Video.defaultSlot = 'videoComponent';
export default Video;
export {
Video
};