← Back to list

On-Device Video Quality Enhancement

A blurry video stream is not fixed by simply adding a sharper filter.

Hui Wang · 2026-06-03 18:33 · 0 claps · 5.2 min read
#video-technology #mobile-development #artificial-intelligence #ios-development #android-development
Open on Medium ↗
Wiki topics: AI · AI · General 📱 · Mobile Development

On-Device Video Quality Enhancement

A blurry video stream is not fixed by simply adding a sharper filter.

Real on-device video quality enhancement has a harder constraint: every decoded frame must be enhanced before the next frame arrives. At 60 fps, the system has about 16 ms per frame for decoding, enhancement, rendering, UI work, and synchronization. If the app copies each frame into CPU memory, converts formats, runs a model, then copies it back to the GPU, the feature may look good in a demo and still fail in production.

The important idea is this: on-device video enhancement is a frame pipeline problem first, and an AI model problem second.

The key idea

Client-side video enhancement usually combines several techniques: super-resolution, deblocking, denoising, tone mapping, and region-based enhancement for faces or subtitles. The business reason is clear: transmit a lower-bitrate stream, then recover perceived quality locally on the device. The source material correctly emphasizes this trade-off and the need for a zero-copy-oriented pipeline.

But the engineering boundary is often misunderstood.

The goal is not to “make every frame beautiful.” The goal is to improve the frame without breaking playback.

That means the frame should stay as close as possible to the hardware decode and render path. On Apple platforms, Core ML can use CPU, GPU, and Neural Engine depending on configuration and model support, and MLComputeUnits lets developers restrict which compute units are allowed. For Metal rendering, CVMetalTextureCache is designed for sharing Core Video image buffers with Metal kernels.

On Android, AHardwareBuffer represents memory that can be shared across hardware components, which makes it relevant to decoder, GPU, camera, and rendering pipelines.

The practical rule is simple: avoid turning video frames into ordinary images unless you are willing to pay the performance cost.

Core example: a small enhancement switch with real frame boundaries

Problem:

The user enables AI enhancement during video playback.
The app should enhance frames only when the device can afford it.
The UI should not know whether the engine uses Core ML, Metal, Vulkan, GPU delegates, or a fallback shader.

Bad direction:

Decoded frame
  -> convert to UIImage or Bitmap
  -> run enhancement
  -> convert back to texture
  -> render

This design is easy to write but expensive to run. UIImage and Bitmap are convenient UI abstractions, not ideal real-time video frame abstractions.

Better direction:

Decoded frame
  -> hardware-backed frame buffer
  -> enhancement engine
  -> renderable output frame
  -> display

The code should reflect that boundary.

iOS:

import SwiftUI
import Observation
import CoreML
import CoreVideo

enum EnhancementMode {
    case off
    case regionOnly
    case fullFrame
}

@Observable
final class VideoEnhancementState {
    private let engine: VideoEnhancementEngine
    private let policy: EnhancementPolicy

    var enabled = false
    var mode: EnhancementMode = .off
    var message: String?

    init(engine: VideoEnhancementEngine, policy: EnhancementPolicy) {
        self.engine = engine
        self.policy = policy
    }

    func setEnabled(_ value: Bool) {
        guard value else {
            enabled = false
            mode = .off
            engine.stop()
            return
        }

        let selectedMode = policy.bestMode()

        guard selectedMode != .off else {
            enabled = false
            mode = .off
            message = "Enhancement is unavailable under current device conditions."
            return
        }

        enabled = true
        mode = selectedMode
        message = nil
        engine.start(mode: selectedMode)
    }

    func process(_ frame: CVPixelBuffer) -> CVPixelBuffer {
        guard enabled else { return frame }
        return engine.enhance(frame)
    }
}

final class VideoEnhancementEngine {
    private lazy var modelConfiguration: MLModelConfiguration = {
        let configuration = MLModelConfiguration()
        configuration.computeUnits = .cpuAndNeuralEngine
        return configuration
    }()

    func start(mode: EnhancementMode) {
        // Warm up model, allocate reusable buffers, prepare Metal/Core ML path.
    }

    func stop() {
        // Release temporary resources or downgrade to normal rendering.
    }

    func enhance(_ frame: CVPixelBuffer) -> CVPixelBuffer {
        // Key point:
        // Keep the API at CVPixelBuffer level.
        // Avoid converting every frame into UIImage or Data.
        return frame
    }
}

struct EnhancementPanel: View {
    @State private var state: VideoEnhancementState

    init(state: VideoEnhancementState) {
        _state = State(initialValue: state)
    }

    var body: some View {
        VStack(alignment: .leading) {
            Toggle("AI Enhance", isOn: $state.enabled)
                .onChange(of: state.enabled) { _, value in
                    state.setEnabled(value)
                }

            Text("Mode: \(String(describing: state.mode))")

            if let message = state.message {
                Text(message).font(.footnote)
            }
        }
        .padding()
    }
}

The important line is not the placeholder model call. It is this boundary:

func process(_ frame: CVPixelBuffer) -> CVPixelBuffer

That keeps the enhancement path compatible with decoder output, Core Video, Metal, and Core ML-oriented workflows.

Android: Kotlin + Compose

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

enum class EnhancementMode {
    OFF,
    REGION_ONLY,
    FULL_FRAME
}

data class EnhancementState(
    val enabled: Boolean = false,
    val mode: EnhancementMode = EnhancementMode.OFF,
    val message: String? = null
)

class VideoEnhancementController(
    private val engine: VideoEnhancementEngine,
    private val policy: EnhancementPolicy
) : ViewModel() {

    private val _state = MutableStateFlow(EnhancementState())
    val state: StateFlow<EnhancementState> = _state

    fun setEnabled(enabled: Boolean) {
        if (!enabled) {
            engine.stop()
            _state.value = EnhancementState()
            return
        }

        val selectedMode = policy.bestMode()

        if (selectedMode == EnhancementMode.OFF) {
            _state.update {
                it.copy(
                    enabled = false,
                    mode = EnhancementMode.OFF,
                    message = "Enhancement is unavailable under current device conditions."
                )
            }
            return
        }

        engine.start(selectedMode)

        _state.update {
            it.copy(
                enabled = true,
                mode = selectedMode,
                message = null
            )
        }
    }

    fun process(frame: HardwareVideoFrame): HardwareVideoFrame {
        return if (state.value.enabled) engine.enhance(frame) else frame
    }
}

interface VideoEnhancementEngine {
    fun start(mode: EnhancementMode)
    fun stop()
    fun enhance(frame: HardwareVideoFrame): HardwareVideoFrame
}

data class HardwareVideoFrame(
    val timestampNs: Long
    // Production code may wrap a decoder surface, hardware buffer,
    // external texture, or Vulkan-compatible image handle.
)
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

@Composable
fun EnhancementPanel(controller: VideoEnhancementController) {
    val state by controller.state.collectAsState()

    Switch(
        checked = state.enabled,
        onCheckedChange = controller::setEnabled
    )

    Text(text = "Mode: ${state.mode}")

    state.message?.let {
        Text(text = it)
    }
}

Again, the key detail is the frame type:

fun process(frame: HardwareVideoFrame): HardwareVideoFrame

The UI never receives a Bitmap. The enhancement engine can later choose a GPU path, Vulkan path, hardware buffer path, or lightweight shader fallback without changing the UI.

Why this works

This design works because it protects the hot path.

Video playback is a continuous pipeline. Every avoidable copy increases latency, memory bandwidth, and thermal pressure. Once the phone heats up, the system may throttle CPU or GPU frequency, which makes the next frames even harder to process.

That is why production systems usually need multiple modes:

High-end device:
full-frame super-resolution + denoise + tone mapping

Mid-range device:
region-based enhancement for face or subtitle areas

Low-end or hot device:
disable AI enhancement and keep stable rendering

The best user experience is not always the strongest model. Sometimes the best choice is a weaker enhancement that never drops frames.

One practical limitation

Do not describe the pipeline as perfectly zero-copy unless you have measured it.

Real devices may still require format conversion, texture synchronization, intermediate buffers, or runtime-specific layout changes. The better wording is zero-copy-oriented: design the system to avoid CPU round trips on the critical path, then verify with profiling tools.

This is especially important on Android now that NNAPI is deprecated in Android 15. New implementations should hide the inference backend behind an engine interface so the app can migrate between runtime choices without rewriting playback logic.

Practical rule

Design the frame abstraction before choosing the enhancement model.

If the frame type is UIImage or Bitmap, you are probably building an image feature.

If the frame type is CVPixelBuffer, hardware buffer, decoder surface, external texture, or a renderable frame wrapper, you are building a real-time video feature.

Conclusion

On-device video quality enhancement is valuable because it lets products trade network bandwidth for local computation.

The common mistake is treating it as a model showcase. In production, the pipeline decides whether the model can run every frame without damaging playback.

The core implementation idea is to keep frames in hardware-friendly representations and downgrade gracefully when the device cannot afford full enhancement.

Interview-friendly sentence: real-time video enhancement is not just about generating sharper pixels; it is about doing it inside the frame budget.


메타데이터
post_id
4fcfdafec62f
slug
on-device-video-quality-enhancement-4fcfdafec62f
url
https://medium.com/@foks.wang/on-device-video-quality-enhancement-4fcfdafec62f
canonical_url
https://medium.com/@foks.wang/on-device-video-quality-enhancement-4fcfdafec62f
author_url
https://medium.com/@foks.wang
status
ok
fetched_at
2026-06-09 15:37:30