Issue when using Bokeh slider to control html video

Use case:

  • Load a video in html
  • Use bokeh slider to control the frame in the video

What I have tried (minimum reproducible example):

from bokeh.io import output_file, show

from bokeh.layouts import column

from bokeh.models import Div, Slider, CustomJS

# Define the HTML code for the video tag

video_html = """

<video id="video_player" controls preload="metadata">

<source src="video.mp4" type="video/mp4">

Your browser does not support the video tag.

</video>

"""

# Create a Bokeh Div element to embed the video HTML

video_div = Div(text=video_html)

# Create a slider to control video playback

slider = Slider(start=0, end=100, value=0, step=1, title="Video Time (%)")

# Define a JavaScript callback to sync slider with video playback

callback = CustomJS(args=dict(slider=slider), code="""

var video = document.getElementById('video_player');

video.currentTime = slider.value * video.duration / 100;

""")

# Attach the JavaScript callback to the slider's 'value' property

slider.js_on_change('value', callback)

# Create a Bokeh layout with the video Div and slider

layout = column(video_div, slider)

# Show the Bokeh plot

output_file("video_player.html")

show(layout)

Error I see in the javascript console is the following:

TypeError: null is not an object (evaluating 'video.duration')

It seems that I cannot get the video object inside the callback.

Help appreciated!

1 Like

The problem is that Bokeh puts the content of your div (and actually of the column) inside a shadow-root, which makes its content inaccessible from the rest of the document.
Here is the structure of your document:

As a consequence, document.getElementById('video_player') doesn’t find the video and returns null, leading to the error message you observed.

This problem was also discussed here, unfortunately without solution:

Since Bokeh was not designed to display videos, there is no built-in video class allowing to access a video or its properties.

A solution could be to create a dashboard with another tool than Bokeh (e.g. Flask), embedding the video outside a shadow-root, then you would be able to access the video and its properties.

Or using a html template for a Bokeh application, see:

In this case, create a directory video-app containing the following:

video-app
   |
   + - main.py
   + - templates
        + - index.html
   + - static
        + - video.mp4

In main.py, write the following:

from bokeh.io import curdoc
from bokeh.models import Slider, CustomJS

# Create a slider to control video playback
slider = Slider(start=0, end=100, value=0, step=1, title="Video Time (%)")

# Define a JavaScript callback to sync slider with video playback
callback = CustomJS(args=dict(slider=slider), code="""
const video = document.getElementById("video_player")
let new_time = slider.value * video.duration / 100
video.currentTime = new_time
console.log("Video currentTime set to " + new_time.toFixed(2) + " s")
""")

# Attach the JavaScript callback to the slider's 'value' property
slider.js_on_change('value', callback)
curdoc().add_root(slider)

In index.html, write the following:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        {{ bokeh_css }}
        {{ bokeh_js }}
    </head>
    <body>
        <video id="video_player" controls preload="metadata">
            <source src="Video-app/static/video.mp4" type="video/mp4">
        </video>
        {{ plot_div|indent(8) }}
        {{ plot_script|indent(8) }}
    </body>
</html>

This inserts the video in the html file in an open way (so not in a shadow-root), making it accessible with document.getElementById.
Then run the app with bokeh serve --show video-app.
This works for me.
A drawback of this method is that a Bokeh server runs in the background although it is actually not needed since everything runs as Javascript.
Stopping the server, the app still works but there are error messages in the console telling that no communication with Bokeh is possible.

I can also propose following “dirty” method:
Create a Python file containing the following:

from bokeh.io import output_file, save
from bokeh.models import Slider, CustomJS

# Create a slider to control video playback
slider = Slider(start=0, end=100, value=0, step=1, title="Video Time (%)")

# Define a JavaScript callback to sync slider with video playback
callback = CustomJS(args=dict(slider=slider), code="""
var video = document.getElementById('video_player');
video.currentTime = slider.value * video.duration / 100;
""")

# Attach the JavaScript callback to the slider's 'value' property
slider.js_on_change('value', callback)

output_file("video_player.html")

save(slider)

In the created html-file, insert following lines just after <body>:

<video id="video_player" controls preload="metadata">
    <source src="Video-app/static/video.mp4" type="video/mp4">
</video>

Then open the html file, it works as standalone.

1 Like