D
DreamLake

API

Architecture

Layer Diagram

TimelineClock         Pure time source (tick + seek events). No playlist knowledge.

Playlist              Parses m3u8, loads segments, LRU cache, auto-prefetch, live poll.

useSegment            Raw decoded current segment (JSONL events, VTT — discrete data).
useSegmentTrack       Current segment → columnar tracks (no cross-segment merge).
useMergedTrack        Current + contiguous neighbors merged (IMU, joints, pose).
useTrackSample        Interpolated query of a track at a precise time.

ClockProvider         Hands the clock down the component tree via React context.

View components       VideoPlayer, SubtitleView, ActionLabelView,
                      ImuChartView, JointAngleView, PoseView.

Each layer has one job. No circular dependencies.

Component Hierarchy

App
├── useTimeline()
│     ├── clock: TimelineClock        (plain JS, owns RAF loop)
│     └── state: TimelineState        (playing/rate/loop/duration — seek events only)

├── <ClockProvider clock={clock}>     (injects clock into context)

├── VideoPlayer { src }
│     ├── hls.js handles m3u8 + segment loading + video buffering
│     ├── <video>.durationchange → clock.extendDuration()
│     └── clock seek events → video.play/pause/seek/playbackRate

├── ActionLabelView { src }
│     ├── usePlaylist(options)                → engine + clock.extendDuration()
│     ├── useSegment(engine)                  → useClockValue(10) + resolveSegment()
│     └── useClockValue(4)                    → highlight active entry

├── ImuChartView { src } / JointAngleView { src } / PoseView { src }
│     ├── usePlaylist(options)
│     ├── useMergedTrack(engine[, options])  → merge Float32Arrays
│     ├── useClockValue(fps)                  → React render rate
│     └── useTrackSample(track, time[, interp]) → interpolated sample

├── SubtitleView { src }
│     ├── usePlaylist(options)
│     ├── useSegment(engine)
│     └── useClockValue(10)

└── TimelineController { state, onPlay, onPause, onSeek, ... }
      └── useClockValue(30) → scrubber position

Every view accepts an optional clock={…} prop that overrides the context value when present.

Data Flow

Discrete data (ActionLabelView, SubtitleView)

1. usePlaylist({ url })
     ├─ engine.init() → fetch + parse m3u8
     └─ clock.extendDuration(totalDuration)
 
2. useSegment(engine)
     ├─ useClockValue(10) → time at ~10fps
     └─ resolveSegment(playlist.segments, time)
          segment index changed?
            yes → engine.getDataAtTime(time) → fetch + decode + LRU cache
                  + prefetchAhead() → background fetch next N segments
            no  → skip (no React re-render)
 
3. useClockValue(4) → highlight active entry at ~4fps

Continuous data (ImuChartView, JointAngleView, PoseView)

1. usePlaylist + clock.extendDuration — same as above
 
2. useMergedTrack(engine[, { normalize }])
     ├─ On segment change → engine.getDataAtTime(time) for N neighbors
     │    → decoder returns [{ts, data:[…]}, …]
     │    → normalize() → { trackName: {times, values, stride} }
     │    → rebuildMerged() → contiguous Float32Arrays (gap-safe)
     └─ Returns: Map<trackName, TrackSamples>
 
3. React render path (ImuChartView / JointAngleView / PoseView)
     ├─ useClockValue(fps) → time at N fps
     └─ useTrackSample(track, time, interp) → reused Float32Array[stride]
 
(Optional 60fps Canvas path — see custom-views)
     ├─ clock.on('tick')
     ├─ sampleTrack(track, clock.time, lerp, hint, out)
     └─ ctx.* → imperative draw, no React

Clock Events

Two events. No segment tracking on the clock.

TimelineClock
├─ play()   → seek{source:'play'}  → starts RAF → tick at ~60fps
├─ RAF      → tick{time, playing, rate}
├─ seek(t)  → seek{source:'seek'} + tick{time}
├─ pause()  → seek{source:'pause'} → stops RAF
└─ end      → tick + seek{source:'pause'}

Who Subscribes to What

tick (~60fps)seek (explicit)
useTimelinestate: playing/rate/loop/duration
TimelineControllervia useClockValue(30)via useTimeline state
VideoPlayerplay/pause/seek/rate
ActionLabelViewvia useClockValue(10+4)force reload via useSegment
SubtitleViewvia useClockValue(10)force reload via useSegment
ImuChartView / JointAngleView / PoseViewvia useClockValue(fps)cache reset in useMergedTrack
Custom Canvas viewsdirect clock.on('tick')

Performance Model

ComponentReact re-renders/secCanvas draws/sec
App0
useTimeline state~0 (seek events only)
TimelineController~30
VideoPlayer0
ActionLabelView~4
SubtitleView~10
ImuChartView / JointAngleView~15
PoseView~30
Custom Canvas view~0~60

Key insight: useTimeline state does NOT contain currentTime. Consumers choose their own render frequency via useClockValue(fps).

Duration Auto-Detection

useTimeline()                          → clock.duration = 0
usePlaylist(jointsUrl)                 → clock.extendDuration(30)   → duration = 30
usePlaylist(actionsUrl)                → clock.extendDuration(25)   → max(30, 25) = 30
VideoPlayer (via durationchange)       → clock.extendDuration(1800) → duration = 1800
live poll discovers new segments       → clock.extendDuration(1810) → duration = 1810

Multiple playlists with different durations on the same clock — clock always extends to the maximum.

Clock Resolution

Every consumer accepts an optional clock prop and calls useClockContext(clock) internally:

explicit prop → context value (from <ClockProvider>) → throw

The throw is intentional — it catches trees where a view was added without a provider, rather than failing silently at first interaction.