Syncing a bokeh plot to a video

The Python library bokeh is great for plotting all kinds of data in the browser. Bokeh includes a serve command, which can host a document, having a per-user state in the associated Python code. When creating a visualisation, you can have callback functions in both Python and JavaScript code. I used this here to sync an HTML video-element to a bokeh line plot.

Accelerometer data synced to a dashcam video:

The HTML video element

<video src="bokeh-video-sync/static/20190912_041033_EF.mp4"
       height="530"
       id="frontcamera"></video>

The JavaScript callback:

/* Select the video with the id frontcamera */
var v = document.getElementById("frontcamera");

/* If the time of the video element updates, run the callback */
v.addEventListener("timeupdate", function () {
    /* The selector below assumes there are no other input fields in your
       code */
    inputs = document.getElementsByTagName('input');
    for (index = 0; index < inputs.length; ++index) {
        /* Update the input field with the new time of the video */
        inputs[index].value = v.currentTime;
        /* Trigger a change of the input field, to call the Python code */
        var event = new Event('change', {bubbles: true});
        inputs[index].dispatchEvent(event);
    }
}, true);

The Python code, with the update callback function:

import pandas

from bokeh.layouts import row
from bokeh.models import ColumnDataSource, TextInput
from bokeh.plotting import curdoc, figure


def update(_, old, new):
    """The callback function we want to invoke if the time in the video
       advances."""
    # Our data source is in milliseconds, but the callback receives seconds,
    # therefore we multiply the input by 1000
    new, old = float(new) * 1000, float(old) * 1000
    subset = df[(df['ms_since_start'] > old) & (df['ms_since_start'] < new)]
    data.stream({
        'ms': list(subset['ms_since_start']),
        'z': list(subset['z_int']),
        'x': list(subset['x_int']),
        'y': list(subset['y_int']),
    })

# Read in the data using pandas
df = pandas.read_csv('./20190912_041033_EF.acc.csv')

# Create a new bokeh figure
p = figure(plot_width=1900, plot_height=400)

# Define the columns to plot later
data = ColumnDataSource({
    'ms': [],
    'z': [],
    'x': [],
    'y': [],
})

# Plot three lines, that listen to changes in the ColumnDataSource
p.line('ms', 'z', source=data, color='red')
p.line('ms', 'x', source=data, color='white')
p.line('ms', 'y', source=data, color='lightgreen')

# Add a TextInput() that we use to pass on the current time in the video
# This is hacky, and could potentially be done in a nicer way
current_time = TextInput()
 
# If the text field current_time changes, invoke the update callback function 
current_time.on_change('value', update)

# Add all elements to the bokeh document
curdoc().add_root(row(p))
curdoc().add_root(row(current_time))

Although I like this first try at syncing plots, there is still room for improvement. It would be nicer to have the video element call the Python function directly, instead of through an input field. This could be achieved by implementing the video element in bokeh.models. Currently, the line will not disappear when you rewind the video, or start it a second time. All these things can be achieved using the HTML5 media events. Maybe I’ll make a proper media player for bokeh one day…