How to Detect Seek Events with the Youtube Player API

Published Jun 3, 2020

The Youtube Player API provides lots of events to notify changes to an embedded player.

For instance, the API fires the followings events for the player:

  • onReady fires when a player finishes loading and can begin receiving API calls
  • onStateChange fires when the player’s state changes
  • onPlaybackQualityChange fires when the playback quality changes
  • onPlaybackRateChange fires when the playback rate changes
  • onError fires when a player error occurs
  • onApiChange fires when the player has loaded or unloaded a module with exposed API methods

We want to focus on the onStateChange event. From the documentation, we can see that there is a data property in the associated event object which will hold one of the values below, depending on the state of the player.

  • -1: unstarted
  • 0: ended
  • 1: playing
  • 2: paused
  • 3: buffering
  • 5: video cued

How to Detect Play/Pause Events

From the native player API, we can create simple handlePlay(), handlePause(), and handleBuffer() event handlers.

Heads up. I’ll be working in React.

const handleStateChange = event => {
  const playerState = event.data;
  switch (playerState) {
    case 1: // playing
      handlePlay();
      break;
    case 2: // paused
      handlePause();
      break;
    case 3: // buffering
      handleBuffer();
      break;
  }
};
const handlePlay = () => console.log("Play!");
const handlePause = () => console.log("Pause!");
const handleBuffer = () => console.log("Buffer!");

As long as we properly trigger handleStateChange() when an onStateChange event fires, we should be perfectly handling play and pause events.

But what about seek events? When a user jumps or skips to a new position in the video timeline?

How to Detect Seek Events

If you run the code above with a properly instantiated YouTube player, you’ll notice that a seek event triggers pauses, plays, and buffers all in one seek event.

There are two kinds of seeks which trigger different events: mouse seeks and arrow key seeks. Mouse seek events occur when you click on a separate time in the timeline. Arrow key seeks occur when you use the left and right arrow keys to change the time in the timeline.

  1. Mouse seeks trigger pause, buffer, play events (2, 3, 1), in that order
  2. Arrow key seeks trigger buffer and play events (3, 1), in that order

We’re going to redirect all the event handlers into one single function, instead of using the switch statement from above, which will then determine if the event was a play, pause, or seek.

We’ll also add an event handler for seek events.

const handleStateChange = event => handleEvent(event.data);
const handlePlay = () => console.log("Play!");
const handlePause = () => console.log("Pause!");
const handleBuffer = () => console.log("Buffer!");
const handleSeek = () => console.log("Seek!");

We’ll create a new state called sequence that will record the state changes since the last event.

We also need a method for checking if this sequence contains an event sequence that would trigger a seek event (either a [2, 3, 1] or a [3, 1]).

We’ll use isSubArrayEnd() for this purpose, which will check if array B is a subarray of A and lies at the end of A.

const isSubArrayEnd = (A, B) => {
  if (A.length < B.length)
    return false;
  let i = 0;
  while (i < B.length) {
    if (A[A.length - i - 1] !== B[B.length - i - 1]) 
      return false;
    i++;
  }
  return true;
};

Let’s handle the seek event logic.

const [sequence, setSequence] = useState([]);
const [timer, setTimer] = useState(null);

const handleEvent = type => {
  // Update sequence with current state change event
  setSequence([...sequence, type]);
  if (type == 1 && isSubArrayEnd(sequence, [2, 3])) {
    handleSeek(); // Mouse seek
    setSequence([]); // Reset event sequence
  } else if (type === 1 && isSubArrayEnd(sequence, [3])) {
    handleSeek(); // Arrow keys seek
    setSequence([]); // Reset event sequence
  } else {
    clearTimeout(timer); // Cancel previous event
    if (type !== 3) { // If we're not buffering,
      let timeout = setTimeout(function () { // Start timer
        if (type === 1) handlePlay();
        else if (type === 2) handlePause();
        setSequence([]); // Reset event sequence
      }, 250);
      setTimer(timeout);
    }
  }
};

We know that a seek event always ends with a play event. In the first two if statements, we’re checking that the current event is a play event and that the previous events follow either the [2, 3, 1] sequence for mouse seeks or [3, 1] sequence for arrow key seeks.

We’re also using a timer state variable to determine how long we should wait before we know a play or pause event is just a play or pause event, and not a seek event.

Suppose we trigger a pause event. This could be an actual pause, or just the beginning of a seek. We know a seek event should trigger a buffer (3) then a play (1) after the pause (2). We use this timer to allow us to wait and check whether the event is truly a seek event before deciding too quickly.

If the previous event was a pause, and we realize we were actually at the beginning of a seek, then clearTimeout() will cancel the previous timer, preventing the handlePause() from the previous iteration from firing.

One Last Note

If we don’t care about differentiating between mouse and arrow key seek events, then we can remove the first if statement.

If you ran the code above, you might’ve also noticed playing the video directly after loading it will trigger a seek event, so we need to check that we didn’t just come from the UNSTARTED or -1 state.

const [sequence, setSequence] = useState([]);
const [timer, setTimer] = useState(null);

const handleEvent = type => {
  // Update sequence with current state change event
  setSequence([...sequence, type]);
  if (type === 1 && isSubArrayEnd(sequence, [3]) && !sequence.includes(-1)) {
    handleSeek(); // Arrow keys seek
    setSequence([]); // Reset event sequence
  } else {
    clearTimeout(timer); // Cancel previous event
    if (type !== 3) { // If we're not buffering,
      let timeout = setTimeout(function () { // Start timer
        if (type === 1) handlePlay();
        else if (type === 2) handlePause();
        setSequence([]); // Reset event sequence
      }, 250);
      setTimer(timeout);
    }
  }
};

Solution

Let’s put this all together. The code below is what I use to track play, pause, and seek events.

const [sequence, setSequence] = useState([]);
const [timer, setTimer] = useState(null);

const handleStateChange = event => handleEvent(event.data);
const handlePlay = () => console.log("Play!");
const handlePause = () => console.log("Pause!");
const handleBuffer = () => console.log("Buffer!");
const handleSeek = () => console.log("Seek!");

const isSubArrayEnd = (A, B) => {
  if (A.length < B.length)
    return false;
  let i = 0;
  while (i < B.length) {
    if (A[A.length - i - 1] !== B[B.length - i - 1]) 
      return false;
    i++;
  }
  return true;
};
const handleEvent = type => {
  // Update sequence with current state change event
  setSequence([...sequence, type]);
  if (type === 1 && isSubArrayEnd(sequence, [3]) && !sequence.includes(-1)) {
    handleSeek(); // Arrow keys seek
    setSequence([]); // Reset event sequence
  } else {
    clearTimeout(timer); // Cancel previous event
    if (type !== 3) { // If we're not buffering,
      let timeout = setTimeout(function () { // Start timer
        if (type === 1) handlePlay();
        else if (type === 2) handlePause();
        setSequence([]); // Reset event sequence
      }, 250);
      setTimer(timeout);
    }
  }
};

More JS Articles