diff --git a/src/components/RouteVideoPlayer.tsx b/src/components/RouteVideoPlayer.tsx index 4f91c6a3..681117c7 100644 --- a/src/components/RouteVideoPlayer.tsx +++ b/src/components/RouteVideoPlayer.tsx @@ -9,6 +9,7 @@ type RouteVideoPlayerProps = { class?: string routeName: string onProgress?: (seekTime: number) => void + ref?: (el: HTMLVideoElement) => void } const RouteVideoPlayer: VoidComponent = (props) => { @@ -19,6 +20,9 @@ const RouteVideoPlayer: VoidComponent = (props) => { const timeUpdate = () => props.onProgress?.(video.currentTime) video.addEventListener('timeupdate', timeUpdate) onCleanup(() => video.removeEventListener('timeupdate', timeUpdate)) + if (props.ref) { + props.ref(video) + } }) let hls = new Hls() createEffect(() => { diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index 70f1ce3b..f79dc317 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -1,5 +1,5 @@ -import { For, createResource, Show, Suspense } from 'solid-js' -import type { VoidComponent } from 'solid-js' +import { For, createResource, createSignal, createEffect, createMemo, Show, Suspense } from 'solid-js' +import type { VoidComponent, Accessor } from 'solid-js' import clsx from 'clsx' import { TimelineEvent, getTimelineEvents } from '~/api/derived' @@ -92,43 +92,87 @@ function renderTimelineEvents( ) } -// TODO: align to first camera frame event -function renderMarker(route: Route | undefined, seekTime: number | undefined) { - if (!route) return null - if (seekTime === undefined) return null - - const duration = getRouteDuration(route)?.asSeconds() ?? 0 - const offsetPct = (seekTime / duration) * 100 - return ( -
- ) -} - interface TimelineProps { class?: string routeName: string - seekTime?: number + seekTime: Accessor + updateTime: (newTime: number) => void } const Timeline: VoidComponent = (props) => { const [route] = createResource(() => props.routeName, getRoute) const [events] = createResource(route, getTimelineEvents) + // TODO: align to first camera frame event + const [markerOffsetPct, setMarkerOffsetPct] = createSignal(0) + const duration = createMemo(() => + route() + ? getRouteDuration(route()!)?.asSeconds() ?? 0 + : 0, + ) + + let ref: HTMLDivElement + let handledTouchStart = false + + function updateMarker(clientX: number, rect: DOMRect) { + const x = clientX - rect.left + const fraction = x / rect.width + // Update marker immediately without waiting for video + setMarkerOffsetPct(fraction * 100) + const newTime = duration() * fraction + props.updateTime(newTime) + } + + function onMouseDownOrTouchStart(ev: MouseEvent | TouchEvent) { + if (handledTouchStart || !route()) return + + const rect = ref.getBoundingClientRect() + + if (ev.type === 'mousedown') { + ev = ev as MouseEvent + updateMarker(ev.clientX, rect) + const onMove = (moveEv: MouseEvent) => { + updateMarker(moveEv.clientX, rect) + } + const onUpOrLeave = () => { + ref.removeEventListener('mousemove', onMove) + ref.removeEventListener('mouseup', onUpOrLeave) + ref.removeEventListener('mouseleave', onUpOrLeave) + } + ref.addEventListener('mousemove', onMove) + ref.addEventListener('mouseup', onUpOrLeave) + ref.addEventListener('mouseleave', onUpOrLeave) + } else { + ev = ev as TouchEvent + if (ev.touches.length === 1) { + updateMarker(ev.touches[0].clientX, rect) + } + } + } + + createEffect(() => { + setMarkerOffsetPct((props.seekTime() / duration()) * 100) + }) return (
{ + handledTouchStart = false + onMouseDownOrTouchStart(ev) + handledTouchStart = true + }} + onTouchMove={(ev) => { + if (ev.touches.length !== 1 || !route()) return + const rect = ref.getBoundingClientRect() + updateMarker(ev.touches[0].clientX, rect) + }} > }> @@ -137,7 +181,14 @@ const Timeline: VoidComponent = (props) => { {(events) => renderTimelineEvents(route, events)} - {renderMarker(route, props.seekTime)} +
)} diff --git a/src/pages/dashboard/activities/RouteActivity.tsx b/src/pages/dashboard/activities/RouteActivity.tsx index d4aa35f7..75db7103 100644 --- a/src/pages/dashboard/activities/RouteActivity.tsx +++ b/src/pages/dashboard/activities/RouteActivity.tsx @@ -30,6 +30,12 @@ const RouteActivity: VoidComponent = (props) => { const [route] = createResource(routeName, getRoute) const [startTime] = createResource(route, (route) => parseDateStr(route.start_time)?.format('ddd, MMM D, YYYY')) + let videoRef: HTMLVideoElement + + function onTimelineChange(newTime: number) { + videoRef.currentTime = newTime + } + return ( <> arrow_back}> @@ -42,12 +48,17 @@ const RouteActivity: VoidComponent = (props) => {
} > - + videoRef = ref} routeName={routeName()} onProgress={setSeekTime} />

Timeline

- + }>