Skip to main content

One post tagged with "MediaCodec"

MediaCodec is a low-level Android API used to access hardware and software media codecs for encoding and decoding audio, video, and compressed data. It enables developers to perform high-performance tasks like streaming, transcoding, and low-latency playback while offloading heavy processing from the main CPU to dedicated hardware.

View All Tags

Preloading Multiple ExoPlayer Instances Without Exhausting the VPU's MPS Budget

· 21 min read
Austin Kim
Staff Software Engineer, System programming and media framework specialist

TL;DR

  • The problem: preloading several ExoPlayer instances so playback can start instantly hits the VPU's shared decoder admission budget — creating too many MediaCodec hardware decoders concurrently gets rejected (OMX_ErrorInsufficientResources / MediaCodec.CodecException, ExoPlayer error codes 4001/4003) — even though only one video is ever actually playing at a time.
  • Why it happens: ExoPlayer sets KEY_OPERATING_RATE to the real content frame rate — effectively "1x," the actual playback rate — on every decoder it creates, whether that decoder is actively playing or just sitting preloaded and idle (Section 5), and it never sets MediaFormat.KEY_PRIORITY at all (confirmed absent from MediaCodecRenderer.java/MediaCodecVideoRenderer.java). The net effect: the VPU's admission accounting has no signal telling it "this decoder isn't playing yet" and treats every created instance as if it needs guaranteed real-time decode throughput the moment it's created, not just once it's actually feeding frames. (Qualcomm's own kernel driver control technically defaults KEY_PRIORITY to non-realtime — Section 3.5 — but the resource exhaustion this document exists to solve is consistent with the vendor's closed-source Codec2/OMX HAL layer not preserving that default in practice; unverified from here, flagged as a caveat throughout.)
  • The fix: explicitly set KEY_PRIORITY = 1 (non-realtime) and a low KEY_OPERATING_RATE on every decoder in the preload pool while it's idle, then flip both to realtime / full rate only on the one instance that's actually about to play (Section 6). Non-realtime load is charged at roughly resolution × 1 instead of resolution × fps in the admission math (Sections 3.1–3.3), and non-realtime instances are excluded entirely from the per-core clock-fit check that gates new decoder admission (Section 3.6). For 30fps video, that's roughly a 30× difference in admission cost between an idle non-realtime decoder and one actively playing at realtime priority (worked example, Section 3.4) — meaning, in principle, on the order of 30× more preloaded decoder instances can coexist in the same hardware budget than the naive "every decoder is realtime by default" approach allows.

1. The problem

An app wants to preload several videos so playback can start instantly when the user picks one — but only one video ever plays at a time. The naive approach (create and start() a fully warm MediaCodec hardware decoder for every preloaded ExoPlayer instance) runs into a hard limit: the device's video decode block (VPU) rejects new decoder instances once the platform's admission-control accounting decides the combined load would exceed hardware capacity. This shows up as "insufficient resources" / codec creation failures once enough decoders are open concurrently, even though only one is ever actually decoding frames at once.

The real optimization target is hardware decoder component init latency — creating, configure()-ing, and start()-ing a MediaCodec against a HW codec is what's slow (tens of ms and up). Prefetching network/container data alone doesn't remove that latency; only having an already-initialized decoder does.

2. What the "MPS limit" actually is

There's no single standardized "MPS limit" in AOSP. Two different things are easy to conflate:

  • MediaCodecInfo.VideoCapabilities (getMacroblocksPerSecond(), isSizeAndRateSupported()) describes what one codec instance can sustain. This is public API and standardized.
  • The shared, cross-instance VPU budget that rejects a new decoder once too many are open is enforced by the vendor's resource manager (Qualcomm, MediaTek, etc.), not by the Android framework. It's undocumented, implementation-specific, and can differ across SoC vendors, chipset generations, and even firmware/Android version updates on the same chipset.

Everything below was reverse-engineered from Qualcomm's msm-vidc kernel driver (the Venus/Iris video decode block used on Snapdragon devices). It is a case study, not a platform guarantee — see Section 8.

3. The core mechanism (Qualcomm case study)

3.1 The load formula

From msm_vidc_common.c (drivers/media/platform/msm/vidc/msm_vidc_common.c):

static int msm_comm_get_mbs_per_sec(struct msm_vidc_inst *inst)
{
int output_port_mbs, capture_port_mbs;
int fps;

output_port_mbs = NUM_MBS_PER_FRAME(inst->prop.width[OUTPUT_PORT],
inst->prop.height[OUTPUT_PORT]);
capture_port_mbs = NUM_MBS_PER_FRAME(inst->prop.width[CAPTURE_PORT],
inst->prop.height[CAPTURE_PORT]);

if ((inst->clk_data.operating_rate >> 16) > inst->prop.fps)
fps = (inst->clk_data.operating_rate >> 16) ?: 1;
else
fps = inst->prop.fps;

return max(output_port_mbs, capture_port_mbs) * fps;
}

NUM_MBS_PER_FRAME(w, h) is (w × h) / (16 × 16) — macroblocks per frame. This per-instance value is summed across every open instance of a given type (msm_comm_get_load(), which iterates core->instances and adds msm_comm_get_mbs_per_sec() for each) and checked against a platform-defined max_load on codec creation and clock-scaling recalculation. That sum is the actual admission gate.

3.2 What KEY_OPERATING_RATE really does to that formula

The critical detail: fps in the formula is max(configured frame-rate, operating_rate)not the frame-rate alone. KEY_FRAME_RATE sets inst->prop.fps at configure() time; KEY_OPERATING_RATE sets clk_data.operating_rate.

This means declaring a low KEY_FRAME_RATE for idle/preloaded decoders and later raising KEY_OPERATING_RATE right before playback does not quietly bypass accounting — the moment operating rate goes up on an instance, that instance's contribution to the aggregate load recomputes to the real value too. The technique only works because the total is a sum across instances: as long as (N−1 idle instances × low load) + (1 active instance × full load) ≤ max_load, admission still succeeds. It requires bringing operating rate back down (or releasing/recreating the codec) once an instance stops being the active one — leaving it elevated defeats the "cheap while idle" premise.

3.3 Realtime vs. non-realtime sessions — the more robust lever

The same file defines a documented split for exactly this pattern, in a comment directly above the load-calc logic:

| OPERATING RATE SET | OPERATING RATE NOT SET |
-----------------|----------------------|------------------------|
REALTIME | load = res * op_rate | load = res * fps |
| clk = res * op_rate | clk = res * fps |
-----------------|----------------------|------------------------|
NON-REALTIME | load = res * 1 fps | load = res * 1 fps |
| clk = res * op_rate | clk = res * fps |
-----------------|----------------------|------------------------|

For a non-realtime session (is_realtime_session(inst) false, gated by the LOAD_CALC_IGNORE_NON_REALTIME_LOAD quirk), the admission load stays pinned near the bare macroblock count (effectively "1 fps") regardless of operating rate, while the clk (actual achievable decode throughput) still scales with operating rate. In other words: a non-realtime-priority decoder can be clocked to real-time decode speed via KEY_OPERATING_RATE while still contributing almost nothing to the shared admission budget. This looks like the intended, driver-supported way to do what the frame-rate trick only approximates.

3.4 Worked example: four concurrent decoders

KEY_PRIORITY to is_realtime_session to VPU admission load diagram

The diagram above traces the full path — MediaFormat.KEY_PRIORITYC2_PARAMKEY_PRIORITY (Codec2 vendor HAL) → is_realtime_session(inst) (msm-vidc kernel driver) → which branch of the load formula from Section 3.3 applies — and then applies it to four example MediaCodec instances: three preloaded/idle decoders configured non-realtime (KEY_PRIORITY = 1), and one actively-playing decoder configured realtime (KEY_PRIORITY = 0, the default). All numbers are illustrative, not a real device's certified budget.

Each instance's MPS (macroblocks per second) is NUM_MBS_PER_FRAME(w, h) × fps, where NUM_MBS_PER_FRAME rounds each dimension up to the nearest 16-pixel macroblock boundary before multiplying — this is why 1080p uses a height of 1088 (1080 isn't a multiple of 16; the hardware pads to 68 macroblock rows instead of 67.5) while 720p, whose dimensions are already multiples of 16, needs no rounding:

  • Decoder A (non-realtime, idle): 1920×1088 → 120 × 68 = 8,160 mb/frame. Non-realtime forces fps = 1 for the load calculation regardless of the real content frame rate, so load = 8,160 × 1 = 8,160 MPS.
  • Decoder B (non-realtime, idle): 1280×720 → 80 × 45 = 3,600 mb/frame. Same non-realtime rule: load = 3,600 × 1 = 3,600 MPS.
  • Decoder C (non-realtime, idle): same resolution as A → load = 8,160 MPS.
  • Decoder D (realtime, actively playing): 1920×1088 → 8,160 mb/frame. Realtime uses fps = max(configured_fps, operating_rate) = max(30, 30) = 30, so load = 8,160 × 30 = 244,800 MPS — roughly 30× more expensive than the same resolution sitting idle as non-realtime.

Summed: 8,160 + 3,600 + 8,160 + 244,800 = 264,720 MPS. Against the example MAX_LOAD ≈ 300,000 budget, that fits with roughly 35,000 MPS of headroom. The callout in the diagram shows why this matters: if all four decoders had instead been left at the realtime default while merely sitting idle (i.e. no KEY_PRIORITY override at all, each still counted at its full configured/operating fps), the sum would be 8,160×30 + 3,600×30 + 8,160×30 + 8,160×30 = 842,400 MPS — nearly 3× the same budget, and the fourth decoder would fail to admit. That gap is the entire value of the non-realtime-while-idle pattern from Sections 3.2–3.3.

3.5 Qualcomm's actual default, and what CTS actually tests

Two directly-grounded findings, closing gaps left open above.

Qualcomm's driver-level default is non-realtime. In msm_vdec.c, the V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY control's default_value is V4L2_MPEG_MSM_VIDC_DISABLE, and the DISABLE case inside msm_vdec_s_ctrl() clears VIDC_REALTIME (inst->flags &= ~VIDC_REALTIME). Since is_realtime_session() is !!(inst->flags & VIDC_REALTIME) and inst starts zero-initialized, a session that never touches KEY_PRIORITY is non-realtime by default at the kernel driver level — cheap (mb/frame × 1) rather than expensive (mb/frame × fps). This is grounded independently of, and matches, the generic AOSP contract quoted below. Caveat: this is the kernel control's default specifically; the vendor Codec2/OMX HAL sitting above it could still explicitly override it during its own init sequence, and that layer is closed-source and unverifiable from here.

What CTS actually tests isn't priority-specific MPS accounting — it's the resource-exhaustion contract. From AOSP's SoC vendor dependencies for media resource manager:

An Android Compatibility Test Suite (CTS) test exists to allocate, configure and start each codec repeatedly until catching OMX_ErrorInsufficientResources (pass) or any other error (fail).

There's no CTS assertion that a KEY_PRIORITY = 1 session gets any specific MPS discount — that math is vendor-internal policy, explicitly framed in the same doc as a "hint... for resource planning." What's actually certified is narrower: once a device's shared decoder budget is exhausted (however the vendor accounts for it), the component must fail with a clean, structured error — OMX_ErrorInsufficientResources, or its Codec2 equivalent C2_NO_MEMORY — rather than hang, crash, or ANR. CCodec.cpp carries that status up to the Java layer as MediaCodec.CodecException (the 0xfffffff4 / -12 / ENOMEM error reported against real devices in the wild, and the same failure class behind ExoPlayer's error codes 4001/4003 noted in Section 1). That's the behavior to design a retry/fallback path around when a preload pool actually does exceed budget on a given device — not graceful silent degradation.

The practical CTS-relevant bar for C2_PARAMKEY_PRIORITY itself is just that a compliant component must accept config()/query() on it without error (no C2_BAD_INDEX/unsupported-param failure) — CTS doesn't verify what the component does with the value internally.

3.6 The actual clk/core admission mechanism — and why it's realtime-aware by construction

A natural follow-up question: is a resource-exhaustion failure (ENOMEM/OMX_ErrorInsufficientResources) triggered by load (the MPS-sum admission gate from Section 3.1) or by clk (the clock-scaling value from Section 3.3's table)? The honest answer is that they aren't two separate mechanisms — clk is load converted into hardware cycles, and the actual hard-failure path lives in msm_vidc_clocks.c, in msm_vidc_decide_core_and_power_mode():

if (current_inst_load + min_load < max_freq) {
inst->clk_data.core_id = min_core_id;
...
} else if (current_inst_lp_load + min_load < max_freq) {
...
} else if (current_inst_lp_load + min_lp_load < max_freq) {
...
} else {
rc = -EINVAL;
dprintk(VIDC_ERR, "Sorry ... Core Can't support this load\n");
return rc;
}

current_inst_load is msm_comm_get_inst_load(inst, ...) × vpp_cycles / work_route — the same REALTIME/NON-REALTIME-gated load value from Section 3.3's table (res×fps or res×1), converted to cycles/sec via a codec-specific cycles-per-macroblock constant. max_freq is allowed_clks_tbl[0].clock_rate — the fastest clock rate the SoC's VPU cores can run at. Failing every branch above returns -EINVAL ("Core Can't support this load") — the mechanism behind the resource-exhaustion failures discussed in 3.5.

The side of the comparison that matters most for this document is how "how full is this core already" gets computed — get_core_load(core, core_id, lp_mode, real_time), which filters:

real_time_mode = inst->flags & VIDC_REALTIME ? true : false;
...
if (real_time_mode != real_time)
continue;

msm_vidc_decide_core_and_power_mode() always calls this with real_time = true — meaning only already-open realtime instances count toward how full a core is when deciding whether a new instance fits. Non-realtime instances are invisible to this sum entirely, and — because a non-realtime instance's own current_inst_load is computed at the "×1" rate — it barely registers its own demand either. This is the most direct, mechanistic confirmation in this document of the non-realtime-while-idle strategy from Sections 3.2–3.4: it isn't just cheap in the abstract MPS-sum math, it's specifically excluded from the exact code path that decides whether new decoders get admitted or rejected on a given core.

4. The three MediaFormat keys involved

KeyWhat it doesWhen settable
KEY_FRAME_RATEBaseline fps used in resource/MPS calculations; required for encoders, optional for decoders.configure() only
KEY_OPERATING_RATEHints the desired processing rate; drives clock scaling (and, per above, is folded into the load formula on Qualcomm).configure(), and confirmed dynamically adjustable later via setParameters()
KEY_PRIORITY0 = realtime priority (default), any nonzero value (conventionally 1) = non-realtime/best-effort. Generic AOSP-level key, not vendor-specific — it's the documented framework signal that (on Qualcomm) maps to is_realtime_session().configure(), and the framework's SDK→Codec2 mapping layer does not restrict it to configure-only (see note below) — but whether a given vendor's component actually applies a live change is unconfirmed and must be tested on-device

Recommended pattern given the driver behavior above: set KEY_PRIORITY = 1 at configure() time for every decoder in your preload pool, and toggle KEY_OPERATING_RATE dynamically per-instance — raise it right before feeding real frames to the instance that's about to play, lower it again once that instance goes back to being merely "preloaded."

Update on KEY_PRIORITY mutability: frameworks/av/media/codec2/sfplugin/CCodecConfig.cpp (the modern Codec2-based SDK→component mapping layer) maps KEY_PRIORITY to C2_PARAMKEY_PRIORITY with no domain restriction (Domain::ALL, which includes the IS_PARAM bit used for setParameters()), unlike KEY_OPERATING_RATE, which is explicitly restricted to D::PARAM | D::CONFIG and marked // write-only. In other words, the framework does not treat KEY_PRIORITY as configure-only — a live setParameters() call carrying it will be forwarded to the component. Whether the specific vendor Codec2 component actually honors a live priority change once running (vs. silently ignoring it) is component-specific and wasn't verifiable from this generic layer — test it directly: toggle KEY_PRIORITY via setParameters() on a running, idle decoder and watch the vendor's resource-manager/vidc logs to see whether its accounted load actually changes. If it does work live, you may not need a separate configure-only pool at all — you could flip a single decoder's priority down while idle and back up right before promoting it to active playback, alongside the operating-rate toggle.

5. What ExoPlayer / Media3 does out of the box

Checked directly against the androidx/media source (release branch):

  • MediaFormat.KEY_PRIORITY is never set anywhere in MediaCodecRenderer.java (the shared base class for all codec-backed renderers) or MediaCodecVideoRenderer.java. Every codec ExoPlayer creates gets whatever the platform/vendor default priority is (typically realtime, 0). ExoPlayer's own rendererPriority / MSG_SET_PRIORITY / PriorityQueue are an unrelated, ExoPlayer-internal renderer-scheduling concept — not the codec-level priority.
  • KEY_OPERATING_RATE is actively managed. MediaCodecVideoRenderer.getCodecOperatingRateV23() computes it as the highest frame rate among the current adaptive stream formats × target playback speed. MediaCodecRenderer applies it at initial configure() and also updates it live later via codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, newCodecOperatingRate) — confirming operating rate really is adjustable post-configure through setParameters(), both per Android's own docs and in ExoPlayer's actual implementation.
  • The injection point for a custom KEY_PRIORITY/operating-rate policy already exists: MediaCodecVideoRenderer.getMediaCodecConfiguration() is protected and overridable, and ExoPlayer itself mutates the MediaFormat there before returning it (e.g. maybeSetKeyAllowFrameDrop() sets KEY_ALLOW_FRAME_DROP). A subclass can add mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 1) the same way.
  • Caveat: getCodecOperatingRateV23()'s default logic derives operating rate from the real content frame rate regardless of whether the renderer is actually started or just sitting idle post-configure(). Left unmodified, a "preloaded but not yet playing" ExoPlayer instance will still request the real operating rate by default — you need to override this too so idle/preloaded instances ask for a low rate, and only the promoted "about to play" instance asks for the real one.

Note: Media3's PreloadMediaSource / DefaultPreloadManager (added ~1.4, extended through 1.8) is not a solution to this problem. It preloads MediaSource extraction/parsing/buffered sample data, and its shared RenderersFactory use is explicitly to check format compatibility "without creating the full renderers." It does not warm an actual MediaCodec decoder instance, so it doesn't touch decoder-init latency at all — the thing this whole exercise is trying to optimize.

  1. Maintain a bounded pool of ExoPlayer instances (or raw MediaCodec decoders) sized to fit your target devices' real admission budget — not "as many as the user might preload."
  2. Subclass MediaCodecVideoRenderer (via a custom RenderersFactory) to override:
    • getMediaCodecConfiguration() — inject KEY_PRIORITY = 1 at configure() time for every decoder in the pool. Treat this as the safe default; if on-device testing (Section 7) confirms your target vendor's component honors live setParameters() priority changes, you can instead toggle it dynamically alongside operating rate rather than only setting it once.
    • getCodecOperatingRateV23() — return a low value while an instance is idle/preloaded; return the real value (content frame rate × speed) only once that instance is promoted to "about to actively play."
  3. When promoting an instance to active playback, raise its operating rate (this can ride ExoPlayer's existing dynamic-update path); when demoting it back to idle, lower it again. Don't leave operating rate elevated on instances that aren't currently playing.
  4. Don't rely on KEY_FRAME_RATE spoofing as the primary lever — treat KEY_PRIORITY (non-realtime) as the intended mechanism, and KEY_OPERATING_RATE as the real-time throughput control.

6.1 Concrete hook points: onStarted() / onStopped()

BaseRenderer (the root of ExoPlayer's renderer hierarchy) exposes onStarted() and onStopped() lifecycle callbacks, fired when a renderer transitions to/from actually being started (i.e. playWhenReady and the player is in STATE_STARTED). MediaCodecRenderer.java already overrides both (currently no-ops, just present "to remove throws clause"), confirming these are legitimate, safe override points in a subclass — this is the natural place to promote/demote priority, separate from getMediaCodecConfiguration() which only runs once at codec creation.

MediaCodecVideoRenderer lifecycle: KEY_PRIORITY flips between idle and active playback

As the diagram shows: getMediaCodecConfiguration() sets KEY_PRIORITY = 1 once, at configure() time, before the decoder ever plays anything. Overriding onStarted() is where you promote — call codec.setParameters() with KEY_PRIORITY = 0 and the real operating rate right as the renderer actually starts pushing frames. Overriding onStopped() is the mirror — drop back to KEY_PRIORITY = 1 and a low operating rate the moment the renderer stops being active (paused, ended, or the user swipes to a different preloaded item).

This isn't a new mechanism to build from scratch: MediaCodecRenderer.java already has a live parameter-update pipeline wired up — handleMessage(MSG_SET_CODEC_PARAMETERS) calls codec.setParameters(activeCodecParameters.toBundle()) — which is exactly how ExoPlayer pushes KEY_OPERATING_RATE changes to a running codec today. Extending the same Bundle to also carry KEY_PRIORITY from your onStarted()/onStopped() overrides reuses infrastructure that's already proven to work for live parameter changes on a running codec — the only open question (Section 7) is whether the vendor's Codec2 component actually acts on KEY_PRIORITY specifically, versus KEY_OPERATING_RATE, once it arrives.

7. Validation checklist (do this before shipping)

  • Confirm on each target chipset family (Qualcomm, MediaTek, Exynos, Unisoc, etc.) — the load formula above is Qualcomm-specific; other vendors have different resource managers with different, likely undocumented, logic.
  • Watch adb logcat / kernel debug logs for the vendor's resource manager while creating decoders (ResourceManager, Codec2, OMX, vidc, "insufficient resource" strings) to see the actual summed load per instance and confirm it tracks KEY_PRIORITY/KEY_OPERATING_RATE as expected.
  • Confirm whether the vendor's Codec2 component actually applies a live KEY_PRIORITY change sent via setParameters() on an already-running codec. The framework-level mapping (CCodecConfig.cpp) does forward it, but component-side acceptance is vendor/implementation-specific — this determines whether an idle-pool decoder can be "promoted" via a live priority flip or needs a reconfigure/recreate step instead.
  • Verify actual decode is glitch-free (no dropped/late frames) the moment an idle, non-realtime instance is promoted and fed real frames at full operating rate — confirm the clock ramp-up doesn't introduce a startup stutter.
  • Re-run all of the above after any OEM firmware or Android version bump on your test devices; this is implementation-defined behavior, not a platform contract, and can silently change.

8. Caveats

  • Everything under Sections 3–4 beyond the generic MediaFormat key documentation is grounded in one Qualcomm kernel driver source tree (msm-vidc, Nougat-era omx_vdec / msm_vidc_common.c). Modern devices largely run Codec2 rather than OMX, and the exact parameter path from MediaFormat.KEY_PRIORITY down to is_realtime_session() on a current Codec2-based Qualcomm stack was not directly traced in this session — it needs on-device confirmation.

  • MediaTek: the mainline/upstream open-source MediaTek decoder driver (drivers/media/platform/mediatek/vcodec/, used mainly on ChromeOS devices — MT8173/8183/8186/8188/8192/8195) has no equivalent to Qualcomm's is_realtime_session()/MPS-load-formula anywhere in its structures (mtk_vcodec_dec_drv.h). Its resource model looks architecturally different: a fixed hardware-core count (MTK_VDEC_HW_MAX) with per-core mutexes (dec_mutex[]), not a summed numeric budget across instances. MediaTek's actual production Android phone Codec2 HAL (Dimensity/Helio) is closed-source and not published in AOSP, so its internal accounting can't be directly inspected the way Qualcomm's was.

    That said, AOSP's own SoC vendor dependencies for media resource manager documentation specifies OMX_IndexConfigPriority / C2_PARAMKEY_PRIORITY as a hook every SoC vendor — MediaTek included — is expected to implement for Android's media resource manager, backed by a CTS test that repeatedly allocates/configures/starts codecs until it catches OMX_ErrorInsufficientResources. So priority-based resource planning is a platform requirement, not a Qualcomm-only quirk. What's unconfirmed is whether MediaTek's internal accounting resembles Qualcomm's MPS-sum formula or something closer to the mainline driver's fixed-core model (which would behave differently under the "declare many non-realtime, promote one" pattern — worth testing directly on Dimensity/Helio hardware before relying on it).

    One more thing surfaced by that same AOSP doc, independent of the preload-pool question: it states priority defaults to non-realtime (1) unless explicitly configured to 0, and warns implementers not to assume realtime priority unless it's set. Section 5 confirmed ExoPlayer never sets KEY_PRIORITY at all — and Section 3.5 confirms, directly from msm_vdec.c, that Qualcomm's own kernel driver default agrees: the V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY control defaults to DISABLE, which clears VIDC_REALTIME. So on Qualcomm at least, an ExoPlayer decoder that never sets KEY_PRIORITY should land non-realtime by default at the driver level — though whether the vendor's Codec2/OMX HAL layer above the kernel driver preserves that default, rather than overriding it during its own init, remains unverified (that layer is closed-source).

  • Exynos, Unisoc, and other vendor resource managers were not investigated at all. Assume nothing about their behavior without separately confirming it.

  • This entire approach is an unofficial technique riding on vendor implementation details, not a documented Android platform capability. It should be validated per target device and treated as something that can regress on firmware updates.

9. Sources