Video seeking functionality not working as expected

What are you trying to do?

In my app, I have created an Embed HTML widget and added HTML code to it to create a Video.js video player. I have also added a table widget whose data is a list of subtitles for the video line by line. In table, I have two columns: startTime and text. On the table, I have added an on row click action that runs some javascript code, the purpose of which is to seek to the startTime of the row in the video. I am using window.postMessage for this workflow, and it successfully sends the message to my message event listener in the HTML code, but the player.currentTime(time) part does not seem to be triggering. The seeking action never happens.

What have you tried so far? (please link relevant docs and other forum posts)

I have tried different variations of the HTML code and the javascript code, to include exposing the player as window.videoPlayer and trying to call the seeking action from the javascript code directly.

Relevant links:

  • My project: https://video-player.plasmic.run/
    • I have hardcoded the video/subtitle URLs I am using to test with so you can just click Submit to load everything. Press play on the video once it is loaded and then click any row in the subtitle table. It should technically seek to that location in the video, but it does not.
  • My code: Plasmic Video Player Code · GitHub

Hello @torian_crane, welcome to the forum!

It seems that the “DOMContentLoaded” event is never fired, possibly because its already fired before the event listener is added.

Also, I would recommend creating a code component for your video player. Check out our docs for more info: Code components API reference | Learn Plasmic

Hi @sarah_ahmed,

Thank you for your response!

Regarding the code component creation, I don’t fully understand how to do that within the Plasmic Studio browser. When I try to create a custom component, there is no option to provide custom code for the component. It just defaults to something like a vertical stack widget. The documentation is not clear on how to do this.

I get the sense that I need a local react project to be able to create a code component. Is my understanding correct?

Is there any way at all that I can get my Embed HTML code to work properly without having to go the code component route (especially if I have to do anything external to the Plasmic Studio browser)?

Also to follow up on this, the console.log() statements in my HTML are firing successfully, so I’m not sure if the DOM thing is the issue.

Row clicked, time: 00:59.000
VM391:35 Converted time in seconds: 59
VM330:15 Received message: Object
VM330:19 Seeking to time: 59

At one point I added a console.log(“Current time”, player.currentTime()) and that also fired successfully, but the player.currentTime(time) does not actually fire, or if it does, it does not actually do the seek.

Following up just in case anyone else needs it, but I was able to solve it with the following:

<!DOCTYPE html>
<html>
<head>
  <link href="https://vjs.zencdn.net/8.16.1/video-js.css" rel="stylesheet">
  
  <style>
    .video-js {
      width: 100%;
      height: 0;
      padding-top: 56.25%;
      position: relative;
    }
    
    .video-js .vjs-tech {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  </style>
</head>

<body>
  <video
    id="my-video"
    class="video-js"
    controls
    preload="auto"
    data-setup="{}"
  >
    <source src="${$state.videoUrl}" type="video/mp4" />
    <track 
      kind="captions" 
      src="${$state.subtitleUrl}"
      srclang="de" 
      label="German" 
      default
    />
  </video>

  <script src="https://vjs.zencdn.net/8.16.1/video.min.js"></script>

  <script>
    (function() {
      var player = null;
      var isPlayerReady = false;

      function setupPlayer() {
        if (player) {
          player.dispose();
          player = null;
        }

        var videoElement = document.getElementById('my-video');
        if (!videoElement) {
          console.error('Video element not found');
          return;
        }

        try {
          player = videojs('my-video', {}, function() {
            console.log('Player initialized');
            
            this.one('loadedmetadata', function() {
              console.log('Metadata loaded, duration:', this.duration());
              isPlayerReady = true;
            });
          });

          // Handle errors more gracefully
          player.on('error', function(e) {
            var error = player.error();
            if (error) {
              console.error('Video error code:', error.code);
              console.error('Video error message:', error.message);
            }
            // Don't set isPlayerReady to false on all errors
            if (error && error.code !== 2) { // MEDIA_ERR_NETWORK
              isPlayerReady = false;
            }
          });

        } catch (error) {
          console.error('Error setting up player:', error);
        }
      }

      function seekToTime(time) {
        if (!player) {
          console.error('Player not initialized');
          return;
        }

        console.log('Attempting to seek. Player state:', {
          ready: isPlayerReady,
          duration: player.duration(),
          currentSrc: player.currentSrc(),
          paused: player.paused()
        });

        try {
          // Store current play state
          var wasPlaying = !player.paused();

          if (!isPlayerReady) {
            console.log('Waiting for metadata before seeking...');
            player.one('loadedmetadata', function() {
              console.log('Metadata now loaded, performing seek');
              performSeek();
            });
            player.load();
          } else {
            performSeek();
          }

          function performSeek() {
            var duration = player.duration();
            time = Math.min(time, duration);
            
            console.log('Performing seek to', time, 'of', duration);
            
            // Use player.one() to ensure we only handle this seek event
            player.one('seeked', function() {
              console.log('Seek completed to:', player.currentTime());
              // Restore play state
              if (wasPlaying) {
                player.play().catch(function(error) {
                  console.error('Error resuming playback:', error);
                });
              }
            });

            player.currentTime(time);
          }
        } catch (error) {
          console.error('Error during seek:', error);
        }
      }

      window.addEventListener('message', function(event) {
        if (!event.data) return;

        console.log('Received message:', event.data);

        if (event.data.type === 'seekTo') {
          var time = parseFloat(event.data.time);
          if (!isNaN(time)) {
            seekToTime(time);
          } else {
            console.error('Invalid seek time:', event.data.time);
          }
        }
      });

      // Setup observer to watch for source changes
      var observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
          if (mutation.type === 'attributes' && 
              mutation.attributeName === 'src' && 
              mutation.target.nodeName === 'SOURCE') {
            console.log('Source changed to:', mutation.target.src);
            isPlayerReady = false; // Reset ready state
            setupPlayer();
          }
        });
      });

      setTimeout(function() {
        var sourceElement = document.querySelector('#my-video source');
        if (sourceElement) {
          observer.observe(sourceElement, {
            attributes: true
          });
          console.log('Started observing source element');
        }
        setupPlayer();
      }, 100);
    })();
  </script>
</body>
</html>
1 Like