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 explorermain
parent
5c83305260
commit
f205520532
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
const WorkerLoaderPlugin = require('craco-worker-loader');
|
||||
const SentryPlugin = require('craco-sentry-plugin');
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
"build:staging": "env-cmd .env.staging craco build",
|
||||
"test": "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",
|
||||
"sass": "node-sass src/index.scss -o src && node-sass -w src/index.scss -o src",
|
||||
"deploy": "npm run build && gh-pages -d build"
|
||||
|
@ -123,7 +124,8 @@
|
|||
"^@commaai/pandajs$": "<rootDir>/node_modules/@commaai/pandajs/lib/index.js",
|
||||
"^@commaai/hls.js$": "<rootDir>/node_modules/@commaai/hls.js/dist/hls.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": {
|
||||
|
|
|
@ -34,10 +34,10 @@ import * as ObjectUtils from './utils/object';
|
|||
import { hash } from './utils/string';
|
||||
import { modifyQueryParameters } from './utils/url';
|
||||
|
||||
const RLogDownloader = require('./workers/rlog-downloader.worker.js');
|
||||
const LogCSVDownloader = require('./workers/dbc-csv-downloader.worker.js');
|
||||
const MessageParser = require('./workers/message-parser.worker.js');
|
||||
const CanStreamerWorker = require('./workers/CanStreamerWorker.worker.js');
|
||||
const RLogDownloader = require('./workers/rlog-downloader.worker');
|
||||
const LogCSVDownloader = require('./workers/dbc-csv-downloader.worker');
|
||||
const MessageParser = require('./workers/message-parser.worker');
|
||||
const CanStreamerWorker = require('./workers/CanStreamerWorker.worker');
|
||||
|
||||
export default class CanExplorer extends Component {
|
||||
constructor(props) {
|
||||
|
@ -65,7 +65,7 @@ export default class CanExplorer extends Component {
|
|||
dbcText: props.dbc ? props.dbc.text() : new DBC().text(),
|
||||
dbcFilename: props.dbcFilename ? props.dbcFilename : 'New_DBC',
|
||||
dbcLastSaved: null,
|
||||
seekTime: 0,
|
||||
seekTime: props.seekTime || 0,
|
||||
seekIndex: 0,
|
||||
maxByteStateChangeCount: 0,
|
||||
isLoading: true,
|
||||
|
@ -120,7 +120,7 @@ export default class CanExplorer extends Component {
|
|||
this.pandaReader.onMessage(this.processStreamedCanMessages);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { dongleId, name } = this.props;
|
||||
if (CommaAuth.isAuthenticated() && !name) {
|
||||
this.showOnboarding();
|
||||
|
@ -995,6 +995,8 @@ export default class CanExplorer extends Component {
|
|||
partsLoaded
|
||||
} = this.state;
|
||||
|
||||
const { startTime, segments } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="cabana"
|
||||
|
@ -1061,6 +1063,8 @@ export default class CanExplorer extends Component {
|
|||
canFrameOffset={canFrameOffset}
|
||||
firstCanTime={firstCanTime}
|
||||
seekTime={seekTime}
|
||||
startTime={startTime}
|
||||
startSegments={segments}
|
||||
seekIndex={seekIndex}
|
||||
currentParts={currentParts}
|
||||
selectedPart={currentPart}
|
||||
|
@ -1125,5 +1129,7 @@ CanExplorer.propTypes = {
|
|||
githubAuthToken: PropTypes.string,
|
||||
autoplay: PropTypes.bool,
|
||||
max: PropTypes.number,
|
||||
url: PropTypes.string
|
||||
url: PropTypes.string,
|
||||
startTime: PropTypes.number,
|
||||
segments: PropTypes.array
|
||||
};
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint-env jest */
|
||||
class Worker {
|
||||
postMessage = jest.fn()
|
||||
}
|
||||
|
||||
module.exports = Worker;
|
|
@ -1,10 +1,55 @@
|
|||
/* eslint-env jest */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React from 'react';
|
||||
import { shallow, mount, render } from 'enzyme';
|
||||
import { StyleSheetTestUtils } from 'aphrodite';
|
||||
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', () => {
|
||||
const canExplorer = shallow(<CanExplorer />);
|
||||
global.document.querySelector = jest.fn();
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,38 +9,75 @@ import RouteVideoSync from './RouteVideoSync';
|
|||
import CanLog from './CanLog';
|
||||
import Entries from '../models/can/entries';
|
||||
import debounce from '../utils/debounce';
|
||||
import PartSelector from './PartSelector';
|
||||
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 {
|
||||
static propTypes = {
|
||||
selectedMessage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
live: PropTypes.bool.isRequired,
|
||||
messages: PropTypes.objectOf(PropTypes.object),
|
||||
onConfirmedSignalChange: PropTypes.func.isRequired,
|
||||
canFrameOffset: PropTypes.number,
|
||||
firstCanTime: PropTypes.number,
|
||||
onSeek: PropTypes.func.isRequired,
|
||||
autoplay: PropTypes.bool.isRequired,
|
||||
onPartChange: PropTypes.func.isRequired,
|
||||
partsCount: PropTypes.number
|
||||
};
|
||||
updateSegment = debounce((messageId, _segment) => {
|
||||
let segment = _segment;
|
||||
const { messages, selectedMessage, currentParts } = this.props;
|
||||
const { entries } = messages[selectedMessage];
|
||||
let segmentIndices = Entries.findSegmentIndices(entries, segment, true);
|
||||
|
||||
// console.log(this.state.segment, '->', segment, segmentIndices);
|
||||
if (
|
||||
segment[0] === currentParts[0] * 60
|
||||
&& segment[1] === (currentParts[1] + 1) * 60
|
||||
) {
|
||||
segment = [];
|
||||
segmentIndices = [];
|
||||
}
|
||||
this.setState({
|
||||
segment,
|
||||
segmentIndices,
|
||||
userSeekIndex: segmentIndices[0],
|
||||
userSeekTime: segment[0] || 0
|
||||
});
|
||||
}, 250);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
plottedSignals: [],
|
||||
segment: [],
|
||||
segment: props.startSegments || [],
|
||||
segmentIndices: [],
|
||||
shouldShowAddSignal: true,
|
||||
userSeekIndex: 0,
|
||||
userSeekTime: 0,
|
||||
playing: props.autoplay,
|
||||
signals: {},
|
||||
playSpeed: 1
|
||||
};
|
||||
|
||||
this.onSignalPlotPressed = this.onSignalPlotPressed.bind(this);
|
||||
this.onSignalUnplotPressed = this.onSignalUnplotPressed.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.onVideoClick = this.onVideoClick.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.toggleShouldShowAddSignal = this.toggleShouldShowAddSignal.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) {
|
||||
// escape
|
||||
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) {
|
||||
const nextMessage = nextProps.messages[nextProps.selectedMessage];
|
||||
const curMessage = this.props.messages[this.props.selectedMessage];
|
||||
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();
|
||||
}
|
||||
if (nextMessage && nextMessage.frame && nextMessage !== curMessage) {
|
||||
|
@ -142,7 +151,7 @@ export default class Explorer extends Component {
|
|||
// by finding a entry indices
|
||||
// corresponding to old message segment/seek times.
|
||||
|
||||
const { segment, segmentIndices } = this.clipSegment(
|
||||
const { segment, segmentIndices } = clipSegment(
|
||||
this.state.segment,
|
||||
this.state.segmentIndices,
|
||||
nextMessage
|
||||
|
@ -171,7 +180,7 @@ export default class Explorer extends Component {
|
|||
&& curMessage
|
||||
&& nextMessage.entries.length !== curMessage.entries.length
|
||||
) {
|
||||
const { segment, segmentIndices } = this.clipSegment(
|
||||
const { segment, segmentIndices } = clipSegment(
|
||||
this.state.segment,
|
||||
this.state.segmentIndices,
|
||||
nextMessage
|
||||
|
@ -227,26 +236,6 @@ export default class Explorer extends Component {
|
|||
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) {
|
||||
if (Array.isArray(segment)) {
|
||||
this.updateSegment(messageId, segment);
|
||||
|
@ -255,7 +244,6 @@ export default class Explorer extends Component {
|
|||
|
||||
resetSegment() {
|
||||
const { segment, segmentIndices } = this.state;
|
||||
const { messages, selectedMessage } = this.props;
|
||||
if (segment.length > 0 || segmentIndices.length > 0) {
|
||||
// console.log(this.state.segment, '->', segment, segmentIndices);
|
||||
this.setState({
|
||||
|
@ -460,7 +448,7 @@ export default class Explorer extends Component {
|
|||
? 'is-expanded'
|
||||
: null;
|
||||
|
||||
const { thumbnails, messages } = this.props;
|
||||
const { thumbnails, messages, startTime } = this.props;
|
||||
|
||||
let graphSegment = this.state.segment;
|
||||
if (!graphSegment.length && this.props.currentParts) {
|
||||
|
@ -489,6 +477,7 @@ export default class Explorer extends Component {
|
|||
<RouteVideoSync
|
||||
message={messages[this.props.selectedMessage]}
|
||||
segment={this.state.segment}
|
||||
startTime={startTime}
|
||||
seekIndex={this.props.seekIndex}
|
||||
userSeekIndex={this.state.userSeekIndex}
|
||||
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
|
||||
};
|
||||
|
|
|
@ -31,7 +31,7 @@ export default class GithubDbcList extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.props.openDbcClient.list(this.props.repo).then((paths) => {
|
||||
paths = paths.filter((path) => path.indexOf('.dbc') !== -1);
|
||||
this.setState({ paths });
|
||||
|
|
|
@ -54,7 +54,7 @@ export default class Meta extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.lastSavedTimer = setInterval(() => {
|
||||
if (this.props.dbcLastSaved !== null) {
|
||||
this.setState({ lastSaved: this.props.dbcLastSaved.fromNow() });
|
||||
|
|
|
@ -205,7 +205,8 @@ export default class RouteVideoSync extends Component {
|
|||
const {
|
||||
isLoading,
|
||||
shouldRestartHls,
|
||||
shouldShowJpeg
|
||||
shouldShowJpeg,
|
||||
videoElement
|
||||
} = this.state;
|
||||
const {
|
||||
userSeekTime,
|
||||
|
@ -213,7 +214,8 @@ export default class RouteVideoSync extends Component {
|
|||
playSpeed,
|
||||
playing,
|
||||
onVideoClick,
|
||||
segmentIndices
|
||||
segmentIndices,
|
||||
startTime
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="cabana-explorer-visuals-camera">
|
||||
|
@ -231,7 +233,7 @@ export default class RouteVideoSync extends Component {
|
|||
url,
|
||||
process.env.REACT_APP_VIDEO_CDN
|
||||
).getRearCameraStreamIndexUrl()}
|
||||
startTime={this.startTime()}
|
||||
startTime={startTime || 0}
|
||||
videoLength={this.videoLength()}
|
||||
playbackSpeed={playSpeed}
|
||||
onVideoElementAvailable={this.onVideoElementAvailable}
|
||||
|
@ -254,7 +256,7 @@ export default class RouteVideoSync extends Component {
|
|||
segmentIndices={segmentIndices}
|
||||
onUserSeek={this.onUserSeek}
|
||||
onPlaySeek={this.onPlaySeek}
|
||||
videoElement={this.state.videoElement}
|
||||
videoElement={videoElement}
|
||||
onPlay={this.props.onPlay}
|
||||
onPause={this.props.onPause}
|
||||
playing={this.props.playing}
|
||||
|
@ -281,4 +283,5 @@ RouteVideoSync.propTypes = {
|
|||
playSpeed: PropTypes.number.isRequired,
|
||||
onVideoClick: PropTypes.func,
|
||||
segmentIndices: PropTypes.array,
|
||||
startTime: PropTypes.number
|
||||
};
|
||||
|
|
16
src/index.js
16
src/index.js
|
@ -18,7 +18,19 @@ Sentry.init();
|
|||
|
||||
const routeFullName = getUrlParameter('route');
|
||||
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;
|
||||
|
||||
if (routeFullName) {
|
||||
|
@ -96,7 +108,7 @@ async function init() {
|
|||
if (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) {
|
||||
|
|
|
@ -3,9 +3,10 @@ import Raven from 'raven-js';
|
|||
function init() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const opts = {};
|
||||
const webpackHash = __webpack_hash__; // eslint-disable-line
|
||||
|
||||
if (typeof __webpack_hash__ !== 'undefined') {
|
||||
opts.release = __webpack_hash__; // eslint-disable-line no-undef
|
||||
if (typeof webpackHash !== 'undefined') {
|
||||
opts.release = webpackHash;
|
||||
}
|
||||
|
||||
Raven.config(
|
||||
|
|
Loading…
Reference in New Issue