The runtime memory budget of my 1.17 MB player: keeping playback under 15 MB RAM
Quick disclosure up front: I build RKMovies, an ultra-lightweight media player for Android and Windows. This is a dev diary about one number on that page: the RAM figure.
Everyone asks about APK size. Nobody asks what the app does with RAM once it is running, and that is the number that actually decides whether a player survives on a 2 GB phone from 2018. So I profiled it.
What I measured
Setup: Android 11 phone with 2 GB RAM, playing a 1080p H.264 file with a 5.1 audio track and an SRT subtitle file. Profiler: Android Studio Memory Profiler plus dumpsys meminfo on the release build. APK is 1.17 MB; install size is a bit more because of the extracted native libs.
Result: steady-state playback sits under 15 MB of app RAM. Not "15 MB at idle, 200 MB when playing". Under 15 MB while playing.
Where the bytes actually go
This is the part that surprised me. The video frames are not in my memory budget at all. The decoder owns the frame buffers and they live in the codec's own process memory (or shared memory with the surface), not in the app heap. If you copy frames into a Bitmap, you are doing it wrong. That alone can eat 30-80 MB on a 1080p stream. My player renders straight to a SurfaceView and never touches a pixel in app memory.
What is in my heap during playback:
- The subtitle cue window. I parse SRT, convert to WebVTT, and keep only the cues inside a 30-second sliding window. That is a few KB, not the whole file.
- One poster bitmap, downscaled with inSampleSize before decode. A full-res poster from the web is 10-20 MB decoded; mine loads at thumbnail size, a few hundred KB.
- The gesture HUD state: two floats and a timestamp. That is it.
- Watch state (position, audio track, subtitle track) goes to IndexedDB on every pause, not kept as a big in-memory model.
The mistakes I made first
Version 0.9 kept the whole subtitle file parsed in memory, cached every poster it had ever shown, and copied one frame into a Bitmap for the "now playing" thumbnail. On the 2 GB phone it got OOM-killed mid-movie. The fix was not heroic engineering. It was deleting code:
- Sliding window for cues instead of whole-file parse.
- LRU of size 1 for posters.
- Decoder output straight to surface, thumbnail from a downscaled stream.
Deleting code is the best optimization. A 1.17 MB APK is not impressive because the binary is small; it is impressive because there is almost nothing in it to allocate.
Windows side
The 29.5 MB installer is mostly the WebView2 bootstrapper, not my code. Same rule applies on desktop: hardware decode via NVDEC/QuickSync with DXVA fallback, frames never copied into managed memory, and the whole UI is one canvas for the gesture layer so the compositor does the work.
If you want to poke at it: get the Android build (1.17 MB, sideload) or the exe download page for Windows. Hardware-accelerated decode, dual-zone gesture HUD, multi-audio tracks, WebVTT subtitles, zero telemetry, no account. I made it because every player I tried ate storage and RAM my old phone did not have.
What memory tricks do you use to keep small apps small? Genuinely curious what I am still missing.