diff --git a/README.md b/README.md index 6d35b8ad..ee9b0d56 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ There's a ton of them, but these are worth mentioning because they sort of affec * `@material-ui` - Lots of fully featured highly customizable components for building the UIs with. Theming system with global and per-component overrides of any CSS values. * `react-router-redux` - the newer one, 5.x.... Mindlessly simple routing with convenient global access due to redux +<<<<<<< HEAD ## How things works The current playback is tracked not by storing the current offset, but instead storing the local time that the player began, the offset it began at, and the playback rate. Any time any of these values change, it rebases them all back to the current time. It means that at any arbitrary moment you can calculate the current offset with... ```js @@ -32,4 +33,8 @@ The current playback is tracked not by storing the current offset, but instead s With this central authority on current offset time, it becomes much easier to have each data source keep themselves in sync instead of trying to manage synchronizing all of them. +======= +## Development +`pnpm start` +>>>>>>> upstream/logid2 diff --git a/src/actions/history.js b/src/actions/history.js index 61c7db12..a44a7758 100644 --- a/src/actions/history.js +++ b/src/actions/history.js @@ -1,5 +1,5 @@ import { LOCATION_CHANGE } from 'connected-react-router'; -import { getDongleID, getZoom, getPrimeNav } from '../url'; +import { getDongleID, getZoom, getSegmentRange, getPrimeNav } from '../url'; import { primeNav, selectDevice, pushTimelineRange } from './index'; export const onHistoryMiddleware = ({ dispatch, getState }) => (next) => (action) => { @@ -19,7 +19,13 @@ export const onHistoryMiddleware = ({ dispatch, getState }) => (next) => (action const pathZoom = getZoom(action.payload.location.pathname); if (pathZoom !== state.zoom) { - dispatch(pushTimelineRange(pathZoom?.start, pathZoom?.end, false)); + console.debug("TODO: this should redirect to a log id") + //dispatch(pushTimelineRange(pathZoom?.start, pathZoom?.end, false)); + } + + const pathSegmentRange = getSegmentRange(action.payload.location.pathname); + if (pathSegmentRange !== state.segmentRange) { + dispatch(pushTimelineRange(pathSegmentRange?.log_id, pathSegmentRange?.start, pathSegmentRange?.end, false)); } const pathPrimeNav = getPrimeNav(action.payload.location.pathname); diff --git a/src/actions/history.test.js b/src/actions/history.test.js index 47ccc719..b802f11f 100644 --- a/src/actions/history.test.js +++ b/src/actions/history.test.js @@ -77,7 +77,6 @@ describe('history middleware', () => { }; invoke(action); expect(next).toHaveBeenCalledWith(action); - expect(store.dispatch).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledWith(fakeInner); expect(actionsIndex.selectDevice).toHaveBeenCalledWith('0000aaaa0000aaaa', false); }); @@ -96,14 +95,14 @@ describe('history middleware', () => { type: LOCATION_CHANGE, payload: { action: 'POP', - location: { pathname: '0000aaaa0000aaaa/1230/1234' }, + location: { pathname: '0000aaaa0000aaaa/00000014--55a0b1280e/1230/1234' }, }, }; invoke(action); expect(next).toHaveBeenCalledWith(action); expect(store.dispatch).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledWith(fakeInner); - expect(actionsIndex.pushTimelineRange).toHaveBeenCalledWith(1230, 1234, false); + expect(actionsIndex.pushTimelineRange).toHaveBeenCalledWith('00000014--55a0b1280e', 1230, 1234, false); }); it('should call prime nav with history', async () => { @@ -130,7 +129,7 @@ describe('history middleware', () => { expect(store.dispatch).toHaveBeenCalledTimes(2); expect(store.dispatch).toHaveBeenCalledWith(fakeInner); expect(store.dispatch).toHaveBeenCalledWith(fakeInner2); - expect(actionsIndex.pushTimelineRange).toHaveBeenCalledWith(undefined, undefined, false); + expect(actionsIndex.pushTimelineRange).toHaveBeenCalledWith(undefined, undefined, undefined, false); expect(actionsIndex.primeNav).toHaveBeenCalledWith(true); }); }); diff --git a/src/actions/index.js b/src/actions/index.js index 2795b6f1..e06dc99b 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -11,6 +11,244 @@ import { getDeviceFromState, deviceVersionAtLeast } from '../utils'; let routesRequest = null; +function updateTimeline(state, dispatch, log_id, start, end, allowPathChange) { + dispatch(checkRoutesData()); + + if (!state.loop || !state.loop.startTime || !state.loop.duration || state.loop.startTime < start + || state.loop.startTime + state.loop.duration > end || state.loop.duration < end - start) { + dispatch(resetPlayback()); + dispatch(selectLoop(start, end)); + } + + // TODO: fix this up + if (allowPathChange) { + const desiredPath = urlForState(state.dongleId, log_id, start, end, false); + if (window.location.pathname !== desiredPath) { + dispatch(push(desiredPath)); + } + } +} + +export function popTimelineRange(allowPathChange = true) { + return (dispatch, getState) => { + const state = getState(); + if (state.zoom.previous) { + dispatch({ + type: Types.TIMELINE_POP_SELECTION, + }); + + const { start, end } = state.zoom.previous; + updateTimeline(state, dispatch, state.currentRoute?.log_id, start, end, allowPathChange); + } + }; +} + +export function pushTimelineRange(log_id, start, end, allowPathChange = true) { + return (dispatch, getState) => { + const state = getState(); + if (state.zoom?.start !== start || state.zoom?.end !== end) { + dispatch({ + type: Types.TIMELINE_PUSH_SELECTION, + log_id, + start, + end, + }); + } + + updateTimeline(state, dispatch, log_id, start, end, allowPathChange); + }; +} + +export function selectDevice(dongleId, allowPathChange = true) { + return (dispatch, getState) => { + const state = getState(); + let device; + if (state.devices && state.devices.length > 1) { + device = state.devices.find((d) => d.dongle_id === dongleId); + } + if (!device && state.device && state.device.dongle_id === dongleId) { + device = state.device; + } + + dispatch({ + type: Types.ACTION_SELECT_DEVICE, + dongleId, + }); + + dispatch(pushTimelineRange(null, null, null, false)); + if ((device && !device.shared) || state.profile?.superuser) { + dispatch(primeFetchSubscription(dongleId, device)); + dispatch(fetchDeviceOnline(dongleId)); + } + + dispatch(checkRoutesData()); + + if (allowPathChange) { + const desiredPath = urlForState(dongleId, null, null, null, null); + if (window.location.pathname !== desiredPath) { + dispatch(push(desiredPath)); + } + } + }; +} + +export function primeFetchSubscription(dongleId, device, profile) { + return (dispatch, getState) => { + const state = getState(); + + if (!device && state.device && state.device === dongleId) { + device = state.device; + } + if (!profile && state.profile) { + profile = state.profile; + } + + if (device && (device.is_owner || profile.superuser)) { + if (device.prime) { + Billing.getSubscription(dongleId).then((subscription) => { + dispatch(primeGetSubscription(dongleId, subscription)); + }).catch((err) => { + console.error(err); + Sentry.captureException(err, { fingerprint: 'actions_fetch_subscription' }); + }); + } else { + Billing.getSubscribeInfo(dongleId).then((subscribeInfo) => { + dispatch({ + type: Types.ACTION_PRIME_SUBSCRIBE_INFO, + dongleId, + subscribeInfo, + }); + }).catch((err) => { + console.error(err); + Sentry.captureException(err, { fingerprint: 'actions_fetch_subscribe_info' }); + }); + } + } + }; +} + +export function primeNav(nav, allowPathChange = true) { + return (dispatch, getState) => { + const state = getState(); + if (!state.dongleId) { + return; + } + + if (state.primeNav !== nav) { + dispatch({ + type: Types.ACTION_PRIME_NAV, + primeNav: nav, + }); + } + + if (allowPathChange) { + const curPath = document.location.pathname; + const desiredPath = urlForState(state.dongleId, null, null, null, nav); + if (curPath !== desiredPath) { + dispatch(push(desiredPath)); + } + } + }; +} + +export function fetchSharedDevice(dongleId) { + return async (dispatch) => { + try { + const resp = await Devices.fetchDevice(dongleId); + dispatch({ + type: Types.ACTION_UPDATE_SHARED_DEVICE, + dongleId, + device: resp, + }); + } catch (err) { + if (!err.resp || err.resp.status !== 403) { + console.error(err); + Sentry.captureException(err, { fingerprint: 'action_fetch_shared_device' }); + } + } + }; +} + +export function fetchDeviceOnline(dongleId) { + return (dispatch) => { + Devices.fetchDevice(dongleId).then((resp) => { + dispatch({ + type: Types.ACTION_UPDATE_DEVICE_ONLINE, + dongleId, + last_athena_ping: resp.last_athena_ping, + fetched_at: Math.floor(Date.now() / 1000), + }); + }).catch(console.log); + }; +} + +export function updateDeviceOnline(dongleId, lastAthenaPing) { + return (dispatch) => { + dispatch({ + type: Types.ACTION_UPDATE_DEVICE_ONLINE, + dongleId, + last_athena_ping: lastAthenaPing, + fetched_at: Math.floor(Date.now() / 1000), + }); + }; +} + +export function fetchDeviceNetworkStatus(dongleId) { + return async (dispatch, getState) => { + const device = getDeviceFromState(getState(), dongleId); + if (deviceVersionAtLeast(device, '0.8.14')) { + const payload = { + id: 0, + jsonrpc: '2.0', + method: 'getNetworkMetered', + }; + try { + const resp = await Athena.postJsonRpcPayload(dongleId, payload); + if (resp && resp.result !== undefined) { + dispatch({ + type: Types.ACTION_UPDATE_DEVICE_NETWORK, + dongleId, + networkMetered: resp.result, + }); + dispatch(updateDeviceOnline(dongleId, Math.floor(Date.now() / 1000))); + } + } catch (err) { + if (err.message && (err.message.indexOf('Timed out') === -1 || err.message.indexOf('Device not registered') === -1)) { + dispatch(updateDeviceOnline(dongleId, 0)); + } else { + console.error(err); + Sentry.captureException(err, { fingerprint: 'athena_fetch_networkmetered' }); + } + } + } else { + const payload = { + id: 0, + jsonrpc: '2.0', + method: 'getNetworkType', + }; + try { + const resp = await Athena.postJsonRpcPayload(dongleId, payload); + if (resp && resp.result !== undefined) { + const metered = resp.result !== 1 && resp.result !== 6; // wifi or ethernet + dispatch({ + type: Types.ACTION_UPDATE_DEVICE_NETWORK, + dongleId, + networkMetered: metered, + }); + dispatch(updateDeviceOnline(dongleId, Math.floor(Date.now() / 1000))); + } + } catch (err) { + if (err.message && (err.message.indexOf('Timed out') === -1 || err.message.indexOf('Device not registered') === -1)) { + dispatch(updateDeviceOnline(dongleId, 0)); + } else { + console.error(err); + Sentry.captureException(err, { fingerprint: 'athena_fetch_networktype' }); + } + } + } + }; +} + export function checkRoutesData() { return (dispatch, getState) => { let state = getState(); @@ -66,11 +304,12 @@ export function checkRoutesData() { return { ...r, url: r.url.replace('chffrprivate.blob.core.windows.net', 'chffrprivate.azureedge.net'), - offset: Math.round(startTime) - state.filter.start, + log_id: r.fullname.split('|')[1], duration: endTime - startTime, start_time_utc_millis: startTime, end_time_utc_millis: endTime, - segment_offsets: r.segment_start_times.map((x) => x - state.filter.start), + // TODO: get this from the API, this isn't correct for segments with a time jump + segment_durations: r.segment_start_times.map((x, i) => r.segment_end_times[i] - x), }; }); @@ -374,6 +613,30 @@ export function selectTimeFilter(start, end) { }; } +export function primeGetSubscription(dongleId, subscription) { + return { + type: Types.ACTION_PRIME_SUBSCRIPTION, + dongleId, + subscription, + }; +} + +export function urlForState(dongleId, log_id, start, end, prime) { + const path = [dongleId]; + + if (log_id) { + path.push(log_id); + if (start && end && start > 0) { + path.push(start); + path.push(end); + } + } else if (prime) { + path.push('prime'); + } + + return `/${path.join('/')}`; +} + export function analyticsEvent(name, parameters) { return { type: Types.ANALYTICS_EVENT, diff --git a/src/actions/index.test.js b/src/actions/index.test.js index 90449a0f..f4587123 100644 --- a/src/actions/index.test.js +++ b/src/actions/index.test.js @@ -15,7 +15,7 @@ describe('timeline actions', () => { it('should push history state when editing zoom', () => { const dispatch = jest.fn(); const getState = jest.fn(); - const actionThunk = pushTimelineRange(123, 1234); + const actionThunk = pushTimelineRange("log_id", 123, 1234); getState.mockImplementationOnce(() => ({ dongleId: 'statedongle', @@ -23,6 +23,6 @@ describe('timeline actions', () => { zoom: {}, })); actionThunk(dispatch, getState); - expect(push).toBeCalledWith('/statedongle/123/1234'); + expect(push).toBeCalledWith('/statedongle/log_id/123/1234'); }); }); diff --git a/src/actions/types.js b/src/actions/types.js index d6a6a686..96d88ada 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -28,7 +28,6 @@ export const ACTION_BUFFER_VIDEO = 'action_buffer_video'; export const ACTION_RESET = 'action_reset'; // segments -export const ACTION_UPDATE_SEGMENTS = 'update_segments'; export const ACTION_ROUTES_METADATA = 'routes_metadata'; // files diff --git a/src/components/AppHeader/index.jsx b/src/components/AppHeader/index.jsx index ebee8d76..0d69a735 100644 --- a/src/components/AppHeader/index.jsx +++ b/src/components/AppHeader/index.jsx @@ -58,7 +58,7 @@ const styles = () => ({ }); const AppHeader = ({ - profile, classes, dispatch, drawerIsOpen, annotating, showDrawerButton, + profile, classes, dispatch, drawerIsOpen, viewingRoute, showDrawerButton, forwardRef, handleDrawerStateChanged, primeNav, dongleId, }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -112,7 +112,7 @@ const AppHeader = ({
- {Boolean(!primeNav && !annotating && dongleId) && } + {Boolean(!primeNav && !viewingRoute && dongleId) && }
diff --git a/src/components/Dashboard/DriveListItem.jsx b/src/components/Dashboard/DriveListItem.jsx index bfa5b64e..cdb7c892 100644 --- a/src/components/Dashboard/DriveListItem.jsx +++ b/src/components/Dashboard/DriveListItem.jsx @@ -84,7 +84,7 @@ const DriveListItem = (props) => { }, [drive, dispatch, isVisible, el]); const onClick = filterRegularClick( - () => dispatch(pushTimelineRange(drive.start_time_utc_millis, drive.end_time_utc_millis)), + () => dispatch(pushTimelineRange(drive.log_id, 0, drive.duration, true)), ); const small = windowWidth < 580; @@ -117,7 +117,7 @@ const DriveListItem = (props) => { key={drive.fullname} className={`${classes.drive} DriveEntry`} ref={el} - href={`/${drive.dongle_id}/${drive.start_time_utc_millis}/${drive.end_time_utc_millis}`} + href={`/${drive.dongle_id}/${drive.log_id}`} onClick={onClick} >
@@ -148,7 +148,7 @@ const DriveListItem = (props) => { ); diff --git a/src/components/DriveVideo/index.jsx b/src/components/DriveVideo/index.jsx index a44023ff..3be4d25a 100644 --- a/src/components/DriveVideo/index.jsx +++ b/src/components/DriveVideo/index.jsx @@ -12,7 +12,6 @@ import Colors from '../../colors'; import { ErrorOutline } from '../../icons'; import { currentOffset } from '../../timeline'; import { seek, bufferVideo } from '../../timeline/playback'; -import { updateSegments } from '../../timeline/segments'; const VideoOverlay = ({ loading, error }) => { let content; @@ -203,10 +202,6 @@ class DriveVideo extends Component { syncVideo() { const { dispatch, isBufferingVideo, currentRoute, routes } = this.props; if (!currentRoute) { - dispatch(updateSegments()); - if (routes && isBufferingVideo) { - dispatch(bufferVideo(false)); - } return; } @@ -269,7 +264,6 @@ class DriveVideo extends Component { if (!currentRoute) { return 0; } - offset -= currentRoute.offset; if (currentRoute.videoStartOffset) { offset -= currentRoute.videoStartOffset; diff --git a/src/components/DriveView/index.jsx b/src/components/DriveView/index.jsx index b64d04bf..859268a6 100644 --- a/src/components/DriveView/index.jsx +++ b/src/components/DriveView/index.jsx @@ -23,25 +23,26 @@ class DriveView extends Component { this.props.dispatch(popTimelineRange()); } else if (currentRoute) { this.props.dispatch( - pushTimelineRange(currentRoute.start_time_utc_millis, currentRoute.end_time_utc_millis), + pushTimelineRange(currentRoute.log_id, null, null), ); } } close() { - this.props.dispatch(pushTimelineRange(null, null)); + this.props.dispatch(pushTimelineRange(null, null, null)); } render() { - const { dongleId, zoom, routes, currentRoute } = this.props; + const { dongleId, zoom, currentRoute, routes } = this.props; - const currentRouteBoundsSelected = currentRoute?.start_time_utc_millis === zoom.start && currentRoute?.end_time_utc_millis === zoom.end; - const backButtonDisabled = !zoom.previousZoom && currentRouteBoundsSelected; + const currentRouteBoundsSelected = zoom.start === 0 && zoom.end === currentRoute?.duration; + const backButtonDisabled = !zoom?.previousZoom && currentRouteBoundsSelected; // FIXME: end time not always same day as start time - const startDay = dayjs(zoom.start).format('dddd'); - const startTime = dayjs(zoom.start).format('MMM D @ HH:mm'); - const endTime = dayjs(zoom.end).format('HH:mm'); + const start = currentRoute.start_time_utc_millis + zoom.start; + const startDay = dayjs(start).format('dddd'); + const startTime = dayjs(start).format('MMM D @ HH:mm'); + const endTime = dayjs(start + (zoom.end - zoom.start)).format('HH:mm'); return (
diff --git a/src/components/TimeDisplay/index.jsx b/src/components/TimeDisplay/index.jsx index 45c9f20b..de3b9913 100644 --- a/src/components/TimeDisplay/index.jsx +++ b/src/components/TimeDisplay/index.jsx @@ -150,8 +150,8 @@ class TimeDisplay extends Component { getDisplayTime() { const offset = currentOffset(); - const { filter, currentRoute } = this.props; - const now = new Date(offset + filter.start); + const { currentRoute } = this.props; + const now = new Date(offset + currentRoute.start_time_utc_millis); if (Number.isNaN(now.getTime())) { return '...'; } @@ -313,7 +313,6 @@ const stateToProps = Obstruction({ currentRoute: 'currentRoute', zoom: 'zoom', desiredPlaySpeed: 'desiredPlaySpeed', - filter: 'filter', }); export default connect(stateToProps)(withStyles(styles)(TimeDisplay)); diff --git a/src/components/Timeline/index.jsx b/src/components/Timeline/index.jsx index a89816c2..3f25f01f 100644 --- a/src/components/Timeline/index.jsx +++ b/src/components/Timeline/index.jsx @@ -233,6 +233,8 @@ class Timeline extends Component { } handlePointerUp(ev) { + const { route } = this.props; + // prevent preventDefault for back(3) and forward(4) mouse buttons if (ev.button !== 3 && ev.button !== 4) { ev.preventDefault(); @@ -257,11 +259,11 @@ class Timeline extends Component { if (offset < startOffset || offset > endOffset) { this.props.dispatch(seek(startOffset)); } - const { filter, dispatch } = this.props; - const startTime = startOffset + filter.start; - const endTime = endOffset + filter.start; + const { dispatch } = this.props; + const startTime = startOffset; + const endTime = endOffset; - dispatch(pushTimelineRange(startTime, endTime)); + dispatch(pushTimelineRange(route.log_id, startTime, endTime, true)); } else if (ev.currentTarget !== document) { this.handleClick(ev); } @@ -297,14 +299,12 @@ class Timeline extends Component { percentToOffset(perc) { const { zoom } = this.state; - const { filter } = this.props; - return perc * (zoom.end - zoom.start) + (zoom.start - filter.start); + return perc * (zoom.end - zoom.start) + zoom.start; } offsetToPercent(offset) { const { zoom } = this.state; - const { filter } = this.props; - return (offset - (zoom.start - filter.start)) / (zoom.end - zoom.start); + return (offset - zoom.start) / (zoom.end - zoom.start); } segmentNum(offset) { @@ -316,28 +316,16 @@ class Timeline extends Component { } renderRoute() { - const { classes, route, filter } = this.props; + const { classes, route } = this.props; const { zoom } = this.state; if (!route.events) { return null; } - const range = filter.start - filter.end; - let startPerc = (100 * route.offset) / range; - let widthPerc = (100 * route.duration) / range; - - const startOffset = zoom.start - filter.start; - const endOffset = zoom.end - filter.start; - const zoomDuration = endOffset - startOffset; - if (route.offset > endOffset) { - return []; - } - if (route.offset + route.duration < startOffset) { - return []; - } - startPerc = (100 * (route.offset - startOffset)) / zoomDuration; - widthPerc = (100 * route.duration) / zoomDuration; + const zoomDuration = zoom.end - zoom.start; + const startPerc = (100 * (-zoom.start)) / zoomDuration; + const widthPerc = (100 * route.duration) / zoomDuration; const style = { width: `${widthPerc}%`, @@ -376,7 +364,7 @@ class Timeline extends Component { } render() { - const { classes, hasRuler, filter, className, route, thumbnailsVisible } = this.props; + const { classes, hasRuler, className, route, thumbnailsVisible } = this.props; const { thumbnail, hoverX, dragging } = this.state; const hasRulerCls = hasRuler ? 'hasRuler' : ''; @@ -392,7 +380,7 @@ class Timeline extends Component { const hoverOffset = this.percentToOffset((hoverX - rulerBounds.x) / rulerBounds.width); hoverStyle = { left: Math.max(-10, Math.min(rulerBounds.width - 70, hoverX - rulerBounds.x - 40)) }; if (!Number.isNaN(hoverOffset)) { - hoverString = dayjs(filter.start + hoverOffset).format('HH:mm:ss'); + hoverString = dayjs(route.start_time_utc_millis + hoverOffset).format('HH:mm:ss'); const segNum = this.segmentNum(hoverOffset); if (segNum !== null) { hoverString = `${segNum}, ${hoverString}`; @@ -461,7 +449,6 @@ class Timeline extends Component { const stateToProps = Obstruction({ zoom: 'zoom', loop: 'loop', - filter: 'filter', }); export default connect(stateToProps)(withStyles(styles)(Timeline)); diff --git a/src/components/Timeline/thumbnails.jsx b/src/components/Timeline/thumbnails.jsx index 9f1e7d91..4424591e 100644 --- a/src/components/Timeline/thumbnails.jsx +++ b/src/components/Timeline/thumbnails.jsx @@ -33,7 +33,7 @@ export default function Thumbnails(props) { currSegment.length += 1; } else { // 12 per file, 5s each - const seconds = Math.floor((offset - route.offset) / 1000); + const seconds = Math.floor(offset / 1000); const imageIndex = Math.max(0, Math.min(Math.floor(seconds / 5), 11)); const segmentNum = getSegmentNumber(route, offset); const url = `${route.url}/${segmentNum}/sprite.jpg`; diff --git a/src/components/explorer.jsx b/src/components/explorer.jsx index 8e0c6599..35f82513 100644 --- a/src/components/explorer.jsx +++ b/src/components/explorer.jsx @@ -172,7 +172,7 @@ class ExplorerApp extends Component { } render() { - const { classes, zoom, devices, dongleId } = this.props; + const { classes, zoom, currentRoute, devices, dongleId } = this.props; const { drawerIsOpen, pairLoading, pairError, pairDongleId, windowWidth } = this.state; const noDevicesUpsell = (devices?.length === 0 && !dongleId); @@ -197,13 +197,15 @@ class ExplorerApp extends Component { minHeight: `calc(100vh - ${headerHeight}px)`, }; + console.log("current route", currentRoute, zoom) + return (
this.setState({ windowWidth: ww }) } /> { noDevicesUpsell ? - : (zoom ? : )} + : (currentRoute ? : )}
@@ -249,6 +251,7 @@ const stateToProps = Obstruction({ pathname: 'router.location.pathname', dongleId: 'dongleId', devices: 'devices', + currentRoute: 'currentRoute', }); export default connect(stateToProps)(withStyles(styles)(ExplorerApp)); diff --git a/src/initialState.js b/src/initialState.js index ff6b7ac1..18ef0d31 100644 --- a/src/initialState.js +++ b/src/initialState.js @@ -1,4 +1,4 @@ -import { getDongleID, getZoom, getPrimeNav } from './url'; +import { getDongleID, getPrimeNav, getSegmentRange } from './url'; import * as Demo from './demo'; export function getDefaultFilter() { @@ -18,27 +18,13 @@ export function getDefaultFilter() { }; } -function getDefaultLoop(pathname) { - // in time instead of offset - // this makes it so that the timespan can change without this changing - // thats helpful to shared links and other things probably... - const zoom = getZoom(pathname); - if (zoom) { - return { - startTime: zoom.start, - duration: zoom.end - zoom.start, - }; - } - return null; -} - export default { dongleId: getDongleID(window.location.pathname), - desiredPlaySpeed: 1, // speed set by user + desiredPlaySpeed: 1, // speed set by user isBufferingVideo: true, // if we're currently buffering for more data - offset: null, // in miliseconds, relative to `state.filter.start` - startTime: Date.now(), // millisecond timestamp in which play began + offset: null, // in miliseconds, relative to state.zoom.start + startTime: Date.now(), // millisecond timestamp in which play began routes: null, routesMeta: { @@ -63,6 +49,7 @@ export default { }, filter: getDefaultFilter(), - zoom: getZoom(window.location.pathname), - loop: getDefaultLoop(window.location.pathname), + zoom: null, + loop: null, + segmentRange: getSegmentRange(window.location.pathname), }; diff --git a/src/reducers/globalState.js b/src/reducers/globalState.js index fc0fdb39..d57bcf4d 100644 --- a/src/reducers/globalState.js +++ b/src/reducers/globalState.js @@ -269,17 +269,28 @@ export default function reducer(_state, action) { if (!state.zoom || !action.start || !action.end || action.start < state.zoom.start || action.end > state.zoom.end) { state.files = null; } - if (action.start && action.end) { - state.zoom = { - start: action.start, - end: action.end, - previous: state.zoom, - }; + const r = state.routes?.find((route) => route.log_id === action.log_id); + if (action.log_id && r) { + state.currentRoute = r; + if (!action.start) { + state.zoom = { + start: 0, + end: state.currentRoute.duration, + previous: state.zoom, + } + } else { + state.zoom = { + start: action.start, + end: action.end, + previous: state.zoom, + }; + } } else { state.zoom = null; state.loop = null; + state.currentRoute = null; } - break; + break; case Types.ACTION_FILES_URLS: state.files = { ...(state.files !== null ? { ...state.files } : {}), @@ -325,6 +336,26 @@ export default function reducer(_state, action) { start: action.start, end: action.end, }; + + if (!state.currentRoute && state.segmentRange) { + const curr = state.routes?.find((route) => route.log_id === state.segmentRange.log_id); + if (curr) { + state.currentRoute = { + ...curr, + }; + if (state.segmentRange.start && state.segmentRange.end) { + state.zoom = { + start: state.segmentRange.start, + end: state.segmentRange.end, + }; + } else { + state.zoom = { + start: 0, + end: state.currentRoute.duration, + }; + } + } + } break; default: return state; diff --git a/src/reducers/index.js b/src/reducers/index.js index 10f491a6..6a7d8d6d 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,11 +1,9 @@ import { reducer as playbackReducer } from '../timeline/playback'; -import { reducer as segmentsReducers } from '../timeline/segments'; import globalState from './globalState'; const reducers = [ globalState, playbackReducer, - segmentsReducers, ]; -export default reducers; +export default reducers; \ No newline at end of file diff --git a/src/timeline/index.js b/src/timeline/index.js index f1674329..13e797a0 100644 --- a/src/timeline/index.js +++ b/src/timeline/index.js @@ -1,7 +1,7 @@ import store from '../store'; /** - * Get current playback offset, relative to `state.filter.start` + * Get current playback offset * * @param {object} state * @returns {number} @@ -14,7 +14,7 @@ export function currentOffset(state = null) { /** @type {number} */ let offset; if (state.offset === null && state.loop?.startTime) { - offset = state.loop.startTime - state.filter.start; + offset = state.loop.startTime; } else { const playSpeed = state.isBufferingVideo ? 0 : state.desiredPlaySpeed; offset = state.offset + ((Date.now() - state.startTime) * playSpeed); @@ -22,7 +22,7 @@ export function currentOffset(state = null) { if (offset !== null && state.loop?.startTime) { // respect the loop - const loopOffset = state.loop.startTime - state.filter.start; + const loopOffset = state.loop.startTime; if (offset < loopOffset) { offset = loopOffset; } else if (offset > loopOffset + state.loop.duration) { @@ -31,21 +31,4 @@ export function currentOffset(state = null) { } return offset; -} - -/** - * Get current route - * - * @param {object} state - * @param {number} [offset] - * @returns {*|null} - */ -export function getCurrentRoute(state, offset) { - if (!state.routes) return null; - - offset = offset || currentOffset(state); - if (offset === null) return null; - - return state.routes - .find((route) => offset >= route.offset && offset <= route.offset + route.duration); -} +} \ No newline at end of file diff --git a/src/timeline/playback.js b/src/timeline/playback.js index da803029..3c1a4c03 100644 --- a/src/timeline/playback.js +++ b/src/timeline/playback.js @@ -7,7 +7,7 @@ export function reducer(_state, action) { let state = { ..._state }; let loopOffset = null; if (state.loop && state.loop.startTime !== null) { - loopOffset = state.loop.startTime - state.filter.start; + loopOffset = state.loop.startTime; } switch (action.type) { case Types.ACTION_SEEK: @@ -74,8 +74,8 @@ export function reducer(_state, action) { break; } - if (state.currentRoute && state.currentRoute.videoStartOffset && state.loop && state.zoom && state.filter - && state.loop.startTime === state.zoom.start && state.filter.start + state.currentRoute.offset === state.zoom.start) { + if (state.currentRoute && state.currentRoute.videoStartOffset && state.loop && state.zoom + && state.loop.startTime === state.zoom.start && state.zoom.start === 0) { const loopRouteOffset = state.loop.startTime - state.zoom.start; if (state.currentRoute.videoStartOffset > loopRouteOffset) { state.loop = { @@ -89,7 +89,7 @@ export function reducer(_state, action) { if (state.offset !== null && state.loop?.startTime) { const playSpeed = state.isBufferingVideo ? 0 : state.desiredPlaySpeed; const offset = state.offset + (Date.now() - state.startTime) * playSpeed; - loopOffset = state.loop.startTime - state.filter.start; + loopOffset = state.loop.startTime; // has loop, trap offset within the loop if (offset < loopOffset) { state.startTime = Date.now(); diff --git a/src/timeline/playback.test.js b/src/timeline/playback.test.js index 5d56cbbc..54e02546 100644 --- a/src/timeline/playback.test.js +++ b/src/timeline/playback.test.js @@ -5,11 +5,7 @@ import { bufferVideo, pause, play, reducer, seek, selectLoop } from './playback' const makeDefaultStruct = function makeDefaultStruct() { return { - filter: { - start: Date.now(), - end: Date.now() + 100000, - }, - desiredPlaySpeed: 1, // 0 = stopped, 1 = playing, 2 = 2x speed... multiplier on speed + desiredPlaySpeed: 1, // 0 = stopped, 1 = playing, 2 = 2x speed offset: 0, // in miliseconds from the start startTime: Date.now(), // millisecond timestamp in which play began @@ -81,14 +77,14 @@ describe('playback', () => { // set up loop state = reducer(state, play()); state = reducer(state, selectLoop( - state.filter.start + 1000, - state.filter.start + 2000, + 1000, + 2000, )); - expect(state.loop.startTime).toEqual(state.filter.start + 1000); + expect(state.loop.startTime).toEqual(1000); // seek past loop end boundary a state = reducer(state, seek(3000)); - expect(state.loop.startTime).toEqual(state.filter.start + 1000); + expect(state.loop.startTime).toEqual(1000); expect(state.offset).toEqual(2000); }); @@ -99,14 +95,14 @@ describe('playback', () => { // set up loop state = reducer(state, play()); state = reducer(state, selectLoop( - state.filter.start + 1000, - state.filter.start + 2000, + 1000, + 2000, )); - expect(state.loop.startTime).toEqual(state.filter.start + 1000); + expect(state.loop.startTime).toEqual(1000); // seek past loop end boundary a state = reducer(state, seek(0)); - expect(state.loop.startTime).toEqual(state.filter.start + 1000); + expect(state.loop.startTime).toEqual(1000); expect(state.offset).toEqual(1000); }); diff --git a/src/timeline/segments.js b/src/timeline/segments.js index 410f8a43..413f3e52 100644 --- a/src/timeline/segments.js +++ b/src/timeline/segments.js @@ -1,31 +1,7 @@ -import * as Types from '../actions/types'; -import { getCurrentRoute } from '.'; - -export function reducer(_state, action) { - let state = { ..._state }; - switch (action.type) { - case Types.ACTION_UPDATE_SEGMENTS: - state = { - ...state, - }; - break; - default: - break; - } - - const currentRoute = getCurrentRoute(state); - state.currentRoute = currentRoute ? { ...currentRoute } : null; - - return state; -} - -export function updateSegments() { - return { - type: Types.ACTION_UPDATE_SEGMENTS, - }; -} - export function getSegmentFetchRange(state) { + // TODO: fix this for relative routes + return state.filter; + if (!state.zoom) { return state.filter; } diff --git a/src/timeline/segments.test.js b/src/timeline/segments.test.js index 34f98410..592c5c67 100644 --- a/src/timeline/segments.test.js +++ b/src/timeline/segments.test.js @@ -1,78 +1,9 @@ /* eslint-env jest */ -import { getCurrentRoute } from '.'; import { hasRoutesData } from './segments'; -import { getSegmentNumber } from '../utils'; export const SEGMENT_LENGTH = 1000 * 60; -const routes = [{ - fullname: '99c94dc769b5d96e|2018-04-09--10-10-00', - offset: 36600000, - duration: 2558000, - segment_numbers: Array.from(Array(43).keys()), - segment_offsets: Array.from(Array(43).keys()).map((i) => i * SEGMENT_LENGTH + 36600000), - events: [{ - time: 36600123, - type: 'event', - }], -}, { - fullname: '99c94dc769b5d96e|2018-04-09--11-29-08', - offset: 41348000, - duration: 214000, - segment_numbers: Array.from(Array(4).keys()), - segment_offsets: Array.from(Array(4).keys()).map((i) => i * SEGMENT_LENGTH + 41348000), - events: [{ - time: 41348123, - type: 'event', - }], -}]; - describe('segments', () => { - it('finds current segment', async () => { - const [route] = routes; - let r = getCurrentRoute({ - routes, - offset: route.offset, - desiredPlaySpeed: 1, - startTime: Date.now(), - }); - expect(r.fullname).toBe(route.fullname); - expect(getSegmentNumber(r, route.offset)).toBe(0); - - r = getCurrentRoute({ - routes, - offset: route.offset + SEGMENT_LENGTH * 1.1, - desiredPlaySpeed: 1, - startTime: Date.now(), - }); - expect(getSegmentNumber(r, route.offset + SEGMENT_LENGTH * 1.1)).toBe(1); - }); - - it('finds last segment of a route', async () => { - const [route] = routes; - const offset = route.offset + SEGMENT_LENGTH * (route.segment_offsets.length - 1) + 1000; - const r = getCurrentRoute({ - routes, - offset, - desiredPlaySpeed: 1, - startTime: Date.now(), - }); - expect(r.fullname).toBe(route.fullname); - expect(getSegmentNumber(r, offset)).toBe(route.segment_offsets.length - 1); - }); - - it('ends last segment of a route', async () => { - const [route] = routes; - const offset = route.offset + route.duration - 10; - const r = getCurrentRoute({ - routes, - offset, - desiredPlaySpeed: 1, - startTime: Date.now() - 50, - }); - expect(getSegmentNumber(r, offset)).toBe(null); - }); - it('can check if it has segment metadata', () => { expect(hasRoutesData()).toBe(false); expect(hasRoutesData({})).toBe(false); diff --git a/src/url.js b/src/url.js index 734cf456..a47fc364 100644 --- a/src/url.js +++ b/src/url.js @@ -39,11 +39,25 @@ export function getZoom(pathname) { return null; } +export function getSegmentRange(pathname) { + let parts = pathname.split('/'); + parts = parts.filter((m) => m.length); + + if (parts.length >= 2 && logIdRegex.test(parts[1])) { + return { + log_id: parts[1], + start: Number(parts[2]), + end: Number(parts[3]), + }; + } + return null; +} + export function getPrimeNav(pathname) { let parts = pathname.split('/'); parts = parts.filter((m) => m.length); - if (parts.length === 2 && parts[0] !== 'auth' && parts[1] === 'prime') { + if (parts.length === 2 && dongleIdRegex.test(parts[0]) && parts[1] === 'prime') { return true; } return false; diff --git a/src/utils/index.js b/src/utils/index.js index d529a1fa..a5c61b73 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -187,11 +187,16 @@ export function getSegmentNumber(route, offset) { if (offset === undefined) { offset = currentOffset(); } - for (let i = 0; i < route.segment_offsets.length; i++) { - if (offset >= route.segment_offsets[i] - && (i === route.segment_offsets.length - 1 || offset < route.segment_offsets[i + 1])) { + + return Math.floor(offset / (60*1000)); + + /* + for (let i = 0; i < route.segment_durations.length; i++) { + console.log(offset, route.segment_durations.slice(0, i+1).reduce((acc, val) => acc + val, 0)) + if (offset >= route.segment_durations.slice(0, i+1).reduce((acc, val) => acc + val, 0)) { return route.segment_numbers[i]; } } return null; + */ }