Javascript-Python communication with hls.js via streamlit_javascript

Hello,

I am trying to follow in the steps of some others who have managed to achieve Javascript-Python communication, but I am getting stuck.

https://github.com/cpremoshis/livestream-dvr/tree/main

My goal is to get hls.js to send the name of the currently playing segment from a HLS/M3U8 stream to Python/Streamlit so that I can work with that segment information. My goal is to be able to use that information to set “In” and “Out” points to clip the video segments in between. I have managed to get hls.js to post that information to the browser console log while also displaying the info within the player itself:

hls.on(Hls.Events.FRAG_CHANGED, function(event, data) {{
    var segmentInfo = document.getElementById('segment-info');
    segmentInfo.textContent = 'Current Segment: ' + data.frag.url;
    var message = 'Playing Segment: ' + data.frag.url + ' --- ' + data.frag.sn;
    console.log('Sending message:', message);  // Debug log
    window.parent.postMessage(message, "*");
}});


But either Python/Streamlit is not receiving that information or I am just not handling it properly so that I can see it.

st_javascript("""window.addEventListener("message", (event) => {
        if (event.origin === window.location.origin) {
            console.log("Received message:", event.data);
            window.parent.postMessage(event.data, "*");
        }
    });
""")

message = st_javascript("""await new Promise(resolve => {
        window.addEventListener('message', event => {
            if (event.origin === window.location.origin) {
                resolve(event.data);
            }
        });
    });
""")

For now, my app is deployed locally with Python 3.12.4 and Streamlit 1.36.0.

Any idea where I may be going wrong?

Update: There is a bug with streamlit-javascript where the code must start on the first line. JavaScript code must start on first line of string · Issue #12 · thunderbug1/streamlit-javascript · GitHub

I’ve updated based on that and I am now getting the “Received message:…” console log, but the message is blank.

Alright. I’ve now tried creating a simple custom component based on these instructions: Code snippet: create Components without any frontend tooling (no React, Babel, Webpack, etc)

My index.html is below, and mostly functioning. It is posting two messages to the console log for on each segment change, one from within “sendDataToPython” and one from within “hls.on(Hls.Events.FRAG_CHANGED…”

But I am still not receiving a value with Streamlit itself.

<html>
<head>
    <style>
        #video-container {
            position: relative;
            width: 100vw;
            height: 100vh;
        }
        #segment-info {
            position: absolute;
            top: 10px;
            left: 10px;
            color: #fff;
            padding: 5px;
            font-family: Arial, sans-serif;
            font-size: 14px;
            border-radius: 5px;
            background-color: rgba(0, 0, 0, 0.5);
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>
<body>
    <div id="video-container">
        <video id="video" controls autoplay style="width:100vw; height:100vh; object-fit: contain; margin:auto"></video>
        <div id="segment-info"></div>
    </div>
    <script>

        function sendMessageToStreamlitClient(type, data) {
                var outData = Object.assign({
                    isStreamlitMessage: true,
                    type: type,
                }, data);
                window.parent.postMessage(outData, "*");
                }

        function init() {
            sendMessageToStreamlitClient("streamlit:componentReady", {apiVersion: 1});
            }

        function setFrameHeight(height) {
            sendMessageToStreamlitClient("streamlit:setFrameHeight", {height: height});
            }

        function sendDataToPython(data) {
            console.log("Sending data to Python:", data); // Debugging log
            sendMessageToStreamlitClient("streamlit:setComponentValue", data);
            }

        //var myInput = document.getElementById("myinput");

        //function onDataFromPython(event) {
        //    if (event.data.type !== "streamlit:render") return;
        //    myInput.value = event.data.args.my_input_value;  // Access values sent from Python here!
        //}

        //myInput.addEventListener("change", function() {
        //    sendDataToPython({
        //    value: myInput.value,
        //    dataType: "json",
        //    });
        //})

        //window.addEventListener("message", onDataFromPython);
        init();

        // Hack to autoset the iframe height.
        window.addEventListener("load", function() {
            window.setTimeout(function() {
            setFrameHeight(document.documentElement.clientHeight)
            }, 0);
        });

        setFrameHeight(535);

        document.addEventListener('DOMContentLoaded', function () {
            init();
            var video = document.getElementById('video');
            var videoSrc = 'https://globalbroadcasthub.net/stream/hls/reuters/index.m3u8';

            if (Hls.isSupported()) {
                var hls = new Hls();
                hls.loadSource(videoSrc);
                hls.attachMedia(video);
                hls.on(Hls.Events.MANIFEST_PARSED, function() {
                    video.play();
                });
            
                hls.on(Hls.Events.FRAG_CHANGED, function(event, data) {
                    var segmentInfo = document.getElementById('segment-info');
                    segmentInfo.textContent = 'Current Segment: ' + data.frag.url;
                    var message = 'Playing Segment: ' + data.frag.url;
                    console.log('Sending message:', message);
                    sendDataToPython(data.frag.url);
                });
            } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
                video.src = videoSrc;
                video.addEventListener('loadedmetadata', function() {
                    video.play();
                });
            }
        });

        let retryCount = 0;
        function retrySendingData(data) {
            if (retryCount < 5) {
                console.log("Retrying to send data:", data); // Debugging log
                sendDataToPython(data);
                retryCount++;
                setTimeout(() => retrySendingData(data), 1000);
            }
        }

    </script>
</body>
</html>

I’ve tried implementing in my Streamlit app with these two lines of codes, after first having created an init.py, which successfully render the video player which is posting the console log messages:

player_message = my_component()
st.write("Received:", player_message)

But again, Streamlit is not receiving the data.

Hi,

I have published this a few month ago: A more simple way to define components

It seems to me that you can reuse the code to solve your problem. You need to create a component that includes your javascript code. The example shows how to pass parameters from Python to Javascript and how to return a result from javascript.

Regards