2018-03-26 23:32:43 +02:00
|
|
|
// @flow
|
2019-06-28 09:33:07 +02:00
|
|
|
import type { ElementRef } from 'react';
|
2019-01-28 21:05:28 +01:00
|
|
|
import * as PAGES from 'constants/pages';
|
2019-04-03 07:56:58 +02:00
|
|
|
import React, { Suspense } from 'react';
|
2018-03-26 23:32:43 +02:00
|
|
|
import classnames from 'classnames';
|
2018-08-03 17:54:10 +02:00
|
|
|
import analytics from 'analytics';
|
2018-07-05 04:49:12 +02:00
|
|
|
import LoadingScreen from 'component/common/loading-screen';
|
2018-07-10 05:32:59 +02:00
|
|
|
import PlayButton from './internal/play-button';
|
2019-07-04 06:24:33 +02:00
|
|
|
import detectTyping from 'util/detect-typing';
|
2017-05-02 15:58:35 +02:00
|
|
|
|
2019-04-24 16:02:08 +02:00
|
|
|
const Player = React.lazy(() =>
|
2019-05-26 08:18:47 +02:00
|
|
|
import(
|
|
|
|
/* webpackChunkName: "player-legacy" */
|
|
|
|
'./internal/player'
|
|
|
|
)
|
2019-04-24 16:02:08 +02:00
|
|
|
);
|
2019-04-03 07:56:58 +02:00
|
|
|
|
2018-05-16 21:34:38 +02:00
|
|
|
const SPACE_BAR_KEYCODE = 32;
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
type Props = {
|
|
|
|
cancelPlay: () => void,
|
|
|
|
fileInfo: {
|
|
|
|
outpoint: string,
|
|
|
|
file_name: string,
|
|
|
|
written_bytes: number,
|
|
|
|
download_path: string,
|
|
|
|
completed: boolean,
|
2019-01-30 05:15:01 +01:00
|
|
|
blobs_completed: number,
|
2018-03-26 23:32:43 +02:00
|
|
|
},
|
2018-07-18 16:41:14 +02:00
|
|
|
fileInfoErrors: ?{
|
|
|
|
[string]: boolean,
|
|
|
|
},
|
2018-05-11 16:33:33 +02:00
|
|
|
autoplay: boolean,
|
2018-03-26 23:32:43 +02:00
|
|
|
isLoading: boolean,
|
|
|
|
isDownloading: boolean,
|
|
|
|
playingUri: ?string,
|
|
|
|
contentType: string,
|
|
|
|
changeVolume: number => void,
|
|
|
|
volume: number,
|
2019-04-24 16:02:08 +02:00
|
|
|
claim: StreamClaim,
|
2018-03-26 23:32:43 +02:00
|
|
|
uri: string,
|
2018-07-31 14:36:20 +02:00
|
|
|
savePosition: (string, string, number) => void,
|
2018-07-31 20:06:50 +02:00
|
|
|
position: ?number,
|
2018-03-26 23:32:43 +02:00
|
|
|
className: ?string,
|
|
|
|
obscureNsfw: boolean,
|
2018-05-11 16:33:33 +02:00
|
|
|
play: string => void,
|
2018-07-05 04:49:12 +02:00
|
|
|
mediaType: string,
|
2018-08-28 22:46:50 +02:00
|
|
|
claimRewards: () => void,
|
2019-01-28 21:05:28 +01:00
|
|
|
nextFileToPlay: ?string,
|
|
|
|
navigate: (string, {}) => void,
|
2019-03-18 06:09:50 +01:00
|
|
|
costInfo: ?{ cost: number },
|
2019-04-03 06:17:00 +02:00
|
|
|
insufficientCredits: boolean,
|
2019-04-24 16:02:08 +02:00
|
|
|
nsfw: boolean,
|
|
|
|
thumbnail: ?string,
|
2019-05-26 08:18:47 +02:00
|
|
|
isPlayableType: boolean,
|
2019-06-28 09:33:07 +02:00
|
|
|
viewerContainer: { current: ElementRef<any> },
|
2019-07-27 23:17:25 +02:00
|
|
|
changeMute: boolean => void,
|
|
|
|
muted: boolean,
|
2018-03-26 23:32:43 +02:00
|
|
|
};
|
2017-04-23 11:56:50 +02:00
|
|
|
|
2018-07-10 05:32:59 +02:00
|
|
|
class FileViewer extends React.PureComponent<Props> {
|
2018-05-16 20:32:25 +02:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
(this: any).playContent = this.playContent.bind(this);
|
|
|
|
(this: any).handleKeyDown = this.handleKeyDown.bind(this);
|
2018-08-03 17:54:10 +02:00
|
|
|
(this: any).logTimeToStart = this.logTimeToStart.bind(this);
|
2019-01-28 21:05:28 +01:00
|
|
|
(this: any).onFileFinishCb = this.onFileFinishCb.bind(this);
|
|
|
|
(this: any).onFileStartCb = undefined;
|
2018-08-03 17:54:10 +02:00
|
|
|
|
|
|
|
// Don't add these variables to state because we don't need to re-render when their values change
|
|
|
|
(this: any).startTime = undefined;
|
|
|
|
(this: any).playTime = undefined;
|
2018-05-16 20:32:25 +02:00
|
|
|
}
|
|
|
|
|
2018-04-30 17:27:20 +02:00
|
|
|
componentDidMount() {
|
2018-08-03 17:54:10 +02:00
|
|
|
const { fileInfo } = this.props;
|
|
|
|
if (!fileInfo) {
|
2019-01-28 21:05:28 +01:00
|
|
|
this.onFileStartCb = this.logTimeToStart;
|
2018-08-03 17:54:10 +02:00
|
|
|
}
|
|
|
|
|
2018-04-30 17:27:20 +02:00
|
|
|
this.handleAutoplay(this.props);
|
2019-07-04 06:24:33 +02:00
|
|
|
window.addEventListener('keydown', this.handleKeyDown);
|
2018-04-30 17:27:20 +02:00
|
|
|
}
|
|
|
|
|
2018-07-18 16:41:14 +02:00
|
|
|
componentDidUpdate(prev: Props) {
|
2018-08-03 17:54:10 +02:00
|
|
|
const { fileInfo } = this.props;
|
|
|
|
|
|
|
|
if (this.props.uri !== prev.uri) {
|
|
|
|
// User just directly navigated to another piece of content
|
|
|
|
if (this.startTime && !this.playTime) {
|
|
|
|
// They started playing a file but it didn't start streaming
|
|
|
|
// Fire the analytics event with the previous file
|
|
|
|
this.fireAnalyticsEvent(prev.claim);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.startTime = null;
|
|
|
|
this.playTime = null;
|
|
|
|
|
|
|
|
// If this new file is already downloaded, remove the startedPlayingCallback
|
2019-01-28 21:05:28 +01:00
|
|
|
if (fileInfo && this.onFileStartCb) {
|
|
|
|
this.onFileStartCb = null;
|
|
|
|
} else if (!fileInfo && !this.onFileStartCb) {
|
|
|
|
this.onFileStartCb = this.logTimeToStart;
|
2018-08-03 17:54:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-11 16:33:33 +02:00
|
|
|
if (
|
2018-07-18 16:41:14 +02:00
|
|
|
this.props.autoplay !== prev.autoplay ||
|
|
|
|
this.props.fileInfo !== prev.fileInfo ||
|
|
|
|
this.props.isDownloading !== prev.isDownloading ||
|
|
|
|
this.props.playingUri !== prev.playingUri
|
2018-05-11 16:33:33 +02:00
|
|
|
) {
|
2018-07-18 16:41:14 +02:00
|
|
|
// suppress autoplay after download error
|
2019-01-19 19:54:06 +01:00
|
|
|
if (!this.props.fileInfoErrors || !(this.props.uri in this.props.fileInfoErrors)) {
|
2018-07-18 16:41:14 +02:00
|
|
|
this.handleAutoplay(this.props);
|
|
|
|
}
|
2018-05-11 16:33:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2018-08-03 17:54:10 +02:00
|
|
|
const { claim } = this.props;
|
|
|
|
|
|
|
|
if (this.startTime && !this.playTime) {
|
|
|
|
// The user is navigating away before the file started playing, or a play time was never set
|
|
|
|
// Currently will not be set for files that don't use render-media
|
|
|
|
this.fireAnalyticsEvent(claim);
|
|
|
|
}
|
|
|
|
|
2018-05-11 16:33:33 +02:00
|
|
|
this.props.cancelPlay();
|
2019-07-04 06:24:33 +02:00
|
|
|
window.removeEventListener('keydown', this.handleKeyDown);
|
2018-04-30 17:27:20 +02:00
|
|
|
}
|
|
|
|
|
2019-07-04 06:24:33 +02:00
|
|
|
handleKeyDown(event: KeyboardEvent) {
|
|
|
|
if (!detectTyping()) {
|
2019-05-26 08:18:47 +02:00
|
|
|
if (event.keyCode === SPACE_BAR_KEYCODE) {
|
|
|
|
event.preventDefault(); // prevent page scroll
|
|
|
|
this.playContent();
|
|
|
|
}
|
2018-05-16 20:32:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleAutoplay = (props: Props) => {
|
2019-04-24 16:02:08 +02:00
|
|
|
const { autoplay, playingUri, fileInfo, costInfo, isDownloading, uri, nsfw } = props;
|
2018-04-30 17:27:20 +02:00
|
|
|
|
2019-04-24 16:02:08 +02:00
|
|
|
const playable = autoplay && playingUri !== uri && !nsfw;
|
2018-04-30 17:27:20 +02:00
|
|
|
|
2018-05-11 16:33:33 +02:00
|
|
|
if (playable && costInfo && costInfo.cost === 0 && !fileInfo && !isDownloading) {
|
2018-09-12 21:50:04 +02:00
|
|
|
this.playContent();
|
2019-02-18 18:33:02 +01:00
|
|
|
} else if (playable && fileInfo && fileInfo.download_path && fileInfo.written_bytes > 0) {
|
2018-09-12 21:50:04 +02:00
|
|
|
this.playContent();
|
2018-04-30 17:27:20 +02:00
|
|
|
}
|
2018-05-16 20:32:25 +02:00
|
|
|
};
|
2018-04-30 17:27:20 +02:00
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
isMediaSame(nextProps: Props) {
|
2019-05-07 23:38:29 +02:00
|
|
|
return this.props.fileInfo && nextProps.fileInfo && this.props.fileInfo.outpoint === nextProps.fileInfo.outpoint;
|
2017-07-12 22:56:18 +02:00
|
|
|
}
|
|
|
|
|
2018-05-16 20:32:25 +02:00
|
|
|
playContent() {
|
2019-04-03 06:17:00 +02:00
|
|
|
const { play, uri, fileInfo, isDownloading, isLoading, insufficientCredits } = this.props;
|
|
|
|
|
2019-05-02 20:29:45 +02:00
|
|
|
if (!fileInfo && insufficientCredits) {
|
2019-04-03 06:17:00 +02:00
|
|
|
return;
|
|
|
|
}
|
2018-08-22 21:54:52 +02:00
|
|
|
|
2019-03-13 06:59:07 +01:00
|
|
|
// @if TARGET='app'
|
2018-08-22 21:54:52 +02:00
|
|
|
if (fileInfo || isDownloading || isLoading) {
|
|
|
|
// User may have pressed download before clicking play
|
2019-01-28 21:05:28 +01:00
|
|
|
this.onFileStartCb = null;
|
2018-08-22 21:54:52 +02:00
|
|
|
}
|
2018-08-03 17:54:10 +02:00
|
|
|
|
2019-01-28 21:05:28 +01:00
|
|
|
if (this.onFileStartCb) {
|
2018-08-03 17:54:10 +02:00
|
|
|
this.startTime = Date.now();
|
|
|
|
}
|
2019-03-13 06:59:07 +01:00
|
|
|
// @endif
|
2018-08-03 17:54:10 +02:00
|
|
|
|
2018-05-16 20:32:25 +02:00
|
|
|
play(uri);
|
|
|
|
}
|
|
|
|
|
2018-08-03 17:54:10 +02:00
|
|
|
logTimeToStart() {
|
|
|
|
const { claim } = this.props;
|
|
|
|
|
|
|
|
if (this.startTime) {
|
|
|
|
this.playTime = Date.now();
|
|
|
|
this.fireAnalyticsEvent(claim, this.startTime, this.playTime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-24 16:02:08 +02:00
|
|
|
fireAnalyticsEvent(claim: StreamClaim, startTime: ?number, playTime: ?number) {
|
2018-08-28 22:46:50 +02:00
|
|
|
const { claimRewards } = this.props;
|
2018-08-03 17:54:10 +02:00
|
|
|
const { name, claim_id: claimId, txid, nout } = claim;
|
|
|
|
|
|
|
|
// ideally outpoint would exist inside of claim information
|
|
|
|
// we can use it after https://github.com/lbryio/lbry/issues/1306 is addressed
|
|
|
|
const outpoint = `${txid}:${nout}`;
|
|
|
|
|
|
|
|
let timeToStart;
|
|
|
|
if (playTime && startTime) {
|
|
|
|
timeToStart = playTime - startTime;
|
|
|
|
}
|
|
|
|
|
2018-08-28 22:46:50 +02:00
|
|
|
analytics.apiLogView(`${name}#${claimId}`, outpoint, claimId, timeToStart, claimRewards);
|
|
|
|
}
|
2018-08-03 17:54:10 +02:00
|
|
|
|
2019-01-28 21:05:28 +01:00
|
|
|
onFileFinishCb() {
|
|
|
|
// If a user has `autoplay` enabled, start playing the next file at the top of "related"
|
|
|
|
const { autoplay, nextFileToPlay, navigate } = this.props;
|
|
|
|
if (autoplay && nextFileToPlay) {
|
|
|
|
navigate(PAGES.SHOW, { uri: nextFileToPlay });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onFileStartCb: ?() => void;
|
2018-08-03 17:54:10 +02:00
|
|
|
startTime: ?number;
|
|
|
|
playTime: ?number;
|
|
|
|
|
2017-04-23 11:56:50 +02:00
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
isLoading,
|
|
|
|
isDownloading,
|
2017-09-18 04:08:43 +02:00
|
|
|
playingUri,
|
2019-03-15 17:15:31 +01:00
|
|
|
fileInfo = {},
|
2017-05-31 19:36:38 +02:00
|
|
|
contentType,
|
2017-08-25 21:05:00 +02:00
|
|
|
changeVolume,
|
|
|
|
volume,
|
2017-12-29 17:07:03 +01:00
|
|
|
claim,
|
2017-09-18 04:08:43 +02:00
|
|
|
uri,
|
2017-12-23 00:14:54 +01:00
|
|
|
savePosition,
|
2018-07-31 20:06:50 +02:00
|
|
|
position,
|
2018-03-26 23:32:43 +02:00
|
|
|
className,
|
|
|
|
obscureNsfw,
|
2018-07-05 04:49:12 +02:00
|
|
|
mediaType,
|
2019-04-03 06:17:00 +02:00
|
|
|
insufficientCredits,
|
2019-06-05 04:26:57 +02:00
|
|
|
viewerContainer,
|
2019-04-24 16:02:08 +02:00
|
|
|
thumbnail,
|
|
|
|
nsfw,
|
2019-07-27 23:17:25 +02:00
|
|
|
muted,
|
|
|
|
changeMute,
|
2017-06-06 23:19:12 +02:00
|
|
|
} = this.props;
|
2017-04-23 11:56:50 +02:00
|
|
|
|
2017-09-18 04:08:43 +02:00
|
|
|
const isPlaying = playingUri === uri;
|
2019-04-24 16:02:08 +02:00
|
|
|
let isReadyToPlay = false;
|
2019-03-15 17:15:31 +01:00
|
|
|
// @if TARGET='app'
|
2019-04-24 16:02:08 +02:00
|
|
|
isReadyToPlay = fileInfo && fileInfo.download_path && fileInfo.written_bytes > 0;
|
2019-03-15 17:15:31 +01:00
|
|
|
// @endif
|
|
|
|
// @if TARGET='web'
|
|
|
|
// try to play immediately on web, we don't need to call file_list since we are streaming from reflector
|
2019-04-24 16:02:08 +02:00
|
|
|
isReadyToPlay = isPlaying;
|
2019-03-15 17:15:31 +01:00
|
|
|
// @endif
|
2017-05-15 18:34:33 +02:00
|
|
|
|
2019-04-24 16:02:08 +02:00
|
|
|
const shouldObscureNsfw = obscureNsfw && nsfw;
|
2017-12-21 22:08:54 +01:00
|
|
|
let loadStatusMessage = '';
|
2017-04-23 11:56:50 +02:00
|
|
|
|
2019-02-18 18:33:02 +01:00
|
|
|
if (fileInfo && fileInfo.completed && (!fileInfo.download_path || !fileInfo.written_bytes)) {
|
2017-06-06 23:19:12 +02:00
|
|
|
loadStatusMessage = __(
|
|
|
|
"It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds."
|
|
|
|
);
|
2017-05-28 15:38:36 +02:00
|
|
|
} else if (isLoading) {
|
2017-12-21 22:08:54 +01:00
|
|
|
loadStatusMessage = __('Requesting stream...');
|
2017-04-23 11:56:50 +02:00
|
|
|
} else if (isDownloading) {
|
2017-12-21 22:08:54 +01:00
|
|
|
loadStatusMessage = __('Downloading stream... not long left now!');
|
2017-04-23 11:56:50 +02:00
|
|
|
}
|
|
|
|
|
2019-04-03 06:17:00 +02:00
|
|
|
const layoverClass = classnames('content__cover', {
|
|
|
|
'card__media--nsfw': shouldObscureNsfw,
|
2019-05-02 20:29:45 +02:00
|
|
|
'card__media--disabled': !fileInfo && insufficientCredits,
|
2019-04-03 06:17:00 +02:00
|
|
|
});
|
2019-01-16 05:33:06 +01:00
|
|
|
|
2019-05-07 23:38:29 +02:00
|
|
|
const layoverStyle = !shouldObscureNsfw && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {};
|
2017-05-31 19:36:38 +02:00
|
|
|
|
2017-04-23 11:56:50 +02:00
|
|
|
return (
|
2019-06-05 04:26:57 +02:00
|
|
|
<div className={classnames('video', {}, className)} ref={viewerContainer}>
|
2018-03-26 23:32:43 +02:00
|
|
|
{isPlaying && (
|
2019-03-25 07:18:22 +01:00
|
|
|
<div className="content__view">
|
2018-03-26 23:32:43 +02:00
|
|
|
{!isReadyToPlay ? (
|
2018-04-30 17:27:20 +02:00
|
|
|
<div className={layoverClass} style={layoverStyle}>
|
|
|
|
<LoadingScreen status={loadStatusMessage} />
|
|
|
|
</div>
|
2018-05-11 16:33:33 +02:00
|
|
|
) : (
|
2019-04-24 16:02:08 +02:00
|
|
|
<Suspense fallback={<div />}>
|
2019-04-03 07:56:58 +02:00
|
|
|
<Player
|
|
|
|
fileName={fileInfo.file_name}
|
2019-04-24 16:02:08 +02:00
|
|
|
poster={thumbnail}
|
2019-04-03 07:56:58 +02:00
|
|
|
downloadPath={fileInfo.download_path}
|
|
|
|
mediaType={mediaType}
|
|
|
|
contentType={contentType}
|
|
|
|
downloadCompleted={fileInfo.completed}
|
|
|
|
changeVolume={changeVolume}
|
|
|
|
volume={volume}
|
2019-05-07 23:38:29 +02:00
|
|
|
savePosition={newPosition => savePosition(claim.claim_id, `${claim.txid}:${claim.nout}`, newPosition)}
|
2019-04-03 07:56:58 +02:00
|
|
|
claim={claim}
|
|
|
|
uri={uri}
|
|
|
|
position={position}
|
|
|
|
onStartCb={this.onFileStartCb}
|
|
|
|
onFinishCb={this.onFileFinishCb}
|
|
|
|
playingUri={playingUri}
|
2019-06-05 04:26:57 +02:00
|
|
|
viewerContainer={viewerContainer}
|
2019-07-27 23:17:25 +02:00
|
|
|
muted={muted}
|
|
|
|
changeMute={changeMute}
|
2019-04-03 07:56:58 +02:00
|
|
|
/>
|
|
|
|
</Suspense>
|
2018-03-26 23:32:43 +02:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{!isPlaying && (
|
2019-05-07 23:38:29 +02:00
|
|
|
<div role="button" onClick={this.playContent} className={layoverClass} style={layoverStyle}>
|
2018-07-10 05:32:59 +02:00
|
|
|
<PlayButton
|
2019-01-19 19:54:06 +01:00
|
|
|
play={(e: SyntheticInputEvent<*>) => {
|
2018-05-31 06:32:31 +02:00
|
|
|
e.stopPropagation();
|
|
|
|
this.playContent();
|
|
|
|
}}
|
2018-03-26 23:32:43 +02:00
|
|
|
fileInfo={fileInfo}
|
2018-01-05 20:27:58 +01:00
|
|
|
uri={uri}
|
2018-03-26 23:32:43 +02:00
|
|
|
isLoading={isLoading}
|
|
|
|
mediaType={mediaType}
|
2017-12-19 19:20:53 +01:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
2017-05-31 19:36:38 +02:00
|
|
|
</div>
|
2017-06-06 23:19:12 +02:00
|
|
|
);
|
2017-04-23 11:56:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-10 05:32:59 +02:00
|
|
|
export default FileViewer;
|