Chapter 3: PlaybackControl#
Welcome back! In Chapter 1, we introduced the XVIZ Loader Interface, the standard way to load and access your AV log data. In Chapter 2, we learned about the LogViewer, the main component that takes data from a loader and visualizes a single frame in a beautiful 3D scene.
But logs are dynamic! They represent a sequence of events over time. Just seeing one static frame isn't very useful. We need a way to navigate through the log's timeline β to play it forward, pause it, or jump to a specific moment.
This is exactly what the PlaybackControl component provides.
Think of the PlaybackControl as the familiar control bar you see on a video player (like YouTube or Netflix). It gives the user the ability to control the "playback head" or the current time position within the log data.
What is PlaybackControl?#
PlaybackControl is a React component designed to provide a user interface for controlling the time dimension of an XVIZ log. Its main function is to connect to an XVIZ Loader Interface instance and offer interactive elements like:
- Play/Pause button: To start or stop advancing through the log's time.
- Timeline/Scrubber: A visual representation of the log's duration, showing the current position and allowing the user to click or drag to seek to a different time.
- Buffered Range Indicator: Often visually shows which parts of the log data have been loaded into memory, helping the user understand if seeking to a distant time is possible.
- Current Time Display: Shows the exact timestamp or time offset currently being viewed.
- Look Ahead Slider (Optional): Allows controlling how far into the future the viewer should display data (if the log supports it).
Just like the LogViewer, the PlaybackControl doesn't load data itself; it relies entirely on the XVIZ Loader Interface instance you provide it.
Using the PlaybackControl#
To use the PlaybackControl, you need to:
- Have an instance of an object that implements the XVIZ Loader Interface (like
XVIZFileLoader
orXVIZStreamLoader
). - Pass this loader instance to the
log
prop of the<PlaybackControl />
component.
Let's see a simple example, building on our previous chapters:
import React from 'react';
import { PlaybackControl } from 'streetscape.gl';
import { XVIZFileLoader } from 'streetscape.gl'; // Or your preferred loader
// Assume 'logLoader' is already created and connected (as in Chapter 1)
// const logLoader = new XVIZFileLoader({...});
// logLoader.connect();
function MyLogControls({ logLoader }) {
return (
<div style={{ width: '800px', marginTop: '20px' }}>
<PlaybackControl log={logLoader} />
</div>
);
}
In this code:
- We import the
PlaybackControl
component. - We render
<PlaybackControl />
. - We pass our existing
logLoader
instance to thelog
prop.
That's the minimum required! Once rendered and connected to a ready log loader, the component will display the playback interface.
When the user clicks the play button or drags the timeline, the PlaybackControl will call methods on the logLoader
instance (specifically seek()
) to tell the loader to update its internal current time. Since the LogViewer is also connected to the same logLoader
, it will automatically receive the update notification and render the new frame for that time.
This is a key pattern in streetscape.gl
: multiple components (like LogViewer
and PlaybackControl
) connect to the same XVIZ Loader Interface instance. The loader acts as the single source of truth for the log's state (like current time, loaded data), and components update themselves automatically when the loader emits events.
Let's put the LogViewer and PlaybackControl together in a single component:
import React from 'react';
import { LogViewer, PlaybackControl } from 'streetscape.gl';
import { XVIZFileLoader } from 'streetscape.gl'; // Or your preferred loader
// Dummy loader for example structure - replace with your actual loader setup
const logLoader = new XVIZFileLoader({ /* ... options ... */ });
logLoader.connect(); // Don't forget to connect your actual loader!
const MAPBOX_TOKEN = 'YOUR_MAPBOX_ACCESS_TOKEN'; // If you're using Mapbox
function CompleteLogDisplay() {
// In a real app, you'd manage logLoader creation/connection in a higher component
// or hook, perhaps handling loading state.
// For this simplified example, we just use the instance directly.
return (
<div>
<div style={{ width: '100%', height: '60vh' }}>
{/* The LogViewer displays the 3D scene based on logLoader's current time */}
<LogViewer
log={logLoader} // Pass the loader
mapboxApiAccessToken={MAPBOX_TOKEN} // Optional: for map background
mapStyle="mapbox://styles/mapbox/light-v10"
// ... other LogViewer props ...
/>
</div>
<div style={{ width: '100%', padding: '0 20px' }}>
{/* The PlaybackControl provides the UI to change logLoader's current time */}
<PlaybackControl
log={logLoader} // Pass the *same* loader instance
// Optional: customize appearance/behavior
compact={false}
maxLookAhead={10} // Show look ahead slider
/>
</div>
</div>
);
}
In this combined example, both <LogViewer />
and <PlaybackControl />
receive the same logLoader
instance.
- The
LogViewer
listens for updates fromlogLoader
(likeupdate
events or when the current time changes) and re-renders the 3D scene. - The
PlaybackControl
also listens for updates fromlogLoader
(likeready
,update
to get buffer ranges, and when the current time changes) to update its UI (timeline position, buffered ranges). - When the user interacts with
PlaybackControl
(e.g., drags the scrubber or clicks play), it calls methods likelogLoader.seek(newTimestamp)
. - Calling
logLoader.seek()
changes the loader's internal state, triggers anupdate
event (or equivalent state change notification) from the loader, which bothLogViewer
andPlaybackControl
pick up, causing them to update their display accordingly.
This collaborative pattern makes streetscape.gl
components work together seamlessly.
Customizing the PlaybackControl#
The PlaybackControl
component offers several props to customize its appearance and behavior:
width
: Set the width of the control (e.g.,'100%'
,800
).compact
: Use a more compact layout (true
/false
).formatTimestamp
: A function to customize how the current time is displayed.formatTick
: A function to customize how timeline ticks are labeled.maxLookAhead
: Set the maximum value for the look ahead slider (set to0
to hide it).onSeek
: A callback function that is triggered when the user seeks. You can use this to perform actions before or instead of the default seek behavior.onPlay
,onPause
: Callbacks for when play/pause buttons are clicked.
Refer to the API documentation for a full list of customization options.
Under the Hood (A Simple Look)#
How does PlaybackControl interact with the XVIZ Loader Interface and update its display?
- Connection: Like LogViewer, PlaybackControl uses an internal mechanism (the
connectToLog
wrapper, which we'll explore in Chapter 5) to subscribe to state changes from thelog
prop. - Getting Log State: It automatically retrieves crucial information from the loader instance via methods like:
log.getLogStartTime()
: The beginning of the log's timeline.log.getLogEndTime()
: The end of the log's timeline.log.getCurrentTime()
: The current position of the playhead.log.getBufferedTimeRanges()
: An array of time ranges[[start1, end1], [start2, end2], ...]
that have been loaded into the buffer.log.getLookAhead()
: The current look ahead value.
- Rendering UI: Using this information (
startTime
,endTime
,currentTime
,buffered
), the component renders the visual timeline, the play/pause button state, the current time label, and the buffered ranges. - Handling Interaction: When a user interacts with the component (clicks play, drags the scrubber, changes the look ahead slider), PlaybackControl:
- If playing/pausing is handled internally by starting/stopping an animation loop.
- If seeking (dragging the scrubber), it gets the timestamp corresponding to the user's interaction.
- If changing look ahead, it gets the new value from the slider.
- It then calls the corresponding method on the
log
instance:log.seek(timestamp)
orlog.setLookAhead(value)
.
- Propagation: Calling
log.seek()
orlog.setLookAhead()
updates the loader's state, which triggers the connected components (including itself and the LogViewer) to re-render with the new state.
Here's a simplified interaction flow:
sequenceDiagram
participant User as User Interaction
participant PlaybackControl as PlaybackControl Component
participant Loader as XVIZ Loader Instance
participant LogViewer as LogViewer Component
User->>PlaybackControl: Drag scrubber / Click Play
PlaybackControl->>Loader: seek(newTimestamp) / setLookAhead(value)
Loader->>Loader: Update internal state<br>Process data for new time
Loader->>PlaybackControl: Notify update (via connect mechanism)
Loader->>LogViewer: Notify update (via connect mechanism)
PlaybackControl->>PlaybackControl: Update UI (timeline, time label)
LogViewer->>LogViewer: Get current frame<br>Re-render 3D scene
Let's look at a tiny piece of the actual PlaybackControl
component source code (modules/core/src/components/playback-control/index.js
). Notice how it retrieves state from props
(which are populated by the connectToLog
wrapper from the log
instance) and how it calls log.seek()
and log.setLookAhead()
.
// From modules/core/src/components/playback-control/index.js (simplified)
// This 'getLogState' function is used by the connectToLog wrapper
// to map loader state to component props.
const getLogState = log => ({
timestamp: log.getCurrentTime(),
lookAhead: log.getLookAhead(),
startTime: log.getLogStartTime(),
endTime: log.getLogEndTime(),
buffered: log.getBufferedTimeRanges()
});
// ... Inside the PlaybackControl component class ...
_onSeek = timestamp => {
const {log, onSeek} = this.props;
// If user provides onSeek callback and it returns true, stop here
if (!onSeek(timestamp) && log) {
// Otherwise, call seek on the log instance
log.seek(timestamp);
}
};
_onLookAheadChange = lookAhead => {
const {log, onLookAheadChange} = this.props;
// If user provides onLookAheadChange callback and it returns true, stop here
if (!onLookAheadChange(lookAhead) && log) {
// Otherwise, call setLookAhead on the log instance
log.setLookAhead(lookAhead);
}
};
// ... In the render method, it passes these props to the base PlaybackControl component
// which handles the actual rendering and user input detection ...
// return (
// <DualPlaybackControl
// ...
// currentTime={timestamp} // Data from log state
// lookAhead={lookAhead} // Data from log state
// startTime={startTime} // Data from log state
// endTime={endTime} // Data from log state
// bufferRange={bufferRange} // Processed buffered data
// onSeek={this._onSeek} // Callback wires user action to log.seek
// onLookAheadChange={this._onLookAheadChange} // Callback wires user action to log.setLookAhead
// ...
// />
// );
PlaybackControl
component itself is quite minimal. It primarily gets the necessary time-related data from the log
prop and provides methods (_onSeek
, _onLookAheadChange
) that call the corresponding methods on the log
instance when user interaction occurs. The actual visual rendering and input handling are delegated to an internal component (DualPlaybackControl
from @streetscape.gl/monochrome
).
The use of connectToLog
(linking getLogState
to the component) ensures that whenever the loader's state changes (e.g., currentTime
is updated by an external seek or internal animation), the PlaybackControl
component's props are updated, triggering a re-render to reflect the new state.
Conclusion#
The PlaybackControl component provides essential media player functionality for navigating through your XVIZ log data. By connecting to the same XVIZ Loader Interface instance as the LogViewer, it allows users to control the playback time, which in turn causes the LogViewer to update its 3D display. This creates an interactive experience for exploring the log data over time.
With the ability to load data (Chapter 1), visualize it in 3D (Chapter 2), and control the time ([Chapter 3: PlaybackControl]), you have the fundamental building blocks for a streetscape.gl
application.
In the next chapter, we'll explore the XVIZPanel component, which displays rich UI elements like metrics, plots, and tables defined directly within your XVIZ data, providing context and detailed information alongside the 3D view.
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/docs/api-reference/playback-control.md), 2(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/docs/get-started/starter-kit.md), 3(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/playback-control/dual-playback-control.js), 4(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/playback-control/index.js), 5(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/index.js)