Fixing html video playback on chrome
Mon Jan 23 2023
Google Chrome does not let users seek, rewind, or fast forward HTML videos. Here
's how to fix that!
--------------------------------------------------------------------------------
Fixing html video playback on chrome
====================================
Published Jan 23, 2023 - 10 min read
/----------- Table of contents ----------\
| Table of contents |
| * Fixing html video playback on chrome |
| * Responding to Chrome |
| * ETags |
| * Caching |
| * Progressive Web Apps |
| * Conclusion |
\----------------------------------------/
I do not often include videos on my blog, but when I do, I test to make sure
they work. And to my dismay, these HTML5 videos could start, they could pause,
but any time I attempted to seek the video playback position, it would fail.
When I wrote Device OAuth Flow is Phishable [L1], I included a video like this
below.
[I1: Video: Apple TV login with remote]
The video is so dull that the only way to appreciate it was to intentionally
move the playback slider from start to finish to see how terrible it is to enter
an email address and password with a tv remote.
You don't have to play this video. Really.
My videos aren't big, the above is merely 540KB. Some websites literally have
2MB jpegs because marketing can't figure out how to resize what they upload. But
that doesn't matter to the browser. It keeps in memory only what it has to at
that moment. A video can be small like mine, or it could be a few gigabytes.
No matter how big or small the video is, browsers like Google Chrome take a very
conservative approach. It does not download the full video. It does not buffer
the full video. It often does not cache parts of the video. Google Chrome is
designed to take a slice of video data. Maybe a few seconds, usually under a
megabyte. Enough video time to start a new connection to resume playing the
video without interrupting the user.
Open the network tab and you'll see not just one request, but multiple when
playing this video.
[I2: A chrome network tab with three recorded requests for the same endpoint]
If you are on a slow connection and skip around the video several times, you
will see multiple range requests! Even on a slow connection, I can quickly skip
to a section of the video that has not been downloaded yet. That's why this
feature exists!
GET https://c.cdyn.dev/PFCc64ah - Ok @ 1/22/2023, 5:21:33 PM
(log) Got Range request: 0 - : {"range":{"offset":0}}
(log) Range result: 'bytes 0-540152/540153'
GET https://c.cdyn.dev/PFCc64ah - Ok @ 1/22/2023, 5:21:36 PM
(log) Got Range request: 491520 - : {"range":{"offset":491520}}
(log) Range result: 'bytes 491520-540152/540153'
GET https://c.cdyn.dev/PFCc64ah - Ok @ 1/22/2023, 5:21:39 PM
(log) Got Range request: 32768 - : {"range":{"offset":32768}}
(log) Range result: 'bytes 32768-540152/540153'
GET https://c.cdyn.dev/PFCc64ah - Ok @ 1/22/2023, 5:21:42 PM
(log) Got Range request: 262144 - : {"range":{"offset":262144}}
(log) Range result: 'bytes 262144-540152/540153'
Since I skipped around, the network tab shows only the partial amounts that were
downloaded before the request was cancelled mid-transmission. This is fine, and
intended behavior.
The first request knows that this endpoint is a video source. So there is an
additional request header that it sends: Range [L2], as it may download the
media in slices.
[I3: Chrome sends a range header on a request to a video source]
This header is the first part in Chrome supporting correct video playback.
/[cendyne: typing]-------------------------------------------------------------\
| If you use AWS Cloudfront, Akamai, big company CDNs and so on, or even |
| nginx, this problem is likely solved for you already. The rest of this |
| article is meant to educate why this is happening and how to solve it |
| yourself if you're in a situation like me. |
\------------------------------------------------------------------------------/
/[cendyne: vibrating]----------------------------------------------------------\
| I've seen this happen on mastodon servers too. Some do fine, but only if |
| their user content is served from another domain. |
\------------------------------------------------------------------------------/
Responding to Chrome
--------------------
The following information will only be helpful if you control the request and
response on the edge.
So chrome asks: May I have some range please? 🥺 👉👈
We have to then respond with new headers Content-Range [L3] and Accept-Ranges
[L4], which tells Chrome how much range there is to get and that of course we
handle range requests.
[I4: A request with a content-range header in the response]
Since we would serve all the content, the response status code is 200 OK. Some
may use 206 Partial Content since it is a range request, but that seems weird to
me since the request would not be partial by any means.
At this point, Chrome may intentionally slow down the download to match with how
fast the user is watching the video. Some servers will close connections that
stay alive for too long, Chrome has a workaround for this! Do not assume that
all 540153 bytes (in this example) have been served over the network!
It can start new connections when either the original slow request times out, or
the user skips ahead to content that is not cached or buffered.
In that case it will make a partial request. The range header looks like bytes=
- Notice that the offset is
greater than 0.
[I5: A partial request]
While the MDN docs for Range [L2] include a list of ranges, you do not need to
support that for video content on Chrome.
The correct move here is to serve a partial response with status code 206
Partial Content. If the starting offset is greater than zero, skip that far into
the file and then read until the ending offset (or the end of the file if
blank).
[I6: A partial response]
Key things to note here: we truly are sending a shorter output, see Content-
Length [L5] : 146937 which is less than 540153; second we still let the browser
know how much content is being served and how much there is to serve with
Content-Range [L3].
/[cendyne: kernel-panic]-------------------------------------------------------\
| Careful! You might think "It should be |
| bytes 393216-540153/540153, why is it |
| bytes 393216-540152/540153?" |
\------------------------------------------------------------------------------/
/[cendyne: teaching]-----------------------------------------------------------\
| This tricked me up too. The second number there is the ending position / |
| offset. There are 540153 bytes, the last byte's position is 540152. Remember |
| how we started at position 0 before? If you're serving the end of the file, |
| you send file_size - 1 as the end position. In other words, if you have a 4 |
| byte file, its content range could be bytes 0-3/4. |
\------------------------------------------------------------------------------/
ETags
^^^^^
There's also a header called If-Range [L6], where the user agent can condition
the request to ensure the same file is served later.
If you are using Cloudflare R2 like me, you will need to strip the quotes off
your Etag [L7].
Caching
^^^^^^^
Partial requests are not cacheable on the edge. If the status code is 206
Partial Content, do not attempt to cache the response.
Progressive Web Apps
^^^^^^^^^^^^^^^^^^^^
If you're serving videos on a PWA, you may find out it doesn't stream right!
Turns out, Google's modern web development evangelism brand web.dev documents a
way to simulate streaming requests within an offline environment! In fact, this
was the final inspiration and reference that got me to implement this feature.
Check out PWA with offline streaming [L8] (archived [L9]).
Conclusion
----------
I get why Chrome would prefer servers that behave this way. Partial responses
are clearly superior in respecting the user's bandwidth and integrating with a
video player. That said, only Chrome and Chrome derivatives have this problem.
Safari used to, Firefox used to. Both solved it.
If you are having this problem on your mastodon instance, you might be able to
serve static content through nginx instead of through a ruby process. If your
blog is having this issue and you're doing coolâ„¢ things on the edge like this
blog, then you'll have to do some handiwork to get this working. Should you go
this path Chrome will not log any errors when videos break or stop loading. Be
prepared to debug with careful observation and thorough testing.
--------------------------------------------------------------------------------
[L1]: /posts/2022-12-12-device-oauth-flow-is-phishable.html
[L2]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
[L3]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
[L4]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
[L5]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length
[L6]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range
[L7]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
[L8]: https://web.dev/pwa-with-offline-streaming/
[L9]: https://archive.is/Qknnj
[I1]: https://c.cdyn.dev/YA8W7ZYb
[I2]: https://c.cdyn.dev/BhvJFJc8
[I3]: https://c.cdyn.dev/vYbcbtjy
[I4]: https://c.cdyn.dev/4YzXGiFQ
[I5]: https://c.cdyn.dev/ogUorBdQ
[I6]: https://c.cdyn.dev/nox4OQ7u