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 generic
main
Chris Vickery 2019-09-11 13:36:15 -07:00 committed by GitHub
parent 58c77f2aca
commit edf84da5ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 224 additions and 183 deletions

View File

@ -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"
]
}
}
} }

View File

@ -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"
}
} }
} }

View File

@ -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) {

View File

@ -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);
}); });

View File

@ -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={() => {}}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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;
} }
} }

View File

@ -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;
}

View File

@ -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}

View File

@ -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;

View File

@ -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 => {

View File

@ -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) {

View File

@ -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"