DEV Community

hammi
hammi

Posted on

Dead stream mid-episode? My 1.17 MB player just falls down the quality list

Disclosure: I built RKMovies, a small 4K media player client for Android and Windows. This is a dev diary about one of its error paths. The landing page is RKMovies, with the 1.17 MB Android APK and the PC build.

Stream URLs die. A CDN edge returns 410 Gone, a signed URL expires an hour in, a proxy node has a bad day. In a 1.17 MB APK you do not get to ship ffmpeg. You get the system's MediaCodec through the WebView's video element, and its error event. So the player treats error as a signal, not a failure.

The handler is small (public/parts/app-player.js):

art.on("error", () => {
  if (art.seeking || (art.video && art.video.seeking)) return;
  const v = art.video;
  if (v && v.error && v.error.code === 1) return; // MEDIA_ERR_ABORTED is a normal seek abort
  const curTime = (v && typeof v.currentTime === "number") ? v.currentTime : (art.currentTime || 0);
  const i = list.indexOf(STATE.selRes);
  if (i > -1 && i < list.length - 1) {
    toast("Stream failed - trying another quality");
    STATE.selRes = list[i + 1];
    const updatedRec = Object.assign({}, rec || {}, { pos: curTime, watched: false });
    renderPlayer(shell, s, updatedRec);
  } else {
    toast("This source failed to play.");
  }
});
Enter fullscreen mode Exit fullscreen mode

The first guard matters most. MEDIA_ERR_ABORTED (code 1) is filtered out because every seek fires an aborted load on the old source. Without that check the player would "recover" from every scrub the user does.

Position is preserved. curTime gets written into the watch record, and renderPlayer re-seeks on ready when pos is past 10 seconds. The user loses about a second of buffer, nothing else.

The stream list is sorted by resolution descending, so list[i + 1] is the next lower quality. Falling down the ladder beats dying: a 1080p stream with a hiccup still plays, while nothing at all does not. If the error fires on the last entry in the list, the UI just says the source failed.

One level up, the play call itself gets a silent retry (public/app.js): two attempts with a 2.6 second pause between them, because the proxy already retries internally and this only covers a full 502 burst. If both attempts fail, the UI shows "Stream not available for this title" and adds "(limited region)" when the API flags it.

The honest trade-off: on very old phones, software decode of a high bitrate fallback can still stutter. Hardware decode is what makes the fallback actually smooth, which is also why the APK stays at 1.17 MB. No bundled codec libraries, system codecs only (MediaCodec on Android, NVDEC/QuickSync in the Windows build).

If you want to poke at the error path yourself, grab the APK or the Windows installer.

Top comments (1)

Collapse
 
alexcodebytes profile image
Oleksandr •

The MEDIA_ERR_ABORTED filter is the real MVP here. Without it, every manual seek would trigger a fake stream failure. Gracefully degrading to a lower quality while saving the exact timestamp is brilliant UX. Keeping the APK at 1.17 MB by ditching ffmpeg for system MediaCodec is a massive flex!