cabana/src/components/RouteVideoSync.js

261 lines
6.2 KiB
JavaScript
Raw Normal View History

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, css } from 'aphrodite/no-important';
import { video as VideoApi } from '@commaai/comma-api';
2017-06-13 18:40:05 -06:00
import HLS from './HLS';
import RouteSeeker from './RouteSeeker/RouteSeeker';
const Styles = StyleSheet.create({
2017-12-12 19:27:20 -07:00
loadingOverlay: {
position: 'absolute',
2017-12-12 19:27:20 -07:00
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
2017-12-12 19:27:20 -07:00
zIndex: 3
},
loadingSpinner: {
width: '25%',
height: '25%',
display: 'block'
2017-12-12 19:27:20 -07:00
},
img: {
height: 480,
display: 'block',
position: 'absolute',
2017-12-12 19:27:20 -07:00
zIndex: 2
},
hls: {
zIndex: 1,
height: 480,
backgroundColor: 'rgba(0,0,0,0.9)'
2017-12-12 19:27:20 -07:00
},
seekBar: {
position: 'absolute',
2017-12-12 19:27:20 -07:00
bottom: 0,
left: 0,
width: '100%',
2017-12-12 19:27:20 -07:00
zIndex: 4
}
});
export default class RouteVideoSync extends Component {
2017-12-12 19:27:20 -07:00
constructor(props) {
super(props);
this.state = {
shouldShowJpeg: true,
isLoading: true,
2017-12-12 19:27:20 -07:00
videoElement: null,
2017-06-13 18:40:05 -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);
this.onPlaySeek = this.onPlaySeek.bind(this);
2017-12-12 19:27:20 -07:00
this.ratioTime = this.ratioTime.bind(this);
}
componentDidUpdate(nextProps) {
const { userSeekTime } = this.props;
const { videoElement } = this.state;
if (
nextProps.userSeekTime
&& userSeekTime !== nextProps.userSeekTime
) {
if (videoElement) {
videoElement.currentTime = nextProps.userSeekTime;
}
}
2017-12-12 19:27:20 -07: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;
if (ratio !== 0) {
funcSeekToRatio();
}
2017-12-12 19:27:20 -07:00
}
onPlaySeek(offset) {
const { onPlaySeek } = this.props;
this.seekTime = offset;
onPlaySeek(offset);
2017-12-12 19:27:20 -07:00
}
onLoadStart() {
this.setState({
shouldShowJpeg: true,
isLoading: true
});
}
onLoadEnd() {
this.setState({
shouldShowJpeg: false,
isLoading: false
});
}
loadingOverlay() {
return (
<div className={css(Styles.loadingOverlay)}>
<img
className={css(Styles.loadingSpinner)}
src={`${process.env.PUBLIC_URL}/img/loading.svg`}
alt="Loading video"
/>
</div>
);
}
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]
const startTime = this.startTime();
if (currentTime < startTime) {
currentTime = startTime;
2017-06-13 18:40:05 -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) {
return ratio * this.videoLength() + this.startTime();
2017-12-12 19:27:20 -07:00
}
2017-06-13 18:40:05 -06:00
nearestFrameUrl() {
const { thumbnails } = this.props;
if (!this.seekTime) {
return '';
}
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}`;
}
}
return '';
2017-12-12 19:27:20 -07:00
}
render() {
const {
isLoading,
shouldShowJpeg,
videoElement
} = this.state;
const {
userSeekTime,
url,
playSpeed,
playing,
onVideoClick,
segmentIndices,
startTime,
segment
} = this.props;
2017-12-12 19:27:20 -07:00
return (
<div className="cabana-explorer-visuals-camera">
{isLoading ? this.loadingOverlay() : null}
{shouldShowJpeg ? (
2017-12-12 19:27:20 -07:00
<img
src={this.nearestFrameUrl()}
className={css(Styles.img)}
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(
url,
2019-08-09 16:48:54 -06:00
process.env.REACT_APP_VIDEO_CDN
).getRearCameraStreamIndexUrl()}
startTime={startTime || 0}
videoLength={this.videoLength()}
playbackSpeed={playSpeed}
2017-12-12 19:27:20 -07:00
onVideoElementAvailable={this.onVideoElementAvailable}
playing={playing}
onClick={onVideoClick}
onLoadStart={this.onLoadStart}
onLoadEnd={this.onLoadEnd}
onUserSeek={this.onUserSeek}
onPlaySeek={this.onPlaySeek}
2017-12-12 19:27:20 -07:00
segmentProgress={this.segmentProgress}
/>
<RouteSeeker
className={css(Styles.seekBar)}
nearestFrameTime={userSeekTime}
2017-12-12 19:27:20 -07:00
segmentProgress={this.segmentProgress}
startTime={this.startTime()}
videoLength={this.videoLength()}
segmentIndices={segmentIndices}
onUserSeek={this.onUserSeek}
onPlaySeek={this.onPlaySeek}
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}
segment={segment}
2017-12-12 19:27:20 -07:00
/>
</div>
);
}
}
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,
startTime: PropTypes.number
};