Refactor graph data flow, re-add subsection looping and seeking (#19)
* Close web workers when they're done * Have graphs generate their own data, insert new data better * Replace all messages in graphs to handle out of order signals * Fix bugs, add video looping * Fix looping a bit, optimizations * Fix test cases * Remove debug statements * Make jest comma package workaround more genericmain
parent
58c77f2aca
commit
edf84da5ab
17
.babelrc
17
.babelrc
|
@ -7,10 +7,23 @@
|
||||||
"targets": {
|
"targets": {
|
||||||
"browsers": ["last 2 versions", "safari >= 7"]
|
"browsers": ["last 2 versions", "safari >= 7"]
|
||||||
},
|
},
|
||||||
useBuiltIns: "usage"
|
"useBuiltIns": "usage"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stage-0"
|
"stage-0"
|
||||||
],
|
],
|
||||||
"plugins": ["emotion"]
|
"plugins": ["emotion"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"env",
|
||||||
|
{
|
||||||
|
"modules": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stage-0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://community.comma.ai/cabana",
|
"homepage": "https://community.comma.ai/cabana",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@commaai/comma-api": "^1.1.2",
|
"@commaai/comma-api": "^1.1.4",
|
||||||
"@commaai/hls.js": "^0.12.2",
|
"@commaai/hls.js": "^0.12.2",
|
||||||
"@commaai/log_reader": "^0.3.1",
|
"@commaai/log_reader": "^0.3.1",
|
||||||
"@commaai/my-comma-auth": "^1.1.0",
|
"@commaai/my-comma-auth": "^1.1.0",
|
||||||
|
@ -90,5 +90,10 @@
|
||||||
"*.{graphql,gql}": ["prettier --parser graphql --write", "git add"],
|
"*.{graphql,gql}": ["prettier --parser graphql --write", "git add"],
|
||||||
"*.{md,markdown}": ["prettier --parser markdown --write", "git add"],
|
"*.{md,markdown}": ["prettier --parser markdown --write", "git add"],
|
||||||
"*.scss": ["prettier --parser postcss --write", "git add"]
|
"*.scss": ["prettier --parser postcss --write", "git add"]
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@commaai/(.*)$": "<rootDir>/node_modules/@commaai/$1/dist/index.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -673,8 +673,14 @@ export default class CanExplorer extends Component {
|
||||||
currentParts = [minPart, maxPart];
|
currentParts = [minPart, maxPart];
|
||||||
currentPart = part;
|
currentPart = part;
|
||||||
|
|
||||||
// update state then load new parts
|
if (
|
||||||
this.setState({ currentParts, currentPart }, this.partChangeDebounced);
|
currentPart !== this.state.currentPart ||
|
||||||
|
currentParts[0] !== this.state.currentParts[0] ||
|
||||||
|
currentParts[1] !== this.state.currentParts[1]
|
||||||
|
) {
|
||||||
|
// update state then load new parts
|
||||||
|
this.setState({ currentParts, currentPart }, this.partChangeDebounced);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showEditMessageModal(msgKey) {
|
showEditMessageModal(msgKey) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { shallow, mount, render } from "enzyme";
|
||||||
|
|
||||||
test("PartSelector successfully mounts with minimal default props", () => {
|
test("PartSelector successfully mounts with minimal default props", () => {
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<PartSelector onPartChange={() => {}} partsCount={0} />
|
<PartSelector onPartChange={() => {}} partsCount={0} selectedPart={0} />
|
||||||
);
|
);
|
||||||
expect(component.exists()).toBe(true);
|
expect(component.exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ test("RouteSeeker successfully mounts with minimal default props", () => {
|
||||||
<RouteSeeker
|
<RouteSeeker
|
||||||
nearestFrameTime={0}
|
nearestFrameTime={0}
|
||||||
segmentProgress={() => {}}
|
segmentProgress={() => {}}
|
||||||
secondsLoaded={0}
|
videoLength={0}
|
||||||
segmentIndices={[]}
|
segmentIndices={[]}
|
||||||
onUserSeek={() => {}}
|
onUserSeek={() => {}}
|
||||||
onPlaySeek={() => {}}
|
onPlaySeek={() => {}}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
global.__JEST__ = 1;
|
global.__JEST__ = 1;
|
||||||
|
|
||||||
|
import API from "@commaai/comma-api";
|
||||||
import RouteVideoSync from "../../components/RouteVideoSync";
|
import RouteVideoSync from "../../components/RouteVideoSync";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow, mount, render } from "enzyme";
|
import { shallow, mount, render } from "enzyme";
|
||||||
|
@ -20,9 +21,11 @@ test("RouteVideoSync successfully mounts with minimal default props", () => {
|
||||||
message={null}
|
message={null}
|
||||||
secondsLoaded={0}
|
secondsLoaded={0}
|
||||||
startOffset={0}
|
startOffset={0}
|
||||||
|
segment={[]}
|
||||||
seekIndex={0}
|
seekIndex={0}
|
||||||
userSeekIndex={0}
|
userSeekIndex={0}
|
||||||
playing={false}
|
playing={false}
|
||||||
|
playSpeed={1}
|
||||||
url={"http://comma.ai"}
|
url={"http://comma.ai"}
|
||||||
canFrameOffset={0}
|
canFrameOffset={0}
|
||||||
firstCanTime={0}
|
firstCanTime={0}
|
||||||
|
|
|
@ -4,7 +4,9 @@ import PropTypes from "prop-types";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
import Signal from "../models/can/signal";
|
import Signal from "../models/can/signal";
|
||||||
|
import GraphData from "../models/graph-data";
|
||||||
import CanPlot from "../vega/CanPlot";
|
import CanPlot from "../vega/CanPlot";
|
||||||
|
import debounce from "../utils/debounce";
|
||||||
|
|
||||||
const DefaultPlotInnerStyle = {
|
const DefaultPlotInnerStyle = {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
@ -16,7 +18,7 @@ export default class CanGraph extends Component {
|
||||||
static emptyTable = [];
|
static emptyTable = [];
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
data: PropTypes.object,
|
plottedSignal: PropTypes.string,
|
||||||
messages: PropTypes.object,
|
messages: PropTypes.object,
|
||||||
messageId: PropTypes.string,
|
messageId: PropTypes.string,
|
||||||
messageName: PropTypes.string,
|
messageName: PropTypes.string,
|
||||||
|
@ -43,7 +45,8 @@ export default class CanGraph extends Component {
|
||||||
shiftX: 0,
|
shiftX: 0,
|
||||||
shiftY: 0,
|
shiftY: 0,
|
||||||
bounds: null,
|
bounds: null,
|
||||||
isDataInserted: false
|
isDataInserted: false,
|
||||||
|
data: this.getGraphData(props)
|
||||||
};
|
};
|
||||||
this.onNewView = this.onNewView.bind(this);
|
this.onNewView = this.onNewView.bind(this);
|
||||||
this.onSignalClickTime = this.onSignalClickTime.bind(this);
|
this.onSignalClickTime = this.onSignalClickTime.bind(this);
|
||||||
|
@ -52,6 +55,40 @@ export default class CanGraph extends Component {
|
||||||
this.onDragAnchorMouseUp = this.onDragAnchorMouseUp.bind(this);
|
this.onDragAnchorMouseUp = this.onDragAnchorMouseUp.bind(this);
|
||||||
this.onDragStart = this.onDragStart.bind(this);
|
this.onDragStart = this.onDragStart.bind(this);
|
||||||
this.onPlotResize = this.onPlotResize.bind(this);
|
this.onPlotResize = this.onPlotResize.bind(this);
|
||||||
|
this.insertData = this.insertData.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphData(props) {
|
||||||
|
let firstRelTime = -1;
|
||||||
|
let lastRelTime = -1;
|
||||||
|
let series = props.plottedSignals
|
||||||
|
.map(signals => {
|
||||||
|
const { messageId, signalUid } = signals;
|
||||||
|
let entries = props.messages[messageId].entries;
|
||||||
|
if (entries.length) {
|
||||||
|
let messageRelTime = entries[0].relTime;
|
||||||
|
if (firstRelTime === -1) {
|
||||||
|
firstRelTime = messageRelTime;
|
||||||
|
} else {
|
||||||
|
firstRelTime = Math.min(firstRelTime, messageRelTime);
|
||||||
|
}
|
||||||
|
messageRelTime = entries[entries.length - 1].relTime;
|
||||||
|
lastRelTime = Math.max(lastRelTime, messageRelTime);
|
||||||
|
}
|
||||||
|
return GraphData._calcGraphData(
|
||||||
|
props.messages[messageId],
|
||||||
|
signalUid,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce((m, v) => m.concat(v), []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated: Date.now(),
|
||||||
|
series,
|
||||||
|
firstRelTime,
|
||||||
|
lastRelTime
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentIsNew(newSegment) {
|
segmentIsNew(newSegment) {
|
||||||
|
@ -61,14 +98,6 @@ export default class CanGraph extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dataChanged(prevProps, nextProps) {
|
|
||||||
return (
|
|
||||||
nextProps.data.series.length !== prevProps.data.series.length ||
|
|
||||||
!prevProps.signalSpec.equals(nextProps.signalSpec) ||
|
|
||||||
nextProps.data.updated !== this.props.data.updated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
visualChanged(prevProps, nextProps) {
|
visualChanged(prevProps, nextProps) {
|
||||||
return (
|
return (
|
||||||
prevProps.canReceiveGraphDrop !== nextProps.canReceiveGraphDrop ||
|
prevProps.canReceiveGraphDrop !== nextProps.canReceiveGraphDrop ||
|
||||||
|
@ -79,7 +108,6 @@ export default class CanGraph extends Component {
|
||||||
onPlotResize({ bounds }) {
|
onPlotResize({ bounds }) {
|
||||||
this.setState({ bounds });
|
this.setState({ bounds });
|
||||||
|
|
||||||
this.view.run();
|
|
||||||
this.view.signal("width", bounds.width - 70);
|
this.view.signal("width", bounds.width - 70);
|
||||||
this.view.signal("height", 0.4 * (bounds.width - 70)); // 5:2 aspect ratio
|
this.view.signal("height", 0.4 * (bounds.width - 70)); // 5:2 aspect ratio
|
||||||
this.view.run();
|
this.view.run();
|
||||||
|
@ -87,48 +115,51 @@ export default class CanGraph extends Component {
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
if (this.view) {
|
if (this.view) {
|
||||||
// only update if segment is new
|
this.view.runAfter(() => {
|
||||||
let segmentChanged = false;
|
// only update if segment is new
|
||||||
if (this.segmentIsNew(nextProps.segment)) {
|
let segmentChanged = false;
|
||||||
if (nextProps.segment.length > 0) {
|
if (this.segmentIsNew(nextProps.segment)) {
|
||||||
// Set segmented domain
|
if (nextProps.segment.length > 0) {
|
||||||
this.view.signal("segment", nextProps.segment);
|
// Set segmented domain
|
||||||
} else {
|
this.view.signal("segment", nextProps.segment);
|
||||||
// Reset segment to full domain
|
} else {
|
||||||
this.view.signal("segment", 0);
|
// Reset segment to full domain
|
||||||
|
this.view.signal("segment", 0);
|
||||||
|
}
|
||||||
|
segmentChanged = true;
|
||||||
}
|
}
|
||||||
segmentChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nextProps.live && nextProps.currentTime !== this.props.currentTime) {
|
if (
|
||||||
this.view.signal("videoTime", nextProps.currentTime);
|
!nextProps.live &&
|
||||||
segmentChanged = true;
|
nextProps.currentTime !== this.props.currentTime
|
||||||
}
|
) {
|
||||||
|
this.view.signal("videoTime", nextProps.currentTime);
|
||||||
|
segmentChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (segmentChanged) {
|
if (segmentChanged) {
|
||||||
this.view.run();
|
this.view.runAsync();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataChanged = this.dataChanged(this.props, nextProps);
|
return true;
|
||||||
|
|
||||||
return (
|
|
||||||
dataChanged ||
|
|
||||||
JSON.stringify(this.state) !== JSON.stringify(nextState) ||
|
|
||||||
this.visualChanged(this.props, nextProps)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insertData() {
|
insertData = debounce(() => {
|
||||||
this.view.remove("table", () => true).run();
|
let { series } = this.state.data;
|
||||||
this.view.insert("table", this.props.data.series).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
// adding plot points by diff isn't faster since it basically has to be n^2
|
||||||
if (this.dataChanged(prevProps, this.props)) {
|
// out-of-order events make it so that you can't just check the bounds
|
||||||
this.insertData();
|
let changeset = this.view
|
||||||
}
|
.changeset()
|
||||||
}
|
.remove(v => true)
|
||||||
|
.insert(series);
|
||||||
|
this.view.change("table", changeset);
|
||||||
|
this.view.run();
|
||||||
|
}, 250);
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (
|
if (
|
||||||
|
@ -139,6 +170,21 @@ export default class CanGraph extends Component {
|
||||||
} else if (!nextProps.dragPos && this.state.plotInnerStyle !== null) {
|
} else if (!nextProps.dragPos && this.state.plotInnerStyle !== null) {
|
||||||
this.setState({ plotInnerStyle: null });
|
this.setState({ plotInnerStyle: null });
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
this.props.messages !== nextProps.messages ||
|
||||||
|
this.props.plottedSignal !== nextProps.plottedSignal
|
||||||
|
) {
|
||||||
|
let data = this.getGraphData(nextProps);
|
||||||
|
if (
|
||||||
|
data.series.length === this.state.data.series.length &&
|
||||||
|
data.firstRelTime === this.state.data.firstRelTime &&
|
||||||
|
data.lastRelTime === this.state.data.lastRelTime
|
||||||
|
) {
|
||||||
|
// do nothing, the data didn't *actually* change
|
||||||
|
} else {
|
||||||
|
this.setState({ data }, this.insertData);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStyleFromDragPos({ left, top }) {
|
updateStyleFromDragPos({ left, top }) {
|
||||||
|
@ -177,9 +223,8 @@ export default class CanGraph extends Component {
|
||||||
this.view.runAfter(() => {
|
this.view.runAfter(() => {
|
||||||
const state = this.view.getState();
|
const state = this.view.getState();
|
||||||
state.subcontext[0].signals.brush = 0;
|
state.subcontext[0].signals.brush = 0;
|
||||||
this.view.setState(state).runAfter(() => {
|
this.view.setState(state);
|
||||||
this.insertData();
|
this.insertData();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,8 +337,7 @@ export default class CanGraph extends Component {
|
||||||
className="cabana-explorer-visuals-plot-container"
|
className="cabana-explorer-visuals-plot-container"
|
||||||
>
|
>
|
||||||
<CanPlot
|
<CanPlot
|
||||||
logLevel={0}
|
logLevel={1}
|
||||||
data={{ table: CanGraph.emptyTable }}
|
|
||||||
onNewView={this.onNewView}
|
onNewView={this.onNewView}
|
||||||
onSignalClickTime={this.onSignalClickTime}
|
onSignalClickTime={this.onSignalClickTime}
|
||||||
onSignalSegment={this.onSignalSegment}
|
onSignalSegment={this.onSignalSegment}
|
||||||
|
|
|
@ -9,7 +9,6 @@ export default class CanGraphList extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
plottedSignals: PropTypes.array.isRequired,
|
plottedSignals: PropTypes.array.isRequired,
|
||||||
messages: PropTypes.object.isRequired,
|
messages: PropTypes.object.isRequired,
|
||||||
graphData: PropTypes.array.isRequired,
|
|
||||||
onGraphTimeClick: PropTypes.func.isRequired,
|
onGraphTimeClick: PropTypes.func.isRequired,
|
||||||
seekTime: PropTypes.number.isRequired,
|
seekTime: PropTypes.number.isRequired,
|
||||||
onSegmentChanged: PropTypes.func.isRequired,
|
onSegmentChanged: PropTypes.func.isRequired,
|
||||||
|
@ -153,7 +152,6 @@ export default class CanGraphList extends Component {
|
||||||
signalSpec={Object.assign(Object.create(signal), signal)}
|
signalSpec={Object.assign(Object.create(signal), signal)}
|
||||||
onSegmentChanged={this.props.onSegmentChanged}
|
onSegmentChanged={this.props.onSegmentChanged}
|
||||||
segment={this.props.segment}
|
segment={this.props.segment}
|
||||||
data={this.props.graphData[index]}
|
|
||||||
onRelativeTimeClick={this.props.onGraphTimeClick}
|
onRelativeTimeClick={this.props.onGraphTimeClick}
|
||||||
currentTime={this.props.seekTime}
|
currentTime={this.props.seekTime}
|
||||||
onDragStart={this.onGraphDragStart}
|
onDragStart={this.onGraphDragStart}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default class Explorer extends Component {
|
||||||
segmentIndices: [],
|
segmentIndices: [],
|
||||||
shouldShowAddSignal: true,
|
shouldShowAddSignal: true,
|
||||||
userSeekIndex: 0,
|
userSeekIndex: 0,
|
||||||
userSeekTime: props.currentParts[0] * 60,
|
userSeekTime: 0,
|
||||||
playing: props.autoplay,
|
playing: props.autoplay,
|
||||||
signals: {},
|
signals: {},
|
||||||
playSpeed: 1
|
playSpeed: 1
|
||||||
|
@ -124,28 +124,20 @@ export default class Explorer extends Component {
|
||||||
plottedSignals = plottedSignals
|
plottedSignals = plottedSignals
|
||||||
.map(plot =>
|
.map(plot =>
|
||||||
plot.filter(({ messageId, signalUid }, index) => {
|
plot.filter(({ messageId, signalUid }, index) => {
|
||||||
const messageExists =
|
const messageExists = !!nextProps.messages[messageId];
|
||||||
Object.keys(nextProps.messages).indexOf(messageId) !== -1;
|
|
||||||
let signalExists = true;
|
let signalExists = true;
|
||||||
if (!messageExists) {
|
if (messageExists) {
|
||||||
graphData.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
signalExists = Object.values(
|
signalExists = Object.values(
|
||||||
nextProps.messages[messageId].frame.signals
|
nextProps.messages[messageId].frame.signals
|
||||||
).some(signal => signal.uid === signalUid);
|
).some(signal => signal.uid === signalUid);
|
||||||
|
|
||||||
if (!signalExists) {
|
|
||||||
graphData[index].series = graphData[index].series.filter(
|
|
||||||
entry => entry.signalUid !== signalUid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageExists && signalExists;
|
return messageExists && signalExists;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.filter(plot => plot.length > 0);
|
.filter(plot => plot.length > 0);
|
||||||
this.setState({ plottedSignals, graphData });
|
|
||||||
|
this.setState({ plottedSignals });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nextProps.selectedMessage &&
|
nextProps.selectedMessage &&
|
||||||
|
@ -228,15 +220,6 @@ export default class Explorer extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partsDidChange) {
|
|
||||||
const { userSeekTime } = this.state;
|
|
||||||
const nextSeekTime =
|
|
||||||
userSeekTime -
|
|
||||||
this.props.currentParts[0] * 60 +
|
|
||||||
nextProps.currentParts[0] * 60;
|
|
||||||
this.setState({ userSeekTime: nextSeekTime });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changePlaySpeed(value) {
|
changePlaySpeed(value) {
|
||||||
|
@ -356,18 +339,10 @@ export default class Explorer extends Component {
|
||||||
const { segment, segmentIndices } = this.state;
|
const { segment, segmentIndices } = this.state;
|
||||||
const { messages, selectedMessage } = this.props;
|
const { messages, selectedMessage } = this.props;
|
||||||
if (segment.length > 0 || segmentIndices.length > 0) {
|
if (segment.length > 0 || segmentIndices.length > 0) {
|
||||||
let userSeekTime = 0;
|
|
||||||
if (
|
|
||||||
messages[selectedMessage] &&
|
|
||||||
messages[selectedMessage].entries.length > 0
|
|
||||||
) {
|
|
||||||
userSeekTime = messages[selectedMessage].entries[0].relTime;
|
|
||||||
}
|
|
||||||
this.setState({
|
this.setState({
|
||||||
segment: [],
|
segment: [],
|
||||||
segmentIndices: [],
|
segmentIndices: [],
|
||||||
userSeekIndex: 0,
|
userSeekIndex: 0
|
||||||
userSeekTime
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -387,8 +362,13 @@ export default class Explorer extends Component {
|
||||||
if (entries.length === 0) return null;
|
if (entries.length === 0) return null;
|
||||||
|
|
||||||
const { segmentIndices } = this.state;
|
const { segmentIndices } = this.state;
|
||||||
if (segmentIndices.length === 2) {
|
if (segmentIndices.length === 2 && segmentIndices[0] >= 0) {
|
||||||
for (let i = segmentIndices[0]; i <= segmentIndices[1]; i++) {
|
for (
|
||||||
|
let i = segmentIndices[0],
|
||||||
|
l = Math.min(entries.length - 1, segmentIndices[1]);
|
||||||
|
i <= l;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
if (entries[i].relTime >= time) {
|
if (entries[i].relTime >= time) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
@ -407,22 +387,9 @@ export default class Explorer extends Component {
|
||||||
onUserSeek(time) {
|
onUserSeek(time) {
|
||||||
this.setState({ userSeekTime: time });
|
this.setState({ userSeekTime: time });
|
||||||
const message = this.props.messages[this.props.selectedMessage];
|
const message = this.props.messages[this.props.selectedMessage];
|
||||||
if (!message) {
|
|
||||||
this.props.onUserSeek(time);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { entries } = message;
|
this.props.onUserSeek(time);
|
||||||
const userSeekIndex = this.indexFromSeekTime(time);
|
this.setState({ userSeekTime: time });
|
||||||
if (userSeekIndex) {
|
|
||||||
const seekTime = entries[userSeekIndex].relTime;
|
|
||||||
|
|
||||||
this.setState({ userSeekIndex, userSeekTime: seekTime });
|
|
||||||
this.props.onSeek(userSeekIndex, seekTime);
|
|
||||||
} else {
|
|
||||||
this.props.onUserSeek(time);
|
|
||||||
this.setState({ userSeekTime: time });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPlaySeek(time) {
|
onPlaySeek(time) {
|
||||||
|
@ -439,20 +406,7 @@ export default class Explorer extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGraphTimeClick(messageId, time) {
|
onGraphTimeClick(messageId, time) {
|
||||||
const canTime = time + this.props.firstCanTime;
|
this.onUserSeek(time);
|
||||||
|
|
||||||
const { entries } = this.props.messages[messageId];
|
|
||||||
if (entries.length) {
|
|
||||||
const userSeekIndex = Entries.findTimeIndex(entries, canTime);
|
|
||||||
|
|
||||||
this.props.onUserSeek(time);
|
|
||||||
this.setState({
|
|
||||||
userSeekIndex,
|
|
||||||
userSeekTime: time
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({ userSeekTime: time });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPlay() {
|
onPlay() {
|
||||||
|
@ -471,34 +425,6 @@ export default class Explorer extends Component {
|
||||||
return this.props.partsCount * 60;
|
return this.props.partsCount * 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
startOffset() {
|
|
||||||
return 0;
|
|
||||||
const partOffset = this.props.currentParts[0] * 60;
|
|
||||||
const message = this.props.messages[this.props.selectedMessage];
|
|
||||||
if (!message || message.entries.length === 0) {
|
|
||||||
return partOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { entries } = message;
|
|
||||||
const { segment } = this.state;
|
|
||||||
let startTime;
|
|
||||||
if (segment.length === 2) {
|
|
||||||
startTime = segment[0];
|
|
||||||
} else {
|
|
||||||
startTime = entries[0].relTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
startTime > partOffset &&
|
|
||||||
startTime < (this.props.currentParts[1] + 1) * 60
|
|
||||||
) {
|
|
||||||
// startTime is within bounds of currently selected parts
|
|
||||||
return startTime;
|
|
||||||
} else {
|
|
||||||
return partOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVideoClick() {
|
onVideoClick() {
|
||||||
const playing = !this.state.playing;
|
const playing = !this.state.playing;
|
||||||
this.setState({ playing });
|
this.setState({ playing });
|
||||||
|
@ -651,8 +577,7 @@ export default class Explorer extends Component {
|
||||||
<br />
|
<br />
|
||||||
<RouteVideoSync
|
<RouteVideoSync
|
||||||
message={this.props.messages[this.props.selectedMessage]}
|
message={this.props.messages[this.props.selectedMessage]}
|
||||||
secondsLoaded={this.secondsLoaded()}
|
segment={this.state.segment}
|
||||||
startOffset={this.startOffset()}
|
|
||||||
seekIndex={this.props.seekIndex}
|
seekIndex={this.props.seekIndex}
|
||||||
userSeekIndex={this.state.userSeekIndex}
|
userSeekIndex={this.state.userSeekIndex}
|
||||||
playing={this.state.playing}
|
playing={this.state.playing}
|
||||||
|
@ -682,7 +607,6 @@ export default class Explorer extends Component {
|
||||||
<CanGraphList
|
<CanGraphList
|
||||||
plottedSignals={this.state.plottedSignals}
|
plottedSignals={this.state.plottedSignals}
|
||||||
messages={this.props.messages}
|
messages={this.props.messages}
|
||||||
graphData={this.state.graphData}
|
|
||||||
onGraphTimeClick={this.onGraphTimeClick}
|
onGraphTimeClick={this.onGraphTimeClick}
|
||||||
seekTime={this.props.seekTime}
|
seekTime={this.props.seekTime}
|
||||||
onSegmentChanged={this.onSegmentChanged}
|
onSegmentChanged={this.onSegmentChanged}
|
||||||
|
|
|
@ -66,6 +66,7 @@ export default class HLS extends Component {
|
||||||
this.player.loadSource(source);
|
this.player.loadSource(source);
|
||||||
this.player.attachMedia(this.videoElement);
|
this.player.attachMedia(this.videoElement);
|
||||||
this.props.onVideoElementAvailable(this.videoElement);
|
this.props.onVideoElementAvailable(this.videoElement);
|
||||||
|
this.videoElement.currentTime = this.props.startTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import debounce from "../../utils/debounce";
|
||||||
|
|
||||||
export default class RouteSeeker extends Component {
|
export default class RouteSeeker extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
secondsLoaded: PropTypes.number.isRequired,
|
videoLength: PropTypes.number.isRequired,
|
||||||
segmentIndices: PropTypes.arrayOf(PropTypes.number),
|
segmentIndices: PropTypes.arrayOf(PropTypes.number),
|
||||||
onUserSeek: PropTypes.func,
|
onUserSeek: PropTypes.func,
|
||||||
onPlaySeek: PropTypes.func,
|
onPlaySeek: PropTypes.func,
|
||||||
|
@ -58,10 +58,10 @@ export default class RouteSeeker extends Component {
|
||||||
markerStyle: RouteSeeker.hiddenMarkerStyle,
|
markerStyle: RouteSeeker.hiddenMarkerStyle,
|
||||||
ratio: 0
|
ratio: 0
|
||||||
});
|
});
|
||||||
} else if (nextProps.secondsLoaded !== this.props.secondsLoaded) {
|
} else if (nextProps.videoLength !== this.props.videoLength) {
|
||||||
// adjust ratio in line with new secondsLoaded
|
// adjust ratio in line with new videoLength
|
||||||
const secondsSeeked = ratio * this.props.secondsLoaded;
|
const secondsSeeked = ratio * this.props.videoLength;
|
||||||
const newRatio = secondsSeeked / nextProps.secondsLoaded;
|
const newRatio = secondsSeeked / nextProps.videoLength;
|
||||||
this.updateSeekedBar(newRatio);
|
this.updateSeekedBar(newRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,17 +158,24 @@ export default class RouteSeeker extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let { videoLength, startTime } = this.props;
|
||||||
let { currentTime, duration } = videoElement;
|
let { currentTime, duration } = videoElement;
|
||||||
let newRatio = currentTime / duration;
|
|
||||||
|
currentTime = roundTime(currentTime);
|
||||||
|
startTime = roundTime(startTime);
|
||||||
|
videoLength = roundTime(videoLength);
|
||||||
|
duration = roundTime(duration);
|
||||||
|
|
||||||
|
let newRatio = (currentTime - startTime) / videoLength;
|
||||||
|
|
||||||
if (newRatio === this.state.ratio) {
|
if (newRatio === this.state.ratio) {
|
||||||
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
|
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newRatio >= 1) {
|
if (newRatio >= 1 || newRatio < 0) {
|
||||||
newRatio = 0;
|
newRatio = 0;
|
||||||
currentTime = 0;
|
currentTime = startTime;
|
||||||
this.props.onUserSeek(newRatio);
|
this.props.onUserSeek(newRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,3 +243,7 @@ export default class RouteSeeker extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function roundTime(time) {
|
||||||
|
return Math.round(time * 1000) / 1000;
|
||||||
|
}
|
||||||
|
|
|
@ -46,8 +46,7 @@ const Styles = StyleSheet.create({
|
||||||
export default class RouteVideoSync extends Component {
|
export default class RouteVideoSync extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
userSeekIndex: PropTypes.number.isRequired,
|
userSeekIndex: PropTypes.number.isRequired,
|
||||||
secondsLoaded: PropTypes.number.isRequired,
|
segment: PropTypes.array.isRequired,
|
||||||
startOffset: PropTypes.number.isRequired,
|
|
||||||
message: PropTypes.object,
|
message: PropTypes.object,
|
||||||
canFrameOffset: PropTypes.number.isRequired,
|
canFrameOffset: PropTypes.number.isRequired,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
|
@ -56,7 +55,8 @@ export default class RouteVideoSync extends Component {
|
||||||
onUserSeek: PropTypes.func.isRequired,
|
onUserSeek: PropTypes.func.isRequired,
|
||||||
onPlay: PropTypes.func.isRequired,
|
onPlay: PropTypes.func.isRequired,
|
||||||
onPause: PropTypes.func.isRequired,
|
onPause: PropTypes.func.isRequired,
|
||||||
userSeekTime: PropTypes.number.isRequired
|
userSeekTime: PropTypes.number.isRequired,
|
||||||
|
playSpeed: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -87,6 +87,14 @@ export default class RouteVideoSync extends Component {
|
||||||
) {
|
) {
|
||||||
this.setState({ shouldRestartHls: true });
|
this.setState({ shouldRestartHls: true });
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
nextProps.userSeekTime &&
|
||||||
|
this.props.userSeekTime !== nextProps.userSeekTime
|
||||||
|
) {
|
||||||
|
if (this.state.videoElement) {
|
||||||
|
this.state.videoElement.currentTime = nextProps.userSeekTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nearestFrameUrl() {
|
nearestFrameUrl() {
|
||||||
|
@ -121,20 +129,40 @@ export default class RouteVideoSync extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentProgress(currentTime) {
|
videoLength() {
|
||||||
// returns progress as number in [0,1]
|
if (this.props.segment.length) {
|
||||||
|
return this.props.segment[1] - this.props.segment[0];
|
||||||
if (currentTime < this.props.startOffset) {
|
|
||||||
currentTime = this.props.startOffset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratio =
|
if (this.state.videoElement) {
|
||||||
(currentTime - this.props.startOffset) / this.props.secondsLoaded;
|
return this.state.videoElement.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime() {
|
||||||
|
if (this.props.segment.length) {
|
||||||
|
return this.props.segment[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentProgress(currentTime) {
|
||||||
|
// returns progress as number in [0,1]
|
||||||
|
let startTime = this.startTime();
|
||||||
|
|
||||||
|
if (currentTime < startTime) {
|
||||||
|
currentTime = startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = (currentTime - startTime) / this.videoLength();
|
||||||
return Math.max(0, Math.min(1, ratio));
|
return Math.max(0, Math.min(1, ratio));
|
||||||
}
|
}
|
||||||
|
|
||||||
ratioTime(ratio) {
|
ratioTime(ratio) {
|
||||||
return ratio * this.props.secondsLoaded + this.props.startOffset;
|
return ratio * this.videoLength() + this.startTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
onVideoElementAvailable(videoElement) {
|
onVideoElementAvailable(videoElement) {
|
||||||
|
@ -145,7 +173,11 @@ export default class RouteVideoSync extends Component {
|
||||||
/* ratio in [0,1] */
|
/* ratio in [0,1] */
|
||||||
|
|
||||||
let { videoElement } = this.state;
|
let { videoElement } = this.state;
|
||||||
let seekTime = videoElement.duration * ratio;
|
if (isNaN(videoElement.duration)) {
|
||||||
|
this.setState({ shouldRestartHls: true }, funcSeekToRatio);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let seekTime = this.ratioTime(ratio);
|
||||||
videoElement.currentTime = seekTime;
|
videoElement.currentTime = seekTime;
|
||||||
|
|
||||||
const funcSeekToRatio = () => this.props.onUserSeek(seekTime);
|
const funcSeekToRatio = () => this.props.onUserSeek(seekTime);
|
||||||
|
@ -177,7 +209,8 @@ export default class RouteVideoSync extends Component {
|
||||||
this.props.url,
|
this.props.url,
|
||||||
process.env.REACT_APP_VIDEO_CDN
|
process.env.REACT_APP_VIDEO_CDN
|
||||||
).getRearCameraStreamIndexUrl()}
|
).getRearCameraStreamIndexUrl()}
|
||||||
startTime={this.props.userSeekTime}
|
startTime={this.startTime()}
|
||||||
|
videoLength={this.videoLength()}
|
||||||
playbackSpeed={this.props.playSpeed}
|
playbackSpeed={this.props.playSpeed}
|
||||||
onVideoElementAvailable={this.onVideoElementAvailable}
|
onVideoElementAvailable={this.onVideoElementAvailable}
|
||||||
playing={this.props.playing}
|
playing={this.props.playing}
|
||||||
|
@ -194,7 +227,8 @@ export default class RouteVideoSync extends Component {
|
||||||
className={css(Styles.seekBar)}
|
className={css(Styles.seekBar)}
|
||||||
nearestFrameTime={this.props.userSeekTime}
|
nearestFrameTime={this.props.userSeekTime}
|
||||||
segmentProgress={this.segmentProgress}
|
segmentProgress={this.segmentProgress}
|
||||||
secondsLoaded={this.props.secondsLoaded}
|
startTime={this.startTime()}
|
||||||
|
videoLength={this.videoLength()}
|
||||||
segmentIndices={this.props.segmentIndices}
|
segmentIndices={this.props.segmentIndices}
|
||||||
onUserSeek={this.onUserSeek}
|
onUserSeek={this.onUserSeek}
|
||||||
onPlaySeek={this.props.onPlaySeek}
|
onPlaySeek={this.props.onPlaySeek}
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const LOGENTRIES_TOKEN = "4bc98019-8277-4fe0-867c-ed21ea843cc5";
|
||||||
|
|
||||||
export const PART_SEGMENT_LENGTH = 3;
|
export const PART_SEGMENT_LENGTH = 3;
|
||||||
|
|
||||||
export const CAN_GRAPH_MAX_POINTS = 10000;
|
export const CAN_GRAPH_MAX_POINTS = 5000;
|
||||||
|
|
||||||
export const STREAMING_WINDOW = 60;
|
export const STREAMING_WINDOW = 60;
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ function _calcGraphData(msg, signalUid, firstCanTime) {
|
||||||
// Always include last message entry, which faciliates graphData comparison
|
// Always include last message entry, which faciliates graphData comparison
|
||||||
samples.push(msg.entries[msg.entries.length - 1]);
|
samples.push(msg.entries[msg.entries.length - 1]);
|
||||||
}
|
}
|
||||||
|
// sorting these doesn't fix the phantom lines
|
||||||
return samples
|
return samples
|
||||||
.filter(e => e.signals[signal.name] !== undefined)
|
.filter(e => e.signals[signal.name] !== undefined)
|
||||||
.map(entry => {
|
.map(entry => {
|
||||||
|
|
|
@ -56,15 +56,16 @@ function sendBatch(entry) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (entry.ended) {
|
|
||||||
console.log("Sending finished");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
newMessages: messages,
|
newMessages: messages,
|
||||||
maxByteStateChangeCount,
|
maxByteStateChangeCount,
|
||||||
isFinished: entry.ended
|
isFinished: entry.ended
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (entry.ended) {
|
||||||
|
console.log("Sending finished");
|
||||||
|
close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData(entry) {
|
async function loadData(entry) {
|
||||||
|
|
|
@ -27,10 +27,10 @@
|
||||||
joi-browser "^13.4.0"
|
joi-browser "^13.4.0"
|
||||||
querystringify "^2.1.1"
|
querystringify "^2.1.1"
|
||||||
|
|
||||||
"@commaai/comma-api@^1.1.2":
|
"@commaai/comma-api@^1.1.4":
|
||||||
version "1.1.2"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@commaai/comma-api/-/comma-api-1.1.2.tgz#2abf967b708f2d650c8e9cb0eecd5044a3a39da2"
|
resolved "https://registry.yarnpkg.com/@commaai/comma-api/-/comma-api-1.1.4.tgz#75f727bfa43f6f446a341d1f9154f15334cfb79e"
|
||||||
integrity sha512-Pp19FnHSimzkBlbJCgPObklZc2VZvtYrHVQoCcfbsZMYZaOEFolHek4lZ0l2XkCNB8n71QleBbK6xGY56/jb+Q==
|
integrity sha512-S25QY2OwO1aO5HKtvJQUnCwDsfBhgj7+SANZG99kHvxfFxOwWzT/GcV7G/xEtEJ06OsJ+Ih8XoUFm7IxNc1bwA==
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime "^6.26.0"
|
babel-runtime "^6.26.0"
|
||||||
config-request "^0.5.1"
|
config-request "^0.5.1"
|
||||||
|
|
Loading…
Reference in New Issue