I'm the developer of RKMovies, a 0.91 MB media player for Android and Windows. One of the small features people ask about is the brightness gesture: swipe up on the right half of the player and the screen dims.
Here's the thing though. There is no one brightness API that works everywhere my app runs. So I built a two-layer one.
Layer 1: the native path, when it exists
On Android the WebView shell exposes a JS bridge. If window.AndroidBridge.setBrightness exists, I call it and I'm done. That changes the actual system brightness, which is what you want most of the time.
On the Windows build there's a local backend, so I debounce and fire GET /api/system/brightness?val=... instead. 50 ms debounce, because the gesture fires dozens of times per second and I don't want to flood the IPC.
Layer 2: the CSS filter fallback
WebViews and browser contexts can't touch system brightness. So every time the gesture moves, I also set a filter directly on the <video> element:
function applyPlayerBrightness(art, val) {
const clamped = Math.max(0.1, Math.min(1.5, Math.round(val * 100) / 100));
art._brightness = clamped;
if (art.video) {
art.video.style.filter = "brightness(" + (0.4 + clamped * 0.7) + ")";
}
setSystemBrightness(Math.min(1.0, clamped)); // native layer, clamped to 0..1
// ...HUD update
}
Note the math: user-facing range is 10% to 150%, but the filter maps it to 0.4x to 1.45x. I never go below 0.4. A pure black screen feels broken, not dim, and people think the app crashed. 0.4 is the floor where the picture is still readable.
The filter only darkens/brightens the video itself, not the player UI. That's actually a feature: the controls stay readable while the movie dims, which is exactly what you want at 2am under a blanket.
Why not just use the native API everywhere
Because "everywhere" is a lie. Android WebView: no. Windows WebView2: no (hence the local backend). Plain browser PWA: no. If I depended on the native path alone, the gesture would silently do nothing on two of my three platforms.
The filter runs on the compositor, so it costs basically nothing on the CPU. On a low-battery video player that's the whole point. I'd rather spend zero battery on brightness and keep the decoder on the GPU.
The edge case nobody warns you about
CSS filter on a hardware-decoded video can force a software compositing path on some old GPUs, which tanks battery. I tested on a 2GB Redmi and a Moto G4 and the filter stayed on the compositor, but if you ship this, test on your lowest-end device. If it falls off the fast path, drop the filter layer and keep only the native API.
The desktop edition uses the same two-layer code, just with the local /api/system/brightness endpoint as layer 1 instead of the Android bridge.
If you're building a player and want gesture brightness without a native app, this split is the cheapest version that actually works everywhere. Ship the filter first, add the native path when the shell gives you one.
Top comments (0)