Add support for seekTime and segments url parameters (#29)

* Add coverage script for convenience

* Add support for seekTime url parameter

* Support preset segment as well

* Test props pumping through can explorer to explorer
main
Chris Vickery 2019-10-09 15:22:46 -07:00 committed by GitHub
parent 5c83305260
commit f205520532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 182 additions and 100 deletions

View File

@ -1,3 +1,4 @@
/* eslint-disable */
const WorkerLoaderPlugin = require('craco-worker-loader'); const WorkerLoaderPlugin = require('craco-worker-loader');
const SentryPlugin = require('craco-sentry-plugin'); const SentryPlugin = require('craco-sentry-plugin');

View File

@ -92,6 +92,7 @@
"build:staging": "env-cmd .env.staging craco build", "build:staging": "env-cmd .env.staging craco build",
"test": "craco test --env=jsdom", "test": "craco test --env=jsdom",
"test-ci": "CI=true craco test --env=jsdom", "test-ci": "CI=true craco test --env=jsdom",
"test-coverage": "CI=true craco test --env=jsdom --coverage",
"netlify-sass": "node-sass src/index.scss > src/index.css", "netlify-sass": "node-sass src/index.scss > src/index.css",
"sass": "node-sass src/index.scss -o src && node-sass -w src/index.scss -o src", "sass": "node-sass src/index.scss -o src && node-sass -w src/index.scss -o src",
"deploy": "npm run build && gh-pages -d build" "deploy": "npm run build && gh-pages -d build"
@ -123,7 +124,8 @@
"^@commaai/pandajs$": "<rootDir>/node_modules/@commaai/pandajs/lib/index.js", "^@commaai/pandajs$": "<rootDir>/node_modules/@commaai/pandajs/lib/index.js",
"^@commaai/hls.js$": "<rootDir>/node_modules/@commaai/hls.js/dist/hls.js", "^@commaai/hls.js$": "<rootDir>/node_modules/@commaai/hls.js/dist/hls.js",
"^@commaai/(.*comma.*)$": "<rootDir>/node_modules/@commaai/$1/dist/index.js", "^@commaai/(.*comma.*)$": "<rootDir>/node_modules/@commaai/$1/dist/index.js",
"^capnp-split$": "<rootDir>/node_modules/capnp-split/dist/index.js" "^capnp-split$": "<rootDir>/node_modules/capnp-split/dist/index.js",
"\\.worker": "<rootDir>/src/__mocks__/workerMock.js"
} }
}, },
"browserslist": { "browserslist": {

View File

@ -34,10 +34,10 @@ import * as ObjectUtils from './utils/object';
import { hash } from './utils/string'; import { hash } from './utils/string';
import { modifyQueryParameters } from './utils/url'; import { modifyQueryParameters } from './utils/url';
const RLogDownloader = require('./workers/rlog-downloader.worker.js'); const RLogDownloader = require('./workers/rlog-downloader.worker');
const LogCSVDownloader = require('./workers/dbc-csv-downloader.worker.js'); const LogCSVDownloader = require('./workers/dbc-csv-downloader.worker');
const MessageParser = require('./workers/message-parser.worker.js'); const MessageParser = require('./workers/message-parser.worker');
const CanStreamerWorker = require('./workers/CanStreamerWorker.worker.js'); const CanStreamerWorker = require('./workers/CanStreamerWorker.worker');
export default class CanExplorer extends Component { export default class CanExplorer extends Component {
constructor(props) { constructor(props) {
@ -65,7 +65,7 @@ export default class CanExplorer extends Component {
dbcText: props.dbc ? props.dbc.text() : new DBC().text(), dbcText: props.dbc ? props.dbc.text() : new DBC().text(),
dbcFilename: props.dbcFilename ? props.dbcFilename : 'New_DBC', dbcFilename: props.dbcFilename ? props.dbcFilename : 'New_DBC',
dbcLastSaved: null, dbcLastSaved: null,
seekTime: 0, seekTime: props.seekTime || 0,
seekIndex: 0, seekIndex: 0,
maxByteStateChangeCount: 0, maxByteStateChangeCount: 0,
isLoading: true, isLoading: true,
@ -120,7 +120,7 @@ export default class CanExplorer extends Component {
this.pandaReader.onMessage(this.processStreamedCanMessages); this.pandaReader.onMessage(this.processStreamedCanMessages);
} }
componentWillMount() { componentDidMount() {
const { dongleId, name } = this.props; const { dongleId, name } = this.props;
if (CommaAuth.isAuthenticated() && !name) { if (CommaAuth.isAuthenticated() && !name) {
this.showOnboarding(); this.showOnboarding();
@ -995,6 +995,8 @@ export default class CanExplorer extends Component {
partsLoaded partsLoaded
} = this.state; } = this.state;
const { startTime, segments } = this.props;
return ( return (
<div <div
id="cabana" id="cabana"
@ -1061,6 +1063,8 @@ export default class CanExplorer extends Component {
canFrameOffset={canFrameOffset} canFrameOffset={canFrameOffset}
firstCanTime={firstCanTime} firstCanTime={firstCanTime}
seekTime={seekTime} seekTime={seekTime}
startTime={startTime}
startSegments={segments}
seekIndex={seekIndex} seekIndex={seekIndex}
currentParts={currentParts} currentParts={currentParts}
selectedPart={currentPart} selectedPart={currentPart}
@ -1125,5 +1129,7 @@ CanExplorer.propTypes = {
githubAuthToken: PropTypes.string, githubAuthToken: PropTypes.string,
autoplay: PropTypes.bool, autoplay: PropTypes.bool,
max: PropTypes.number, max: PropTypes.number,
url: PropTypes.string url: PropTypes.string,
startTime: PropTypes.number,
segments: PropTypes.array
}; };

View File

@ -0,0 +1,6 @@
/* eslint-env jest */
class Worker {
postMessage = jest.fn()
}
module.exports = Worker;

View File

@ -1,10 +1,55 @@
/* eslint-env jest */
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react'; import React from 'react';
import { shallow, mount, render } from 'enzyme'; import { shallow, mount, render } from 'enzyme';
import { StyleSheetTestUtils } from 'aphrodite'; import { StyleSheetTestUtils } from 'aphrodite';
import CanExplorer from '../../CanExplorer'; import CanExplorer from '../../CanExplorer';
import Explorer from '../../components/Explorer';
import AcuraDbc from '../../acura-dbc';
global.__JEST__ = 1; jest.mock('aphrodite/lib/inject');
jest.mock('../../components/HLS.js');
jest.mock('hls.js');
test('CanExplorer renders', () => { global.document.querySelector = jest.fn();
const canExplorer = shallow(<CanExplorer />);
describe('CanExplorer', () => {
const props = {
max: 12,
url: 'https://chffrprivate.blob.core.windows.net/chffrprivate3-permanent/v2/cb38263377b873ee/78392b99580c5920227cc5b43dff8a70_2017-06-12--18-51-47',
name: '2017-06-12--18-51-47',
dongleId: 'cb38263377b873ee',
dbc: AcuraDbc,
isDemo: true,
dbcFilename: 'acura_ilx_2016_can.dbc',
autoplay: true
};
it('renders', () => {
/*
dongleId: PropTypes.string,
name: PropTypes.string,
dbc: PropTypes.instanceOf(DBC),
dbcFilename: PropTypes.string,
githubAuthToken: PropTypes.string,
autoplay: PropTypes.bool,
max: PropTypes.number,
url: PropTypes.string,
startTime: PropTypes.number,
segments: PropTypes.array
*/
const canExplorer = mount(<CanExplorer {...props} />);
expect(canExplorer.exists()).toBe(true);
expect(canExplorer.find(Explorer).length).toBe(1);
canExplorer.unmount();
});
it('passes props to Explorer', () => {
const canExplorer = mount(<CanExplorer {...props} segments={[123, 321]} startTime={150} />);
expect(canExplorer.find(Explorer).length).toBe(1);
expect(canExplorer.exists()).toBe(true);
expect(canExplorer.find(Explorer).prop('startSegments')).toBe(canExplorer.prop('segments'));
expect(canExplorer.find(Explorer).prop('startTime')).toBe(canExplorer.prop('startTime'));
canExplorer.unmount();
});
}); });

View File

@ -9,38 +9,75 @@ import RouteVideoSync from './RouteVideoSync';
import CanLog from './CanLog'; import CanLog from './CanLog';
import Entries from '../models/can/entries'; import Entries from '../models/can/entries';
import debounce from '../utils/debounce'; import debounce from '../utils/debounce';
import PartSelector from './PartSelector';
import PlaySpeedSelector from './PlaySpeedSelector'; import PlaySpeedSelector from './PlaySpeedSelector';
function clipSegment(_segment, _segmentIndices, nextMessage) {
let segment = _segment;
let segmentIndices = _segmentIndices;
if (segment.length === 2) {
const segmentStartIdx = nextMessage.entries.findIndex(
(e) => e.relTime >= segment[0]
);
let segmentEndIdx = nextMessage.entries.findIndex(
(e) => e.relTime >= segment[1]
);
if (segmentStartIdx !== -1) {
if (segmentEndIdx === -1) {
// previous segment end is past bounds of this message
segmentEndIdx = nextMessage.entries.length - 1;
}
const segmentStartTime = nextMessage.entries[segmentStartIdx].relTime;
const segmentEndTime = nextMessage.entries[segmentEndIdx].relTime;
segment = [segmentStartTime, segmentEndTime];
segmentIndices = [segmentStartIdx, segmentEndIdx];
} else {
// segment times are out of boudns for this message
segment = [];
segmentIndices = [];
}
}
return { segment, segmentIndices };
}
export default class Explorer extends Component { export default class Explorer extends Component {
static propTypes = { updateSegment = debounce((messageId, _segment) => {
selectedMessage: PropTypes.string, let segment = _segment;
url: PropTypes.string, const { messages, selectedMessage, currentParts } = this.props;
live: PropTypes.bool.isRequired, const { entries } = messages[selectedMessage];
messages: PropTypes.objectOf(PropTypes.object), let segmentIndices = Entries.findSegmentIndices(entries, segment, true);
onConfirmedSignalChange: PropTypes.func.isRequired,
canFrameOffset: PropTypes.number, // console.log(this.state.segment, '->', segment, segmentIndices);
firstCanTime: PropTypes.number, if (
onSeek: PropTypes.func.isRequired, segment[0] === currentParts[0] * 60
autoplay: PropTypes.bool.isRequired, && segment[1] === (currentParts[1] + 1) * 60
onPartChange: PropTypes.func.isRequired, ) {
partsCount: PropTypes.number segment = [];
}; segmentIndices = [];
}
this.setState({
segment,
segmentIndices,
userSeekIndex: segmentIndices[0],
userSeekTime: segment[0] || 0
});
}, 250);
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
plottedSignals: [], plottedSignals: [],
segment: [], segment: props.startSegments || [],
segmentIndices: [], segmentIndices: [],
shouldShowAddSignal: true, shouldShowAddSignal: true,
userSeekIndex: 0, userSeekIndex: 0,
userSeekTime: 0, userSeekTime: 0,
playing: props.autoplay, playing: props.autoplay,
signals: {},
playSpeed: 1 playSpeed: 1
}; };
this.onSignalPlotPressed = this.onSignalPlotPressed.bind(this); this.onSignalPlotPressed = this.onSignalPlotPressed.bind(this);
this.onSignalUnplotPressed = this.onSignalUnplotPressed.bind(this); this.onSignalUnplotPressed = this.onSignalUnplotPressed.bind(this);
this.onSegmentChanged = this.onSegmentChanged.bind(this); this.onSegmentChanged = this.onSegmentChanged.bind(this);
@ -52,61 +89,33 @@ export default class Explorer extends Component {
this.onPause = this.onPause.bind(this); this.onPause = this.onPause.bind(this);
this.onVideoClick = this.onVideoClick.bind(this); this.onVideoClick = this.onVideoClick.bind(this);
this.onSignalPlotChange = this.onSignalPlotChange.bind(this); this.onSignalPlotChange = this.onSignalPlotChange.bind(this);
this._onKeyDown = this._onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
this.mergePlots = this.mergePlots.bind(this); this.mergePlots = this.mergePlots.bind(this);
this.toggleShouldShowAddSignal = this.toggleShouldShowAddSignal.bind(this); this.toggleShouldShowAddSignal = this.toggleShouldShowAddSignal.bind(this);
this.changePlaySpeed = this.changePlaySpeed.bind(this); this.changePlaySpeed = this.changePlaySpeed.bind(this);
} }
_onKeyDown(e) { componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
}
onKeyDown(e) {
if (e.keyCode === 27) { if (e.keyCode === 27) {
// escape // escape
this.resetSegment(); this.resetSegment();
} }
} }
componentWillMount() {
document.addEventListener('keydown', this._onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onKeyDown);
}
clipSegment(segment, segmentIndices, nextMessage) {
if (segment.length === 2) {
const segmentStartIdx = nextMessage.entries.findIndex(
(e) => e.relTime >= segment[0]
);
let segmentEndIdx = nextMessage.entries.findIndex(
(e) => e.relTime >= segment[1]
);
if (segmentStartIdx !== -1) {
if (segmentEndIdx === -1) {
// previous segment end is past bounds of this message
segmentEndIdx = nextMessage.entries.length - 1;
}
const segmentStartTime = nextMessage.entries[segmentStartIdx].relTime;
const segmentEndTime = nextMessage.entries[segmentEndIdx].relTime;
segment = [segmentStartTime, segmentEndTime];
segmentIndices = [segmentStartIdx, segmentEndIdx];
} else {
// segment times are out of boudns for this message
segment = [];
segmentIndices = [];
}
}
return { segment, segmentIndices };
}
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const nextMessage = nextProps.messages[nextProps.selectedMessage]; const nextMessage = nextProps.messages[nextProps.selectedMessage];
const curMessage = this.props.messages[this.props.selectedMessage]; const curMessage = this.props.messages[this.props.selectedMessage];
let { plottedSignals } = this.state; let { plottedSignals } = this.state;
if (Object.keys(nextProps.messages).length === 0) { if (Object.keys(nextProps.messages).length === 0 && Object.keys(this.props.messages).length !== 0) {
this.resetSegment(); this.resetSegment();
} }
if (nextMessage && nextMessage.frame && nextMessage !== curMessage) { if (nextMessage && nextMessage.frame && nextMessage !== curMessage) {
@ -142,7 +151,7 @@ export default class Explorer extends Component {
// by finding a entry indices // by finding a entry indices
// corresponding to old message segment/seek times. // corresponding to old message segment/seek times.
const { segment, segmentIndices } = this.clipSegment( const { segment, segmentIndices } = clipSegment(
this.state.segment, this.state.segment,
this.state.segmentIndices, this.state.segmentIndices,
nextMessage nextMessage
@ -171,7 +180,7 @@ export default class Explorer extends Component {
&& curMessage && curMessage
&& nextMessage.entries.length !== curMessage.entries.length && nextMessage.entries.length !== curMessage.entries.length
) { ) {
const { segment, segmentIndices } = this.clipSegment( const { segment, segmentIndices } = clipSegment(
this.state.segment, this.state.segment,
this.state.segmentIndices, this.state.segmentIndices,
nextMessage nextMessage
@ -227,26 +236,6 @@ export default class Explorer extends Component {
this.setState({ plottedSignals: newPlottedSignals }); this.setState({ plottedSignals: newPlottedSignals });
} }
updateSegment = debounce((messageId, segment) => {
const { entries } = this.props.messages[this.props.selectedMessage];
let segmentIndices = Entries.findSegmentIndices(entries, segment, true);
// console.log(this.state.segment, '->', segment, segmentIndices);
if (
segment[0] === this.props.currentParts[0] * 60
&& segment[1] === (this.props.currentParts[1] + 1) * 60
) {
segment = [];
segmentIndices = [];
}
this.setState({
segment,
segmentIndices,
userSeekIndex: segmentIndices[0],
userSeekTime: segment[0] || 0
});
}, 250);
onSegmentChanged(messageId, segment) { onSegmentChanged(messageId, segment) {
if (Array.isArray(segment)) { if (Array.isArray(segment)) {
this.updateSegment(messageId, segment); this.updateSegment(messageId, segment);
@ -255,7 +244,6 @@ export default class Explorer extends Component {
resetSegment() { resetSegment() {
const { segment, segmentIndices } = this.state; const { segment, segmentIndices } = this.state;
const { messages, selectedMessage } = this.props;
if (segment.length > 0 || segmentIndices.length > 0) { if (segment.length > 0 || segmentIndices.length > 0) {
// console.log(this.state.segment, '->', segment, segmentIndices); // console.log(this.state.segment, '->', segment, segmentIndices);
this.setState({ this.setState({
@ -460,7 +448,7 @@ export default class Explorer extends Component {
? 'is-expanded' ? 'is-expanded'
: null; : null;
const { thumbnails, messages } = this.props; const { thumbnails, messages, startTime } = this.props;
let graphSegment = this.state.segment; let graphSegment = this.state.segment;
if (!graphSegment.length && this.props.currentParts) { if (!graphSegment.length && this.props.currentParts) {
@ -489,6 +477,7 @@ export default class Explorer extends Component {
<RouteVideoSync <RouteVideoSync
message={messages[this.props.selectedMessage]} message={messages[this.props.selectedMessage]}
segment={this.state.segment} segment={this.state.segment}
startTime={startTime}
seekIndex={this.props.seekIndex} seekIndex={this.props.seekIndex}
userSeekIndex={this.state.userSeekIndex} userSeekIndex={this.state.userSeekIndex}
playing={this.state.playing} playing={this.state.playing}
@ -532,3 +521,20 @@ export default class Explorer extends Component {
); );
} }
} }
Explorer.propTypes = {
selectedMessage: PropTypes.string,
url: PropTypes.string,
live: PropTypes.bool.isRequired,
messages: PropTypes.objectOf(PropTypes.object),
thumbnails: PropTypes.array.isRequired,
onConfirmedSignalChange: PropTypes.func.isRequired,
canFrameOffset: PropTypes.number,
firstCanTime: PropTypes.number,
onSeek: PropTypes.func.isRequired,
autoplay: PropTypes.bool.isRequired,
currentParts: PropTypes.array.isRequired,
partsCount: PropTypes.number,
startTime: PropTypes.number,
startSegments: PropTypes.array
};

View File

@ -31,7 +31,7 @@ export default class GithubDbcList extends Component {
} }
} }
componentWillMount() { componentDidMount() {
this.props.openDbcClient.list(this.props.repo).then((paths) => { this.props.openDbcClient.list(this.props.repo).then((paths) => {
paths = paths.filter((path) => path.indexOf('.dbc') !== -1); paths = paths.filter((path) => path.indexOf('.dbc') !== -1);
this.setState({ paths }); this.setState({ paths });

View File

@ -54,7 +54,7 @@ export default class Meta extends Component {
}; };
} }
componentWillMount() { componentDidMount() {
this.lastSavedTimer = setInterval(() => { this.lastSavedTimer = setInterval(() => {
if (this.props.dbcLastSaved !== null) { if (this.props.dbcLastSaved !== null) {
this.setState({ lastSaved: this.props.dbcLastSaved.fromNow() }); this.setState({ lastSaved: this.props.dbcLastSaved.fromNow() });

View File

@ -205,7 +205,8 @@ export default class RouteVideoSync extends Component {
const { const {
isLoading, isLoading,
shouldRestartHls, shouldRestartHls,
shouldShowJpeg shouldShowJpeg,
videoElement
} = this.state; } = this.state;
const { const {
userSeekTime, userSeekTime,
@ -213,7 +214,8 @@ export default class RouteVideoSync extends Component {
playSpeed, playSpeed,
playing, playing,
onVideoClick, onVideoClick,
segmentIndices segmentIndices,
startTime
} = this.props; } = this.props;
return ( return (
<div className="cabana-explorer-visuals-camera"> <div className="cabana-explorer-visuals-camera">
@ -231,7 +233,7 @@ export default class RouteVideoSync extends Component {
url, url,
process.env.REACT_APP_VIDEO_CDN process.env.REACT_APP_VIDEO_CDN
).getRearCameraStreamIndexUrl()} ).getRearCameraStreamIndexUrl()}
startTime={this.startTime()} startTime={startTime || 0}
videoLength={this.videoLength()} videoLength={this.videoLength()}
playbackSpeed={playSpeed} playbackSpeed={playSpeed}
onVideoElementAvailable={this.onVideoElementAvailable} onVideoElementAvailable={this.onVideoElementAvailable}
@ -254,7 +256,7 @@ export default class RouteVideoSync extends Component {
segmentIndices={segmentIndices} segmentIndices={segmentIndices}
onUserSeek={this.onUserSeek} onUserSeek={this.onUserSeek}
onPlaySeek={this.onPlaySeek} onPlaySeek={this.onPlaySeek}
videoElement={this.state.videoElement} videoElement={videoElement}
onPlay={this.props.onPlay} onPlay={this.props.onPlay}
onPause={this.props.onPause} onPause={this.props.onPause}
playing={this.props.playing} playing={this.props.playing}
@ -281,4 +283,5 @@ RouteVideoSync.propTypes = {
playSpeed: PropTypes.number.isRequired, playSpeed: PropTypes.number.isRequired,
onVideoClick: PropTypes.func, onVideoClick: PropTypes.func,
segmentIndices: PropTypes.array, segmentIndices: PropTypes.array,
startTime: PropTypes.number
}; };

View File

@ -18,7 +18,19 @@ Sentry.init();
const routeFullName = getUrlParameter('route'); const routeFullName = getUrlParameter('route');
const isDemo = !routeFullName; const isDemo = !routeFullName;
const props = { autoplay: true, isDemo }; let segments = getUrlParameter('segments');
if (segments && segments.length) {
segments = segments.split(',').map(Number);
}
if (segments.length !== 2) {
segments = undefined;
}
const props = {
autoplay: true,
startTime: Number(getUrlParameter('seekTime') || 0),
segments,
isDemo
};
let persistedDbc = null; let persistedDbc = null;
if (routeFullName) { if (routeFullName) {
@ -96,7 +108,7 @@ async function init() {
if (token) { if (token) {
Request.configure(token); Request.configure(token);
} }
ReactDOM.render(<CanExplorer {...props} />, document.getElementById('root')); ReactDOM.render(<CanExplorer {...props} />, document.getElementById('root')); // eslint-disable-line react/jsx-props-no-spreading
} }
if (routeFullName || isDemo) { if (routeFullName || isDemo) {

View File

@ -3,9 +3,10 @@ import Raven from 'raven-js';
function init() { function init() {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
const opts = {}; const opts = {};
const webpackHash = __webpack_hash__; // eslint-disable-line
if (typeof __webpack_hash__ !== 'undefined') { if (typeof webpackHash !== 'undefined') {
opts.release = __webpack_hash__; // eslint-disable-line no-undef opts.release = webpackHash;
} }
Raven.config( Raven.config(