2019-10-07 17:11:53 -06:00
|
|
|
import React, { Component } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
import { StyleSheet, css } from 'aphrodite/no-important';
|
2019-10-22 15:23:14 -06:00
|
|
|
import { video as VideoApi } from '@commaai/comma-api';
|
2017-06-13 18:40:05 -06:00
|
|
|
|
2019-10-07 17:11:53 -06:00
|
|
|
import HLS from './HLS';
|
|
|
|
import RouteSeeker from './RouteSeeker/RouteSeeker';
|
2017-08-07 16:22:57 -06:00
|
|
|
|
|
|
|
const Styles = StyleSheet.create({
|
2017-12-12 19:27:20 -07:00
|
|
|
loadingOverlay: {
|
2019-10-07 17:11:53 -06:00
|
|
|
position: 'absolute',
|
2017-12-12 19:27:20 -07:00
|
|
|
top: 0,
|
|
|
|
left: 0,
|
2019-10-07 17:11:53 -06:00
|
|
|
width: '100%',
|
|
|
|
height: '100%',
|
|
|
|
display: 'flex',
|
|
|
|
justifyContent: 'center',
|
|
|
|
alignItems: 'center',
|
2017-12-12 19:27:20 -07:00
|
|
|
zIndex: 3
|
|
|
|
},
|
|
|
|
loadingSpinner: {
|
2019-10-07 17:11:53 -06:00
|
|
|
width: '25%',
|
|
|
|
height: '25%',
|
|
|
|
display: 'block'
|
2017-12-12 19:27:20 -07:00
|
|
|
},
|
|
|
|
img: {
|
|
|
|
height: 480,
|
2019-10-07 17:11:53 -06:00
|
|
|
display: 'block',
|
|
|
|
position: 'absolute',
|
2017-12-12 19:27:20 -07:00
|
|
|
zIndex: 2
|
|
|
|
},
|
|
|
|
hls: {
|
|
|
|
zIndex: 1,
|
|
|
|
height: 480,
|
2019-10-07 17:11:53 -06:00
|
|
|
backgroundColor: 'rgba(0,0,0,0.9)'
|
2017-12-12 19:27:20 -07:00
|
|
|
},
|
|
|
|
seekBar: {
|
2019-10-07 17:11:53 -06:00
|
|
|
position: 'absolute',
|
2017-12-12 19:27:20 -07:00
|
|
|
bottom: 0,
|
|
|
|
left: 0,
|
2019-10-07 17:11:53 -06:00
|
|
|
width: '100%',
|
2017-12-12 19:27:20 -07:00
|
|
|
zIndex: 4
|
|
|
|
}
|
2017-08-07 16:22:57 -06:00
|
|
|
});
|
|
|
|
|
2018-09-03 15:13:25 -06:00
|
|
|
export default class RouteVideoSync extends Component {
|
2017-12-12 19:27:20 -07:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.state = {
|
2018-09-03 15:13:25 -06:00
|
|
|
shouldShowJpeg: true,
|
|
|
|
isLoading: true,
|
2017-12-12 19:27:20 -07:00
|
|
|
videoElement: null,
|
2017-06-13 18:40:05 -06:00
|
|
|
};
|
|
|
|
|
2018-09-03 15:13:25 -06:00
|
|
|
this.onLoadStart = this.onLoadStart.bind(this);
|
|
|
|
this.onLoadEnd = this.onLoadEnd.bind(this);
|
2017-12-12 19:27:20 -07:00
|
|
|
this.segmentProgress = this.segmentProgress.bind(this);
|
|
|
|
this.onVideoElementAvailable = this.onVideoElementAvailable.bind(this);
|
|
|
|
this.onUserSeek = this.onUserSeek.bind(this);
|
2019-10-09 12:32:17 -06:00
|
|
|
this.onPlaySeek = this.onPlaySeek.bind(this);
|
2017-12-12 19:27:20 -07:00
|
|
|
this.ratioTime = this.ratioTime.bind(this);
|
|
|
|
}
|
|
|
|
|
2019-10-30 16:19:32 -06:00
|
|
|
componentDidUpdate(prevProps) {
|
2019-10-22 15:23:14 -06:00
|
|
|
const { userSeekTime } = this.props;
|
2019-10-09 12:32:17 -06:00
|
|
|
const { videoElement } = this.state;
|
2019-09-11 14:36:15 -06:00
|
|
|
if (
|
2019-10-30 16:19:32 -06:00
|
|
|
prevProps.userSeekTime
|
|
|
|
&& userSeekTime !== prevProps.userSeekTime
|
2019-09-11 14:36:15 -06:00
|
|
|
) {
|
2019-10-09 12:32:17 -06:00
|
|
|
if (videoElement) {
|
2019-10-30 16:19:32 -06:00
|
|
|
videoElement.currentTime = userSeekTime;
|
2019-09-11 14:36:15 -06:00
|
|
|
}
|
|
|
|
}
|
2017-12-12 19:27:20 -07:00
|
|
|
}
|
|
|
|
|
2019-10-09 12:32:17 -06:00
|
|
|
onVideoElementAvailable(videoElement) {
|
|
|
|
this.setState({ videoElement });
|
|
|
|
}
|
|
|
|
|
|
|
|
onUserSeek(ratio) {
|
|
|
|
/* ratio in [0,1] */
|
|
|
|
|
|
|
|
const { videoElement } = this.state;
|
|
|
|
const { onUserSeek } = this.props;
|
|
|
|
const seekTime = this.ratioTime(ratio);
|
|
|
|
const funcSeekToRatio = () => onUserSeek(seekTime);
|
|
|
|
|
|
|
|
if (Number.isNaN(videoElement.duration)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
videoElement.currentTime = seekTime;
|
|
|
|
|
2019-10-22 15:23:14 -06:00
|
|
|
if (ratio !== 0) {
|
2019-10-09 12:32:17 -06:00
|
|
|
funcSeekToRatio();
|
2019-09-30 19:13:01 -06:00
|
|
|
}
|
2017-12-12 19:27:20 -07:00
|
|
|
}
|
|
|
|
|
2019-10-09 12:32:17 -06:00
|
|
|
onPlaySeek(offset) {
|
|
|
|
const { onPlaySeek } = this.props;
|
|
|
|
this.seekTime = offset;
|
|
|
|
onPlaySeek(offset);
|
2017-12-12 19:27:20 -07:00
|
|
|
}
|
|
|
|
|
2018-09-03 15:13:25 -06:00
|
|
|
onLoadStart() {
|
|
|
|
this.setState({
|
|
|
|
shouldShowJpeg: true,
|
|
|
|
isLoading: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onLoadEnd() {
|
|
|
|
this.setState({
|
|
|
|
shouldShowJpeg: false,
|
|
|
|
isLoading: false
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-10-09 12:32:17 -06:00
|
|
|
loadingOverlay() {
|
|
|
|
return (
|
|
|
|
<div className={css(Styles.loadingOverlay)}>
|
|
|
|
<img
|
|
|
|
className={css(Styles.loadingSpinner)}
|
|
|
|
src={`${process.env.PUBLIC_URL}/img/loading.svg`}
|
|
|
|
alt="Loading video"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-09-11 14:36:15 -06:00
|
|
|
videoLength() {
|
|
|
|
if (this.props.segment.length) {
|
|
|
|
return this.props.segment[1] - this.props.segment[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.videoElement) {
|
|
|
|
return this.state.videoElement.duration;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
startTime() {
|
|
|
|
if (this.props.segment.length) {
|
|
|
|
return this.props.segment[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-12-12 19:27:20 -07:00
|
|
|
segmentProgress(currentTime) {
|
|
|
|
// returns progress as number in [0,1]
|
2019-10-07 17:11:53 -06:00
|
|
|
const startTime = this.startTime();
|
2018-09-03 15:13:25 -06:00
|
|
|
|
2019-09-11 14:36:15 -06:00
|
|
|
if (currentTime < startTime) {
|
|
|
|
currentTime = startTime;
|
2017-06-13 18:40:05 -06:00
|
|
|
}
|
|
|
|
|
2019-09-11 14:36:15 -06:00
|
|
|
const ratio = (currentTime - startTime) / this.videoLength();
|
2017-12-12 19:27:20 -07:00
|
|
|
return Math.max(0, Math.min(1, ratio));
|
|
|
|
}
|
2017-06-13 18:40:05 -06:00
|
|
|
|
2017-12-12 19:27:20 -07:00
|
|
|
ratioTime(ratio) {
|
2019-09-11 14:36:15 -06:00
|
|
|
return ratio * this.videoLength() + this.startTime();
|
2017-12-12 19:27:20 -07:00
|
|
|
}
|
2017-06-13 18:40:05 -06:00
|
|
|
|
2019-10-09 12:32:17 -06:00
|
|
|
nearestFrameUrl() {
|
|
|
|
const { thumbnails } = this.props;
|
|
|
|
if (!this.seekTime) {
|
|
|
|
return '';
|
2019-09-11 14:36:15 -06:00
|
|
|
}
|
2019-10-09 12:32:17 -06:00
|
|
|
for (let i = 0, l = thumbnails.length; i < l; ++i) {
|
|
|
|
if (Math.abs(thumbnails[i].monoTime - this.seekTime) < 5) {
|
|
|
|
const data = btoa(String.fromCharCode(...thumbnails[i].data));
|
|
|
|
return `data:image/jpeg;base64,${data}`;
|
|
|
|
}
|
2017-05-29 17:52:17 -06:00
|
|
|
}
|
2019-10-09 12:32:17 -06:00
|
|
|
return '';
|
2017-12-12 19:27:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2019-10-09 12:32:17 -06:00
|
|
|
const {
|
|
|
|
isLoading,
|
2019-10-09 16:22:46 -06:00
|
|
|
shouldShowJpeg,
|
|
|
|
videoElement
|
2019-10-09 12:32:17 -06:00
|
|
|
} = this.state;
|
|
|
|
const {
|
|
|
|
userSeekTime,
|
|
|
|
url,
|
|
|
|
playSpeed,
|
|
|
|
playing,
|
|
|
|
onVideoClick,
|
2019-10-09 16:22:46 -06:00
|
|
|
segmentIndices,
|
2019-10-28 14:24:04 -06:00
|
|
|
startTime,
|
|
|
|
segment
|
2019-10-09 12:32:17 -06:00
|
|
|
} = this.props;
|
2017-12-12 19:27:20 -07:00
|
|
|
return (
|
|
|
|
<div className="cabana-explorer-visuals-camera">
|
2019-10-09 12:32:17 -06:00
|
|
|
{isLoading ? this.loadingOverlay() : null}
|
|
|
|
{shouldShowJpeg ? (
|
2017-12-12 19:27:20 -07:00
|
|
|
<img
|
|
|
|
src={this.nearestFrameUrl()}
|
|
|
|
className={css(Styles.img)}
|
2019-10-09 12:32:17 -06:00
|
|
|
alt={`Camera preview at t = ${Math.round(userSeekTime)}`}
|
2017-12-12 19:27:20 -07:00
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
<HLS
|
|
|
|
className={css(Styles.hls)}
|
2019-08-09 16:48:54 -06:00
|
|
|
source={VideoApi(
|
2019-10-09 12:32:17 -06:00
|
|
|
url,
|
2019-08-09 16:48:54 -06:00
|
|
|
process.env.REACT_APP_VIDEO_CDN
|
|
|
|
).getRearCameraStreamIndexUrl()}
|
2019-10-09 16:22:46 -06:00
|
|
|
startTime={startTime || 0}
|
2019-09-11 14:36:15 -06:00
|
|
|
videoLength={this.videoLength()}
|
2019-10-09 12:32:17 -06:00
|
|
|
playbackSpeed={playSpeed}
|
2017-12-12 19:27:20 -07:00
|
|
|
onVideoElementAvailable={this.onVideoElementAvailable}
|
2019-10-09 12:32:17 -06:00
|
|
|
playing={playing}
|
|
|
|
onClick={onVideoClick}
|
2018-09-03 15:13:25 -06:00
|
|
|
onLoadStart={this.onLoadStart}
|
|
|
|
onLoadEnd={this.onLoadEnd}
|
|
|
|
onUserSeek={this.onUserSeek}
|
2019-10-09 12:32:17 -06:00
|
|
|
onPlaySeek={this.onPlaySeek}
|
2017-12-12 19:27:20 -07:00
|
|
|
segmentProgress={this.segmentProgress}
|
|
|
|
/>
|
|
|
|
<RouteSeeker
|
|
|
|
className={css(Styles.seekBar)}
|
2019-10-09 12:32:17 -06:00
|
|
|
nearestFrameTime={userSeekTime}
|
2017-12-12 19:27:20 -07:00
|
|
|
segmentProgress={this.segmentProgress}
|
2019-09-11 14:36:15 -06:00
|
|
|
startTime={this.startTime()}
|
|
|
|
videoLength={this.videoLength()}
|
2019-10-09 12:32:17 -06:00
|
|
|
segmentIndices={segmentIndices}
|
2018-09-03 15:13:25 -06:00
|
|
|
onUserSeek={this.onUserSeek}
|
2019-10-09 12:32:17 -06:00
|
|
|
onPlaySeek={this.onPlaySeek}
|
2019-10-09 16:22:46 -06:00
|
|
|
videoElement={videoElement}
|
2017-12-12 19:27:20 -07:00
|
|
|
onPlay={this.props.onPlay}
|
|
|
|
onPause={this.props.onPause}
|
|
|
|
playing={this.props.playing}
|
|
|
|
ratioTime={this.ratioTime}
|
2019-10-28 14:24:04 -06:00
|
|
|
segment={segment}
|
2017-12-12 19:27:20 -07:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2017-05-29 17:52:17 -06:00
|
|
|
}
|
2019-10-09 12:32:17 -06:00
|
|
|
|
|
|
|
RouteVideoSync.propTypes = {
|
|
|
|
segment: PropTypes.array.isRequired,
|
|
|
|
thumbnails: PropTypes.array,
|
|
|
|
url: PropTypes.string.isRequired,
|
|
|
|
playing: PropTypes.bool.isRequired,
|
|
|
|
onPlaySeek: PropTypes.func.isRequired,
|
|
|
|
onUserSeek: PropTypes.func.isRequired,
|
|
|
|
onPlay: PropTypes.func.isRequired,
|
|
|
|
onPause: PropTypes.func.isRequired,
|
|
|
|
userSeekTime: PropTypes.number.isRequired,
|
|
|
|
playSpeed: PropTypes.number.isRequired,
|
|
|
|
onVideoClick: PropTypes.func,
|
|
|
|
segmentIndices: PropTypes.array,
|
2019-10-09 16:22:46 -06:00
|
|
|
startTime: PropTypes.number
|
2019-10-09 12:32:17 -06:00
|
|
|
};
|