IFrames and Preloading, Oh My!
November 12, 2014
Okay, so here’s our problem. Wistia’s lovely users can put as many videos as they want on a page. Often, these videos are hidden in tabs or launched in pop-ups, but they all exist on the page when it loads. On the one hand, we want to make videos play as fast as possible, which means using some form of preloading before play. On the other hand, we want to minimize unnecessary bandwidth usage for our customers. Above all else, the videos should just work.
To sum that up, we want to use preload="metadata" on our HTML5 videos where it makes sense. But Google Chrome has a variety of poorly documented rules that complicate this:
- If you have two or more
<video>elements on your page, and they share the same src, you can only play one at a time. First to play wins — the others will just poll until the stream is closed.
- If you have more than 7 concurrent streams (in my tests as of 11/6/2014), some of those streams will get stuck in a “pending” state. When this happens, the pending stream will not play until another stream on the page is closed.
Both of these cases manifest as a persistent “Loading…” message that never goes away. That’s extremely frustrating for visitors trying to view your video.
The “first to play wins” problem is not a huge issue. Normally, you don’t want the same video to appear multiple times on your page and play at the same time. It demands a little bit of cleanup to avoid orphaning a stream — and thereby blocking a new one with the same
src — but doing that is fairly straightforward. For us, it means finding Wistia videos that aren’t actually in the DOM, setting the video
src to a blank string, calling load(), and finally destroying the element. Since we keep track of Wistia embeds on the page, that’s a tractable solution.
The less straightforward problem is #2, where we try to preload a bunch of videos simultaneously. Our options here are:
- Try to preload everything and, if buffering hangs on one, close that stream.
- Make our videos aware of other videos on the page so that they can coordinate and stay below the concurrency limit.
With the first option, if that works, that’s pretty cool. But in my tests, it was very difficult to free up streams once they’d been preloaded. Moreover, it has an issue with headroom. That is, yes, all the videos that can preload will preload. However, if you try to play any new streams, we’re already maxed out on concurrency, so they will encounter the original issue again and load forever. One more problem: it ignores the possibility of any non-Wistia audio/video streams on the page, which is presumptuous at best.
Thinking about the second option, it’s actually kind of trivial to implement if you’re setting up all your
<video> elements in the same HTML document. Just run
$("video[preload!=none]").length > 3, right? And if that’s true, don’t preload any new videos.
But our problem at Wistia, and with many other video hosts, is that our most common embed type is an iframe, which precludes us from easily querying the DOM for other videos. For those of you not familiar with iframes, they are like isolated little documents on your page. They cannot easily tell what’s on the parent page, or what’s in any other frames on that page.
If we want to play by cross-origin rules and avoid all kinds of nasty warnings in the console (we do), we have only one tool at our disposal:
postMessage. If we can use
postMessage so that all the Wistia iframes on the page know about all the other Wistia frames, then we can intelligently set the preload attribute.
Here’s what we did. When an iframe is loaded …
- It announces itself by generating a
postMessageto send it to all the other iframes on the parent page.
- It tells each already-existing iframe to announce itself to the new iframe.
- Existing iframes post back to the new iframe with their
guidin a message like
As long as each iframe is listening for that event, it can keep a list of iframes it has heard from. Using that list, it can determine whether or not it should preload. For example, our logic says not to preload if there are more than two iframes detected on the page. There is a race condition here: because iframes load asynchronously, it’s possible that two exist, don’t see each other in time, and both turn on preloading. A third appears just slightly later, sees the other two, and doesn’t preload. We find this behavior to be acceptable, even desirable since we’d like the videos that load closest to the top of the DOM to be the ones that preload.
One obvious downside to this approach is that we always send N^2 (i.e. N squared) messages, where N is the number of iframes on the page. Fortunately, people usually don’t put 100s of iframes on their pages. But for those edge cases, we can simply cap the number of iframes that we look at, and if we detect a lot, don’t preload. Better safe than sorry! I believe there are ways to get around this N^2 issue, but I’ll leave that as an exercise for the future.
TLDR: When all you’ve got is a hammer, turn everything into a nail.
postMessage is the hammer of iframes.