How to Detect Seek Events with the Youtube Player API
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 callsonStateChange
fires when the player’s state changesonPlaybackQualityChange
fires when the playback quality changesonPlaybackRateChange
fires when the playback rate changesonError
fires when a player error occursonApiChange
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
: unstarted0
: ended1
: playing2
: paused3
: buffering5
: 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.
- Mouse seeks trigger
pause
,buffer
,play
events (2
,3
,1
), in that order - Arrow key seeks trigger
buffer
andplay
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);
}
}
};