My Sad Existence - Building a Video Player with MSE
Part IV: Adding Seeking
So far, we've built a simple MSE player that can play both video and audio, but notice that if you attempt to seek to the middle of the stream, it will buffer indefinitely. This is actually a pretty easy bug to fix!
Give it a go on your own, but if you get stuck, I'll show you how to fix it in the next section.
The player is currently set up to always fetch the next contiguous segment in the stream. This is fine if you want to play back the stream from start to finish, but it's not so great if you want to seek around.
Simple Player with Seeking
The additional code here is actually pretty minimal. When you perform a seek on a video element, the 'timeupdate' event is fired with the new desired playback time.
If we find that the desired time is already in the buffered range, then nothing needs to be done and playback will continue uninterrupted. However, if the desired time is not in the buffered range, we need to:
- Find the segment that contains the desired time
- Fetch that segment
- Append it to the
SourceBuffer
We've already written a fetchAndAppendSegment method that does exact this, so all we need to do is ensure we call it with the new time if we've detected that the desired time is not in the buffered range:
handleTimeUpdate() {
const currentTime = this.config.videoElement.currentTime;
for (let i = 0; i < this.sourceBuffer.buffered.length; i += 1) {
const isCurrentTimeInBufferRange =
this.sourceBuffer.buffered.start(i) <= currentTime &&
this.sourceBuffer.buffered.end(i) >= currentTime;
if (isCurrentTimeInBufferRange) {
const remainingBuffer =
this.sourceBuffer.buffered.end(i) - currentTime;
console.log(`Buffer Remaining: ${remainingBuffer}`);
if (remainingBuffer < this.config.segmentLengthS) {
const nextSegmentTime =
this.sourceBuffer.buffered.end(i) + 1;
this._fetchAndAppendSegment(nextSegmentTime);
}
return;
}
}
this._fetchAndAppendSegment(currentTime);
}
Buffered Ranges
The only additional complexity here is that the seek will create an additional buffered range (this.sourceBuffer.buffered). This means that we need to check if the desired time is in any of the buffered ranges, not just the first one.
For example, if you have started playback of the video from the beginning, and you're currently 20 seconds into the video, your buffered range will look like [ 0, 20 ].
If you then seek to position 50 and playback continues for 10 seconds, your buffered ranges will look like this: [ 0, 20 ], [ 50, 60 ].
These discontinuous ranges are why we need to check all buffered ranges to see if the desired time is in any of them.
These discontinuous ranges can be reconnected. If you were to seek back to position 20 and let the video play to position 50, you would end up again with a single buffered range [ 0, 60 ].
Where are we?
We've now built a player that can play video and audio, seek around, and buffer the next segment as needed.
You might be a little upset that many of the questions you had at the beginning of this series are still unanswered. You might be wondering:
Nick, you talk so much about adaptive streaming, but these code examples are all about playing a single resolution. This doesn't seem very adaptive. Did you lie to me?
I haven't lied, but I've left out quite a large piece of the puzzle when it comes to adaptive streaming: manifests. We will cover that next time!
Try making the current player adaptive by adding a bandwidth parameter to the fetchAndAppendSegment method. The available bandwidths for the example video are: 350000, 1000000, and 3000000 bps. You will notice that the bandwidth is part of the URL for the segment, so you can replace that with your desired bandwidth in order to start fetching segments. The other parameter in the URL is the Representation ID,
which you can just consider an arbitrary index for now: 0, 1, 2 correlating to the bitrates above.
One simple way to make this adaptive would be to calculate how long it takes on average to download a segment. If the download time is longer than the segment duration, then you should fetch the next segment at a lower bitrate. If the download time is shorter than the segment duration, then you should fetch the next segment at a higher bitrate.