DEV Community

hammi
hammi

Posted on

how my player picks video quality without a streaming engine

most players pick quality from the network speedometer. navigator.connection.downlink gives you megabits... once. mobile networks swing hard. 5g at midnight is not the same 5g at 9pm, and a reading taken at minute zero says nothing about minute forty.

this is a build diary. full disclosure up front: i'm the developer of RKMovies, a tiny media player client (0.91 mb apk, system decoders, zero tracking). the auto quality mode in it ships no dash engine, no abr module, nothing. it watches the buffer instead. here is the whole trick.

what the app does instead

RKMovies leans on the system decoders. mediacodec on android, nvdec and quicksync on windows. every stream is a plain mp4 or a native-playable m3u8, so the video element handles playback and i never babysit segments.

auto mode then works in two layers.

layer one is the first guess. if downlink is available, use it: 5 mbps or better starts at 1080p, 2.5 at 720p, 1 at 480p, everything lower at 360p. a budget phone on 4g gets a sane start instead of trying 4k first and choking.

layer two is the interesting one. a 4-second interval just reads video.buffered and measures how many seconds of video sit ahead of the playhead. i do not care what the network claims. i care what actually arrived.

  • buffered ahead over 22 seconds, 4 checks in a row: step one resolution up
  • buffered ahead under 1.5 seconds, 2 checks in a row, past second 5 of playback: step one resolution down
  • never more than one step at a time, in either direction

slow up, fast down. that ratio is the entire product decision.

why slow up, fast down

stepping up when the buffer is fat keeps the picture sharp when the wifi is genuinely good. you need sustained health, not one lucky tick, so 4 in a row is the bar.

stepping down fast matters more, and it matters most on metered data. a 4k stream on a 1.5 gb per day plan is a budget decision, not a tech spec. congestion shows up in the buffer 10 to 20 seconds before pixels freeze, because bytes stop arriving long before the playhead catches up to the gap. waiting for the freeze event means you already lost.

one step at a time also kills oscillation. jump two renditions and a jittery night turns into a yo-yo between 1080p and 360p. crawl one step and the picture settles.

the switch itself

smoothSwitchQuality() grabs currentTime, swaps the source, waits for loadedmetadata, seeks back, resumes. the change costs the user half a second at most and most people never notice it happened. the 4-second tick itself costs basically nothing, it reads one property and does one comparison.

numbers i shipped with

  • tick: 4000 ms
  • healthy target: 22 s buffered ahead
  • panic line: 1.5 s buffered ahead
  • upvotes needed to go sharper: 4 consecutive healthy checks
  • stalls before dropping: 2 consecutive

yes, hls.js does this dance better at scale, with per-fragment stats and bandwidth estimators. if the client ever moves to a full hls.js pipeline, the buffer logic moves with it. but for a player that fits in 0.91 mb and trusts the system decoder, the plain element plus a buffer watcher is the whole game.

if you want to try it, the ad-free 4K player for Android is one tap, and there is a Windows desktop app with the same auto mode for the pc crowd. i read the buffer the same way in both.

Top comments (0)