<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://blog.anthonykim.net/blog</id>
    <title>Pádraig Blog</title>
    <updated>2026-07-11T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://blog.anthonykim.net/blog"/>
    <subtitle>Pádraig Blog</subtitle>
    <icon>https://blog.anthonykim.net/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Preloading Multiple ExoPlayer Instances Without Exhausting the VPU's MPS Budget]]></title>
        <id>https://blog.anthonykim.net/blog/preloading-exo-player</id>
        <link href="https://blog.anthonykim.net/blog/preloading-exo-player"/>
        <updated>2026-07-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[TL;DR]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tldr">TL;DR<a href="https://blog.anthonykim.net/blog/preloading-exo-player#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<ul>
<li class=""><strong>The problem</strong>: preloading several <code>ExoPlayer</code> instances so playback can start instantly hits the VPU's shared decoder admission budget — creating too many <code>MediaCodec</code> hardware decoders concurrently gets rejected (<code>OMX_ErrorInsufficientResources</code> / <code>MediaCodec.CodecException</code>, ExoPlayer error codes 4001/4003) — even though only one video is ever actually playing at a time.</li>
<li class=""><strong>Why it happens</strong>: ExoPlayer sets <code>KEY_OPERATING_RATE</code> 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 <code>MediaFormat.KEY_PRIORITY</code> at all (confirmed absent from <code>MediaCodecRenderer.java</code>/<code>MediaCodecVideoRenderer.java</code>). 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 <code>KEY_PRIORITY</code> 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.)</li>
<li class=""><strong>The fix</strong>: explicitly set <code>KEY_PRIORITY = 1</code> (non-realtime) and a low <code>KEY_OPERATING_RATE</code> 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 <code>resolution × 1</code> instead of <code>resolution × fps</code> 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 <strong>30× difference in admission cost</strong> 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.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-the-problem">1. The problem<a href="https://blog.anthonykim.net/blog/preloading-exo-player#1-the-problem" class="hash-link" aria-label="Direct link to 1. The problem" title="Direct link to 1. The problem" translate="no">​</a></h2>
<p>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 <code>start()</code> a fully warm <code>MediaCodec</code> hardware decoder for every preloaded <code>ExoPlayer</code> 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.</p>
<p>The real optimization target is <strong>hardware decoder component init latency</strong> — creating, <code>configure()</code>-ing, and <code>start()</code>-ing a <code>MediaCodec</code> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-what-the-mps-limit-actually-is">2. What the "MPS limit" actually is<a href="https://blog.anthonykim.net/blog/preloading-exo-player#2-what-the-mps-limit-actually-is" class="hash-link" aria-label="Direct link to 2. What the &quot;MPS limit&quot; actually is" title="Direct link to 2. What the &quot;MPS limit&quot; actually is" translate="no">​</a></h2>
<p>There's no single standardized "MPS limit" in AOSP. Two different things are easy to conflate:</p>
<ul>
<li class=""><code>MediaCodecInfo.VideoCapabilities</code> (<code>getMacroblocksPerSecond()</code>, <code>isSizeAndRateSupported()</code>) describes what <strong>one</strong> codec instance can sustain. This is public API and standardized.</li>
<li class="">The <strong>shared, cross-instance VPU budget</strong> that rejects a new decoder once too many are open is enforced by the <strong>vendor's resource manager</strong> (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.</li>
</ul>
<p>Everything below was reverse-engineered from Qualcomm's <code>msm-vidc</code> kernel driver (the Venus/Iris video decode block used on Snapdragon devices). It is a case study, not a platform guarantee — see <a href="https://blog.anthonykim.net/blog/preloading-exo-player#8-caveats" class="">Section 8</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-the-core-mechanism-qualcomm-case-study">3. The core mechanism (Qualcomm case study)<a href="https://blog.anthonykim.net/blog/preloading-exo-player#3-the-core-mechanism-qualcomm-case-study" class="hash-link" aria-label="Direct link to 3. The core mechanism (Qualcomm case study)" title="Direct link to 3. The core mechanism (Qualcomm case study)" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="31-the-load-formula">3.1 The load formula<a href="https://blog.anthonykim.net/blog/preloading-exo-player#31-the-load-formula" class="hash-link" aria-label="Direct link to 3.1 The load formula" title="Direct link to 3.1 The load formula" translate="no">​</a></h3>
<p>From <code>msm_vidc_common.c</code> (<code>drivers/media/platform/msm/vidc/msm_vidc_common.c</code>):</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">msm_comm_get_mbs_per_sec</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">struct</span><span class="token plain"> </span><span class="token class-name">msm_vidc_inst</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">inst</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> output_port_mbs</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> capture_port_mbs</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> fps</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    output_port_mbs </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">NUM_MBS_PER_FRAME</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">prop</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">width</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">OUTPUT_PORT</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                                         inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">prop</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">height</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">OUTPUT_PORT</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    capture_port_mbs </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">NUM_MBS_PER_FRAME</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">prop</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">width</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">CAPTURE_PORT</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                                          inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">prop</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">height</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">CAPTURE_PORT</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">clk_data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">operating_rate </span><span class="token operator" style="color:#393A34">&gt;&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">16</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">prop</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">fps</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        fps </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">clk_data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">operating_rate </span><span class="token operator" style="color:#393A34">&gt;&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">16</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">?</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        fps </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">prop</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">fps</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">max</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">output_port_mbs</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> capture_port_mbs</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> fps</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p><code>NUM_MBS_PER_FRAME(w, h)</code> is <code>(w × h) / (16 × 16)</code> — macroblocks per frame. This per-instance value is summed across every open instance of a given type (<code>msm_comm_get_load()</code>, which iterates <code>core-&gt;instances</code> and adds <code>msm_comm_get_mbs_per_sec()</code> for each) and checked against a platform-defined <code>max_load</code> on codec creation and clock-scaling recalculation. That sum is the actual admission gate.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="32-what-key_operating_rate-really-does-to-that-formula">3.2 What <code>KEY_OPERATING_RATE</code> really does to that formula<a href="https://blog.anthonykim.net/blog/preloading-exo-player#32-what-key_operating_rate-really-does-to-that-formula" class="hash-link" aria-label="Direct link to 32-what-key_operating_rate-really-does-to-that-formula" title="Direct link to 32-what-key_operating_rate-really-does-to-that-formula" translate="no">​</a></h3>
<p>The critical detail: <code>fps</code> in the formula is <code>max(configured frame-rate, operating_rate)</code> — <strong>not</strong> the frame-rate alone. <code>KEY_FRAME_RATE</code> sets <code>inst-&gt;prop.fps</code> at <code>configure()</code> time; <code>KEY_OPERATING_RATE</code> sets <code>clk_data.operating_rate</code>.</p>
<p>This means declaring a low <code>KEY_FRAME_RATE</code> for idle/preloaded decoders and later raising <code>KEY_OPERATING_RATE</code> right before playback does <strong>not</strong> 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 <strong>sum across instances</strong>: as long as <code>(N−1 idle instances × low load) + (1 active instance × full load) ≤ max_load</code>, 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="33-realtime-vs-non-realtime-sessions--the-more-robust-lever">3.3 Realtime vs. non-realtime sessions — the more robust lever<a href="https://blog.anthonykim.net/blog/preloading-exo-player#33-realtime-vs-non-realtime-sessions--the-more-robust-lever" class="hash-link" aria-label="Direct link to 3.3 Realtime vs. non-realtime sessions — the more robust lever" title="Direct link to 3.3 Realtime vs. non-realtime sessions — the more robust lever" translate="no">​</a></h3>
<p>The same file defines a documented split for exactly this pattern, in a comment directly above the load-calc logic:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">                 | OPERATING RATE SET   | OPERATING RATE NOT SET |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">-----------------|----------------------|------------------------|</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">REALTIME         | load = res * op_rate |  load = res * fps      |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                 | clk  = res * op_rate |  clk  = res * fps      |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">-----------------|----------------------|------------------------|</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NON-REALTIME     | load = res * 1 fps   |  load = res * 1 fps    |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                 | clk  = res * op_rate |  clk  = res * fps      |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">-----------------|----------------------|------------------------|</span><br></div></code></pre></div></div>
<p>For a <strong>non-realtime</strong> session (<code>is_realtime_session(inst)</code> false, gated by the <code>LOAD_CALC_IGNORE_NON_REALTIME_LOAD</code> quirk), the admission <code>load</code> stays pinned near the bare macroblock count (effectively "1 fps") <strong>regardless of operating rate</strong>, while the <code>clk</code> (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 <code>KEY_OPERATING_RATE</code> 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="34-worked-example-four-concurrent-decoders">3.4 Worked example: four concurrent decoders<a href="https://blog.anthonykim.net/blog/preloading-exo-player#34-worked-example-four-concurrent-decoders" class="hash-link" aria-label="Direct link to 3.4 Worked example: four concurrent decoders" title="Direct link to 3.4 Worked example: four concurrent decoders" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="KEY_PRIORITY to is_realtime_session to VPU admission load diagram" src="https://blog.anthonykim.net/assets/images/key-priority-mps-diagram-8417b14ab1d93e50e22a90cb8aa51711.svg" width="1280" height="900" class="img_ev3q"></p>
<p>The diagram above traces the full path — <code>MediaFormat.KEY_PRIORITY</code> → <code>C2_PARAMKEY_PRIORITY</code> (Codec2 vendor HAL) → <code>is_realtime_session(inst)</code> (msm-vidc kernel driver) → which branch of the load formula from Section 3.3 applies — and then applies it to four example <code>MediaCodec</code> instances: three preloaded/idle decoders configured non-realtime (<code>KEY_PRIORITY = 1</code>), and one actively-playing decoder configured realtime (<code>KEY_PRIORITY = 0</code>, the default). All numbers are illustrative, not a real device's certified budget.</p>
<p>Each instance's <code>MPS</code> (macroblocks per second) is <code>NUM_MBS_PER_FRAME(w, h) × fps</code>, where <code>NUM_MBS_PER_FRAME</code> 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:</p>
<ul>
<li class=""><strong>Decoder A</strong> (non-realtime, idle): 1920×1088 → <code>120 × 68 = 8,160</code> mb/frame. Non-realtime forces <code>fps = 1</code> for the load calculation regardless of the real content frame rate, so <code>load = 8,160 × 1 = 8,160 MPS</code>.</li>
<li class=""><strong>Decoder B</strong> (non-realtime, idle): 1280×720 → <code>80 × 45 = 3,600</code> mb/frame. Same non-realtime rule: <code>load = 3,600 × 1 = 3,600 MPS</code>.</li>
<li class=""><strong>Decoder C</strong> (non-realtime, idle): same resolution as A → <code>load = 8,160 MPS</code>.</li>
<li class=""><strong>Decoder D</strong> (realtime, actively playing): 1920×1088 → <code>8,160</code> mb/frame. Realtime uses <code>fps = max(configured_fps, operating_rate) = max(30, 30) = 30</code>, so <code>load = 8,160 × 30 = 244,800 MPS</code> — roughly 30× more expensive than the same resolution sitting idle as non-realtime.</li>
</ul>
<p>Summed: <code>8,160 + 3,600 + 8,160 + 244,800 = 264,720 MPS</code>. Against the example <code>MAX_LOAD ≈ 300,000</code> 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 <code>KEY_PRIORITY</code> override at all, each still counted at its full configured/operating fps), the sum would be <code>8,160×30 + 3,600×30 + 8,160×30 + 8,160×30 = 842,400 MPS</code> — 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="35-qualcomms-actual-default-and-what-cts-actually-tests">3.5 Qualcomm's actual default, and what CTS actually tests<a href="https://blog.anthonykim.net/blog/preloading-exo-player#35-qualcomms-actual-default-and-what-cts-actually-tests" class="hash-link" aria-label="Direct link to 3.5 Qualcomm's actual default, and what CTS actually tests" title="Direct link to 3.5 Qualcomm's actual default, and what CTS actually tests" translate="no">​</a></h3>
<p>Two directly-grounded findings, closing gaps left open above.</p>
<p><strong>Qualcomm's driver-level default is non-realtime.</strong> In <code>msm_vdec.c</code>, the <code>V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY</code> control's <code>default_value</code> is <code>V4L2_MPEG_MSM_VIDC_DISABLE</code>, and the <code>DISABLE</code> case inside <code>msm_vdec_s_ctrl()</code> clears <code>VIDC_REALTIME</code> (<code>inst-&gt;flags &amp;= ~VIDC_REALTIME</code>). Since <code>is_realtime_session()</code> is <code>!!(inst-&gt;flags &amp; VIDC_REALTIME)</code> and <code>inst</code> starts zero-initialized, a session that never touches <code>KEY_PRIORITY</code> is non-realtime by default at the kernel driver level — cheap (<code>mb/frame × 1</code>) rather than expensive (<code>mb/frame × fps</code>). 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.</p>
<p><strong>What CTS actually tests isn't priority-specific MPS accounting — it's the resource-exhaustion contract.</strong> From AOSP's <a href="https://source.android.com/docs/core/media/soc" target="_blank" rel="noopener noreferrer" class="">SoC vendor dependencies for media resource manager</a>:</p>
<blockquote>
<p>An Android Compatibility Test Suite (CTS) test exists to allocate, configure and start each codec repeatedly until catching <code>OMX_ErrorInsufficientResources</code> (pass) or any other error (fail).</p>
</blockquote>
<p>There's no CTS assertion that a <code>KEY_PRIORITY = 1</code> 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 — <code>OMX_ErrorInsufficientResources</code>, or its Codec2 equivalent <code>C2_NO_MEMORY</code> — rather than hang, crash, or ANR. <code>CCodec.cpp</code> carries that status up to the Java layer as <code>MediaCodec.CodecException</code> (the <code>0xfffffff4</code> / <code>-12</code> / <code>ENOMEM</code> 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.</p>
<p>The practical CTS-relevant bar for <code>C2_PARAMKEY_PRIORITY</code> itself is just that a compliant component must accept <code>config()</code>/<code>query()</code> on it without error (no <code>C2_BAD_INDEX</code>/unsupported-param failure) — CTS doesn't verify what the component does with the value internally.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="36-the-actual-clkcore-admission-mechanism--and-why-its-realtime-aware-by-construction">3.6 The actual clk/core admission mechanism — and why it's realtime-aware by construction<a href="https://blog.anthonykim.net/blog/preloading-exo-player#36-the-actual-clkcore-admission-mechanism--and-why-its-realtime-aware-by-construction" class="hash-link" aria-label="Direct link to 3.6 The actual clk/core admission mechanism — and why it's realtime-aware by construction" title="Direct link to 3.6 The actual clk/core admission mechanism — and why it's realtime-aware by construction" translate="no">​</a></h3>
<p>A natural follow-up question: is a resource-exhaustion failure (<code>ENOMEM</code>/<code>OMX_ErrorInsufficientResources</code>) triggered by <code>load</code> (the MPS-sum admission gate from Section 3.1) or by <code>clk</code> (the clock-scaling value from Section 3.3's table)? The honest answer is that they aren't two separate mechanisms — <code>clk</code> is <code>load</code> converted into hardware cycles, and the actual hard-failure path lives in <code>msm_vidc_clocks.c</code>, in <code>msm_vidc_decide_core_and_power_mode()</code>:</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">current_inst_load </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> min_load </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> max_freq</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">clk_data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">core_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> min_core_id</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">current_inst_lp_load </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> min_load </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> max_freq</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">current_inst_lp_load </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> min_lp_load </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> max_freq</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    rc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">EINVAL</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">dprintk</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">VIDC_ERR</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Sorry ... Core Can't support this load\n"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> rc</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p><code>current_inst_load</code> is <code>msm_comm_get_inst_load(inst, ...) × vpp_cycles / work_route</code> — the same REALTIME/NON-REALTIME-gated <code>load</code> value from Section 3.3's table (res×fps or res×1), converted to cycles/sec via a codec-specific cycles-per-macroblock constant. <code>max_freq</code> is <code>allowed_clks_tbl[0].clock_rate</code> — the fastest clock rate the SoC's VPU cores can run at. Failing every branch above returns <code>-EINVAL</code> ("Core Can't support this load") — the mechanism behind the resource-exhaustion failures discussed in 3.5.</p>
<p>The side of the comparison that matters most for this document is how "how full is this core already" gets computed — <code>get_core_load(core, core_id, lp_mode, real_time)</code>, which filters:</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">real_time_mode </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> inst</span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain">flags </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain"> VIDC_REALTIME </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> true </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">real_time_mode </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> real_time</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">continue</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><code>msm_vidc_decide_core_and_power_mode()</code> always calls this with <code>real_time = true</code> — meaning <strong>only already-open realtime instances count toward how full a core is</strong> when deciding whether a new instance fits. Non-realtime instances are invisible to this sum entirely, and — because a non-realtime instance's own <code>current_inst_load</code> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-the-three-mediaformat-keys-involved">4. The three <code>MediaFormat</code> keys involved<a href="https://blog.anthonykim.net/blog/preloading-exo-player#4-the-three-mediaformat-keys-involved" class="hash-link" aria-label="Direct link to 4-the-three-mediaformat-keys-involved" title="Direct link to 4-the-three-mediaformat-keys-involved" translate="no">​</a></h2>
<table><thead><tr><th>Key</th><th>What it does</th><th>When settable</th></tr></thead><tbody><tr><td><code>KEY_FRAME_RATE</code></td><td>Baseline fps used in resource/MPS calculations; required for encoders, optional for decoders.</td><td><code>configure()</code> only</td></tr><tr><td><code>KEY_OPERATING_RATE</code></td><td>Hints the desired processing rate; drives clock scaling (and, per above, is folded into the load formula on Qualcomm).</td><td><code>configure()</code>, and confirmed dynamically adjustable later via <code>setParameters()</code></td></tr><tr><td><code>KEY_PRIORITY</code></td><td><code>0</code> = realtime priority (default), any nonzero value (conventionally <code>1</code>) = non-realtime/best-effort. Generic AOSP-level key, not vendor-specific — it's the documented framework signal that (on Qualcomm) maps to <code>is_realtime_session()</code>.</td><td><code>configure()</code>, <strong>and</strong> 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</td></tr></tbody></table>
<p>Recommended pattern given the driver behavior above: set <code>KEY_PRIORITY = 1</code> at <code>configure()</code> time for every decoder in your preload pool, and toggle <code>KEY_OPERATING_RATE</code> 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."</p>
<p><strong>Update on <code>KEY_PRIORITY</code> mutability</strong>: <code>frameworks/av/media/codec2/sfplugin/CCodecConfig.cpp</code> (the modern Codec2-based SDK→component mapping layer) maps <code>KEY_PRIORITY</code> to <code>C2_PARAMKEY_PRIORITY</code> with no domain restriction (<code>Domain::ALL</code>, which includes the <code>IS_PARAM</code> bit used for <code>setParameters()</code>), unlike <code>KEY_OPERATING_RATE</code>, which is explicitly restricted to <code>D::PARAM | D::CONFIG</code> and marked <code>// write-only</code>. In other words, the framework does not treat <code>KEY_PRIORITY</code> as configure-only — a live <code>setParameters()</code> 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 <code>KEY_PRIORITY</code> via <code>setParameters()</code> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-what-exoplayer--media3-does-out-of-the-box">5. What ExoPlayer / Media3 does out of the box<a href="https://blog.anthonykim.net/blog/preloading-exo-player#5-what-exoplayer--media3-does-out-of-the-box" class="hash-link" aria-label="Direct link to 5. What ExoPlayer / Media3 does out of the box" title="Direct link to 5. What ExoPlayer / Media3 does out of the box" translate="no">​</a></h2>
<p>Checked directly against the <code>androidx/media</code> source (<code>release</code> branch):</p>
<ul>
<li class=""><strong><code>MediaFormat.KEY_PRIORITY</code> is never set anywhere</strong> in <code>MediaCodecRenderer.java</code> (the shared base class for all codec-backed renderers) or <code>MediaCodecVideoRenderer.java</code>. Every codec ExoPlayer creates gets whatever the platform/vendor default priority is (typically realtime, <code>0</code>). ExoPlayer's own <code>rendererPriority</code> / <code>MSG_SET_PRIORITY</code> / <code>PriorityQueue</code> are an unrelated, ExoPlayer-internal renderer-scheduling concept — not the codec-level priority.</li>
<li class=""><strong><code>KEY_OPERATING_RATE</code> is actively managed.</strong> <code>MediaCodecVideoRenderer.getCodecOperatingRateV23()</code> computes it as the highest frame rate among the current adaptive stream formats × target playback speed. <code>MediaCodecRenderer</code> applies it at initial <code>configure()</code> and also updates it live later via <code>codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, newCodecOperatingRate)</code> — confirming operating rate really is adjustable post-configure through <code>setParameters()</code>, both per Android's own docs and in ExoPlayer's actual implementation.</li>
<li class=""><strong>The injection point for a custom <code>KEY_PRIORITY</code>/operating-rate policy already exists</strong>: <code>MediaCodecVideoRenderer.getMediaCodecConfiguration()</code> is <code>protected</code> and overridable, and ExoPlayer itself mutates the <code>MediaFormat</code> there before returning it (e.g. <code>maybeSetKeyAllowFrameDrop()</code> sets <code>KEY_ALLOW_FRAME_DROP</code>). A subclass can add <code>mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 1)</code> the same way.</li>
<li class=""><strong>Caveat</strong>: <code>getCodecOperatingRateV23()</code>'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-<code>configure()</code>. 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.</li>
</ul>
<p>Note: <code>Media3</code>'s <code>PreloadMediaSource</code> / <code>DefaultPreloadManager</code> (added ~1.4, extended through 1.8) is <strong>not</strong> a solution to this problem. It preloads <code>MediaSource</code> extraction/parsing/buffered sample data, and its shared <code>RenderersFactory</code> use is explicitly to check format <em>compatibility</em> "without creating the full renderers." It does not warm an actual <code>MediaCodec</code> decoder instance, so it doesn't touch decoder-init latency at all — the thing this whole exercise is trying to optimize.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-recommended-design">6. Recommended design<a href="https://blog.anthonykim.net/blog/preloading-exo-player#6-recommended-design" class="hash-link" aria-label="Direct link to 6. Recommended design" title="Direct link to 6. Recommended design" translate="no">​</a></h2>
<ol>
<li class="">Maintain a <strong>bounded pool</strong> of <code>ExoPlayer</code> instances (or raw <code>MediaCodec</code> decoders) sized to fit your target devices' real admission budget — not "as many as the user might preload."</li>
<li class="">Subclass <code>MediaCodecVideoRenderer</code> (via a custom <code>RenderersFactory</code>) to override:<!-- -->
<ul>
<li class=""><code>getMediaCodecConfiguration()</code> — inject <code>KEY_PRIORITY = 1</code> at <code>configure()</code> 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 <code>setParameters()</code> priority changes, you can instead toggle it dynamically alongside operating rate rather than only setting it once.</li>
<li class=""><code>getCodecOperatingRateV23()</code> — 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."</li>
</ul>
</li>
<li class="">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.</li>
<li class="">Don't rely on <code>KEY_FRAME_RATE</code> spoofing as the primary lever — treat <code>KEY_PRIORITY</code> (non-realtime) as the intended mechanism, and <code>KEY_OPERATING_RATE</code> as the real-time throughput control.</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="61-concrete-hook-points-onstarted--onstopped">6.1 Concrete hook points: <code>onStarted()</code> / <code>onStopped()</code><a href="https://blog.anthonykim.net/blog/preloading-exo-player#61-concrete-hook-points-onstarted--onstopped" class="hash-link" aria-label="Direct link to 61-concrete-hook-points-onstarted--onstopped" title="Direct link to 61-concrete-hook-points-onstarted--onstopped" translate="no">​</a></h3>
<p><code>BaseRenderer</code> (the root of ExoPlayer's renderer hierarchy) exposes <code>onStarted()</code> and <code>onStopped()</code> lifecycle callbacks, fired when a renderer transitions to/from actually being started (i.e. <code>playWhenReady</code> and the player is in <code>STATE_STARTED</code>). <code>MediaCodecRenderer.java</code> 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 <code>getMediaCodecConfiguration()</code> which only runs once at codec creation.</p>
<p><img decoding="async" loading="lazy" alt="MediaCodecVideoRenderer lifecycle: KEY_PRIORITY flips between idle and active playback" src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwMCA2NTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcm9sZT0iaW1nIj4KPHRpdGxlPk1lZGlhQ29kZWNWaWRlb1JlbmRlcmVyIGxpZmVjeWNsZTogS0VZX1BSSU9SSVRZIGZsaXBzIGJldHdlZW4gaWRsZSBhbmQgYWN0aXZlIHBsYXliYWNrPC90aXRsZT4KPGRlc2M+RGlhZ3JhbSBzaG93aW5nIGEgY3VzdG9tIE1lZGlhQ29kZWNWaWRlb1JlbmRlcmVyIHN1YmNsYXNzIHRoYXQgc2V0cyBLRVlfUFJJT1JJVFk9MSAobm9uLXJlYWx0aW1lKSBhdCBjb25maWd1cmUgdGltZSB2aWEgZ2V0TWVkaWFDb2RlY0NvbmZpZ3VyYXRpb24sIHRoZW4gcHJvbW90ZXMgdG8gS0VZX1BSSU9SSVRZPTAgKHJlYWx0aW1lKSB2aWEgb25TdGFydGVkIHdoZW4gcGxheWJhY2sgYmVnaW5zLCBhbmQgZGVtb3RlcyBiYWNrIHRvIEtFWV9QUklPUklUWT0xIHZpYSBvblN0b3BwZWQgd2hlbiBwbGF5YmFjayBlbmRzLCByaWRpbmcgRXhvUGxheWVyJ3MgZXhpc3RpbmcgTVNHX1NFVF9DT0RFQ19QQVJBTUVURVJTIHRvIGNvZGVjLnNldFBhcmFtZXRlcnMgcGlwZWxpbmUuPC9kZXNjPgo8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI2NTAiIGZpbGw9IiNmZmZmZmYiLz4KCjx0ZXh0IHg9IjYwMCIgeT0iMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjIyIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWYyNDMwIj5SZWNvbW1lbmRlZCBEZXNpZ246IFByaW9yaXR5IEZsaXBzIFdpdGggUGxheWJhY2sgU3RhdGU8L3RleHQ+Cjx0ZXh0IHg9IjYwMCIgeT0iNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNWI2NDcyIj5DdXN0b20gTWVkaWFDb2RlY1ZpZGVvUmVuZGVyZXIgc3ViY2xhc3Mg4oCUIGhvb2tzIGNvbmZpcm1lZCBpbiBhbmRyb2lkeC9tZWRpYSBzb3VyY2UgKFNlY3Rpb24gNSk8L3RleHQ+Cgo8IS0tIENvbmZpZ3VyZSBib3ggLS0+CjxyZWN0IHg9IjYwIiB5PSI2NiIgd2lkdGg9IjMyMCIgaGVpZ2h0PSI3NiIgcng9IjEwIiBmaWxsPSIjZWVmMWY1IiBzdHJva2U9IiNjM2M5ZDMiLz4KPHRleHQgeD0iMjIwIiB5PSI4NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9Ii1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksUm9ib3RvLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzViNjQ3MiI+TWVkaWFDb2RlY1ZpZGVvUmVuZGVyZXIgc3ViY2xhc3M8L3RleHQ+Cjx0ZXh0IHg9IjIyMCIgeT0iMTA4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLFNGTW9uby1SZWd1bGFyLE1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMS41IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWYyNDMwIj5nZXRNZWRpYUNvZGVjQ29uZmlndXJhdGlvbigpPC90ZXh0Pgo8dGV4dCB4PSIyMjAiIHk9IjEyNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9InVpLW1vbm9zcGFjZSxTRk1vbm8tUmVndWxhcixNZW5sbyxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxZjI0MzAiPuKGkiBzZXRJbnRlZ2VyKEtFWV9QUklPUklUWSwgMSk8L3RleHQ+Cgo8cGF0aCBkPSJNMjIwIDE0MiBWMTY4IiBzdHJva2U9IiM4YTk0YTMiIHN0cm9rZS13aWR0aD0iMS42IiBtYXJrZXItZW5kPSJ1cmwoI2Fycm93RykiLz4KCjwhLS0gU3RhdGUgMTogaWRsZSAoZXZlbiBuYXJyb3dlcikgLS0+CjxyZWN0IHg9IjYwIiB5PSIxNzAiIHdpZHRoPSIzMjAiIGhlaWdodD0iMjUwIiByeD0iMTIiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iIzRmN2ZjMCIgc3Ryb2tlLXdpZHRoPSIxLjYiLz4KPHJlY3QgeD0iNzYiIHk9IjE4NCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSIyNCIgcng9IjEyIiBmaWxsPSIjZTNlZGZiIi8+Cjx0ZXh0IHg9IjE1MSIgeT0iMjAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxSb2JvdG8sc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJmNWY5ZSI+SURMRSDCtyBQUkVMT0FERUQ8L3RleHQ+Cjx0ZXh0IHg9Ijc4IiB5PSIyNDAiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE2IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWYyNDMwIj5SZW5kZXJlciBub3Qgc3RhcnRlZDwvdGV4dD4KPHRleHQgeD0iNzgiIHk9IjI2NiIgZm9udC1mYW1pbHk9InVpLW1vbm9zcGFjZSxTRk1vbm8tUmVndWxhcixNZW5sbyxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTMuNSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFmMjQzMCI+UFJJT1JJVFkgPSAxIChub24tcmVhbHRpbWUpPC90ZXh0Pgo8dGV4dCB4PSI3OCIgeT0iMjg4IiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLFNGTW9uby1SZWd1bGFyLE1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMi41IiBmaWxsPSIjM2E0MTUwIj5PUEVSQVRJTkdfUkFURSA9IGxvdyAoMeKAkzUpPC90ZXh0Pgo8bGluZSB4MT0iNzgiIHkxPSIzMDIiIHgyPSIzNjIiIHkyPSIzMDIiIHN0cm9rZT0iI2U0ZTdlYyIvPgo8dGV4dCB4PSI3OCIgeT0iMzIyIiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLFNGTW9uby1SZWd1bGFyLE1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMS41IiBmb250LXN0eWxlPSJpdGFsaWMiIGZpbGw9IiM0YTY2OTAiPmlzX3JlYWx0aW1lX3Nlc3Npb24gPSBmYWxzZTwvdGV4dD4KPHRleHQgeD0iNzgiIHk9IjM0NCIgZm9udC1mYW1pbHk9InVpLW1vbm9zcGFjZSxTRk1vbm8tUmVndWxhcixNZW5sbyxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMxZjI0MzAiPmxvYWQg4omIIG1iL2ZyYW1lIMOXIDE8L3RleHQ+Cjx0ZXh0IHg9Ijc4IiB5PSIzNjYiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXN0eWxlPSJpdGFsaWMiIGZpbGw9IiM1YjY0NzIiPmNoZWFwIOKAlCBzZWUgU2VjdGlvbiAzLjQ8L3RleHQ+Cgo8IS0tIFN0YXRlIDI6IGFjdGl2ZSAoZXZlbiBuYXJyb3dlcikgLS0+CjxyZWN0IHg9IjgyMCIgeT0iMTcwIiB3aWR0aD0iMzIwIiBoZWlnaHQ9IjI1MCIgcng9IjEyIiBmaWxsPSIjZmZmYWYzIiBzdHJva2U9IiNkOThhMmIiIHN0cm9rZS13aWR0aD0iMiIvPgo8cmVjdCB4PSI4MzYiIHk9IjE4NCIgd2lkdGg9IjE2MCIgaGVpZ2h0PSIyNCIgcng9IjEyIiBmaWxsPSIjZmRlY2Q4Ii8+Cjx0ZXh0IHg9IjkxNiIgeT0iMjAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxSb2JvdG8sc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2I1NmExMiI+4pa2IEFDVElWRUxZIFBMQVlJTkc8L3RleHQ+Cjx0ZXh0IHg9IjgzOCIgeT0iMjQwIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxSb2JvdG8sc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNiIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFmMjQzMCI+UmVuZGVyZXIgc3RhcnRlZCAocGxheWluZyk8L3RleHQ+Cjx0ZXh0IHg9IjgzOCIgeT0iMjY2IiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLFNGTW9uby1SZWd1bGFyLE1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMy41IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWYyNDMwIj5QUklPUklUWSA9IDAgKHJlYWx0aW1lKTwvdGV4dD4KPHRleHQgeD0iODM4IiB5PSIyODgiIGZvbnQtZmFtaWx5PSJ1aS1tb25vc3BhY2UsU0ZNb25vLVJlZ3VsYXIsTWVubG8sbW9ub3NwYWNlIiBmb250LXNpemU9IjEyLjUiIGZpbGw9IiMzYTQxNTAiPk9QRVJBVElOR19SQVRFID0gcmVhbCBmcHM8L3RleHQ+CjxsaW5lIHgxPSI4MzgiIHkxPSIzMDIiIHgyPSIxMTIyIiB5Mj0iMzAyIiBzdHJva2U9IiNlZGRhYzAiLz4KPHRleHQgeD0iODM4IiB5PSIzMjIiIGZvbnQtZmFtaWx5PSJ1aS1tb25vc3BhY2UsU0ZNb25vLVJlZ3VsYXIsTWVubG8sbW9ub3NwYWNlIiBmb250LXNpemU9IjExLjUiIGZvbnQtc3R5bGU9Iml0YWxpYyIgZmlsbD0iIzhhNjEzNSI+aXNfcmVhbHRpbWVfc2Vzc2lvbiA9IHRydWU8L3RleHQ+Cjx0ZXh0IHg9IjgzOCIgeT0iMzQ0IiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLFNGTW9uby1SZWd1bGFyLE1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzFmMjQzMCI+bG9hZCA9IG1iL2ZyYW1lIMOXIGZwczwvdGV4dD4KPHRleHQgeD0iODM4IiB5PSIzNjYiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXN0eWxlPSJpdGFsaWMiIGZpbGw9IiM4YTYxMzUiPmZ1bGwgY29zdCDigJQgc2VlIFNlY3Rpb24gMy40PC90ZXh0PgoKPCEtLSBQcm9tb3RlIGFycm93ICsgbGFiZWwgKG11Y2ggd2lkZXIgZ2FwIG5vdzogMzgwIHRvIDgyMCkgLS0+Cjx0ZXh0IHg9IjYwMCIgeT0iMTkyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxSb2JvdG8sc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2I1NmExMiI+UFJPTU9URTwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSIyMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiM4YTYxMzUiPnNldFBsYXlXaGVuUmVhZHkodHJ1ZSkgLyBzZWVrIGhlcmU8L3RleHQ+Cjx0ZXh0IHg9IjYwMCIgeT0iMjMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLFNGTW9uby1SZWd1bGFyLE1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjOGE2MTM1Ij5SZW5kZXJlci5zdGFydCgpIOKGkiBvblN0YXJ0ZWQoKTwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSIyNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJ1aS1tb25vc3BhY2UsU0ZNb25vLVJlZ3VsYXIsTWVubG8sbW9ub3NwYWNlIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiM4YTYxMzUiPnNldFBhcmFtZXRlcnMoUFJJT1JJVFk9MCwgT1BfUkFURT1mcHMpPC90ZXh0Pgo8cGF0aCBkPSJNMzg0IDI2NiBIODEyIiBzdHJva2U9IiNkOThhMmIiIHN0cm9rZS13aWR0aD0iMiIgbWFya2VyLWVuZD0idXJsKCNhcnJvd0FtYmVyMikiLz4KCjwhLS0gRGVtb3RlIGFycm93ICsgbGFiZWwgLS0+CjxwYXRoIGQ9Ik04MTYgMzE0IEgzODgiIHN0cm9rZT0iIzRmN2ZjMCIgc3Ryb2tlLXdpZHRoPSIyIiBtYXJrZXItZW5kPSJ1cmwoI2Fycm93Qmx1ZTIpIi8+Cjx0ZXh0IHg9IjYwMCIgeT0iMzM2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxSb2JvdG8sc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJmNWY5ZSI+REVNT1RFPC90ZXh0Pgo8dGV4dCB4PSI2MDAiIHk9IjM1NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9Ii1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksUm9ib3RvLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzRhNjY5MCI+cGxheWJhY2sgZW5kcyAvIHN3aXBlZCBhd2F5PC90ZXh0Pgo8dGV4dCB4PSI2MDAiIHk9IjM3NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9InVpLW1vbm9zcGFjZSxTRk1vbm8tUmVndWxhcixNZW5sbyxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzRhNjY5MCI+UmVuZGVyZXIuc3RvcCgpIOKGkiBvblN0b3BwZWQoKTwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSIzOTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJ1aS1tb25vc3BhY2UsU0ZNb25vLVJlZ3VsYXIsTWVubG8sbW9ub3NwYWNlIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiM0YTY2OTAiPnNldFBhcmFtZXRlcnMoUFJJT1JJVFk9MSwgT1BfUkFURT1sb3cpPC90ZXh0PgoKPCEtLSBFeGlzdGluZyBwaXBlbGluZSBub3RlIC0tPgo8cmVjdCB4PSI5MCIgeT0iNDUwIiB3aWR0aD0iMTAyMCIgaGVpZ2h0PSIxMTgiIHJ4PSIxMCIgZmlsbD0iI2Y2ZjdmOSIgc3Ryb2tlPSIjYzNjOWQzIiBzdHJva2UtZGFzaGFycmF5PSI0LDMiLz4KPHRleHQgeD0iNjAwIiB5PSI0NzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWYyNDMwIj5UaGlzIGFscmVhZHkgcmlkZXMgYW4gZXhpc3RpbmcgRXhvUGxheWVyIHBpcGVsaW5lPC90ZXh0Pgo8dGV4dCB4PSI2MDAiIHk9IjQ5NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9InVpLW1vbm9zcGFjZSxTRk1vbm8tUmVndWxhcixNZW5sbyxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMxZjI0MzAiPk1lZGlhQ29kZWNSZW5kZXJlci5oYW5kbGVNZXNzYWdlKE1TR19TRVRfQ09ERUNfUEFSQU1FVEVSUyk8L3RleHQ+Cjx0ZXh0IHg9IjYwMCIgeT0iNTE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0idWktbW9ub3NwYWNlLFNGTW9uby1SZWd1bGFyLE1lbmxvLG1vbm9zcGFjZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzFmMjQzMCI+4oaSIGNvZGVjLnNldFBhcmFtZXRlcnMoYWN0aXZlQ29kZWNQYXJhbWV0ZXJzLnRvQnVuZGxlKCkpPC90ZXh0Pgo8dGV4dCB4PSI2MDAiIHk9IjUzNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9Ii1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksUm9ib3RvLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtc3R5bGU9Iml0YWxpYyIgZmlsbD0iIzViNjQ3MiI+Q29uZmlybWVkIGluIE1lZGlhQ29kZWNSZW5kZXJlci5qYXZhIOKAlCBhbHJlYWR5IHVzZWQgdG8gcHVzaCBLRVlfT1BFUkFUSU5HX1JBVEUgbGl2ZTs8L3RleHQ+Cjx0ZXh0IHg9IjYwMCIgeT0iNTUxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxSb2JvdG8sc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC1zdHlsZT0iaXRhbGljIiBmaWxsPSIjNWI2NDcyIj5leHRlbmQgdGhlIHNhbWUgQnVuZGxlIHRvIGFsc28gY2FycnkgS0VZX1BSSU9SSVRZIGZvciB0aGUgcHJvbW90ZS9kZW1vdGUgY2FsbHMgYWJvdmUuPC90ZXh0PgoKPHRleHQgeD0iNjAwIiB5PSI1OTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLFNlZ29lIFVJLFJvYm90byxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOWMyYjFmIj5WZXJpZnkgcGVyIHRhcmdldCB2ZW5kb3Igd2hldGhlciBhIGxpdmUgS0VZX1BSSU9SSVRZIGNoYW5nZSB2aWEgc2V0UGFyYW1ldGVycygpIGlzIGhvbm9yZWQgd2hpbGUgcnVubmluZyAoU2VjdGlvbiA3KS48L3RleHQ+Cjx0ZXh0IHg9IjYwMCIgeT0iNjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxSb2JvdG8sc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzljMmIxZiI+SWYgbm90IGhvbm9yZWQsIGZhbGwgYmFjayB0byBhIHRlYXJkb3duL3JlY29uZmlndXJlIHN0ZXAgd2hlbiBwcm9tb3RpbmcvZGVtb3RpbmcgaW5zdGVhZCBvZiBhIGxpdmUgZmxpcC48L3RleHQ+Cgo8ZGVmcz4KICA8bWFya2VyIGlkPSJhcnJvd0ciIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPgogICAgPHBhdGggZD0iTTAgMCBMMTAgNSBMMCAxMCB6IiBmaWxsPSIjOGE5NGEzIi8+CiAgPC9tYXJrZXI+CiAgPG1hcmtlciBpZD0iYXJyb3dBbWJlcjIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPgogICAgPHBhdGggZD0iTTAgMCBMMTAgNSBMMCAxMCB6IiBmaWxsPSIjZDk4YTJiIi8+CiAgPC9tYXJrZXI+CiAgPG1hcmtlciBpZD0iYXJyb3dCbHVlMiIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI5IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNyIgbWFya2VySGVpZ2h0PSI3IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSI+CiAgICA8cGF0aCBkPSJNMCAwIEwxMCA1IEwwIDEwIHoiIGZpbGw9IiM0ZjdmYzAiLz4KICA8L21hcmtlcj4KPC9kZWZzPgo8L3N2Zz4K" width="1200" height="650" class="img_ev3q"></p>
<p>As the diagram shows: <code>getMediaCodecConfiguration()</code> sets <code>KEY_PRIORITY = 1</code> once, at <code>configure()</code> time, before the decoder ever plays anything. Overriding <code>onStarted()</code> is where you promote — call <code>codec.setParameters()</code> with <code>KEY_PRIORITY = 0</code> and the real operating rate right as the renderer actually starts pushing frames. Overriding <code>onStopped()</code> is the mirror — drop back to <code>KEY_PRIORITY = 1</code> and a low operating rate the moment the renderer stops being active (paused, ended, or the user swipes to a different preloaded item).</p>
<p>This isn't a new mechanism to build from scratch: <code>MediaCodecRenderer.java</code> already has a live parameter-update pipeline wired up — <code>handleMessage(MSG_SET_CODEC_PARAMETERS)</code> calls <code>codec.setParameters(activeCodecParameters.toBundle())</code> — which is exactly how ExoPlayer pushes <code>KEY_OPERATING_RATE</code> changes to a running codec today. Extending the same <code>Bundle</code> to also carry <code>KEY_PRIORITY</code> from your <code>onStarted()</code>/<code>onStopped()</code> 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 <code>KEY_PRIORITY</code> specifically, versus <code>KEY_OPERATING_RATE</code>, once it arrives.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="7-validation-checklist-do-this-before-shipping">7. Validation checklist (do this before shipping)<a href="https://blog.anthonykim.net/blog/preloading-exo-player#7-validation-checklist-do-this-before-shipping" class="hash-link" aria-label="Direct link to 7. Validation checklist (do this before shipping)" title="Direct link to 7. Validation checklist (do this before shipping)" translate="no">​</a></h2>
<ul>
<li class="">Confirm on each <strong>target chipset family</strong> (Qualcomm, MediaTek, Exynos, Unisoc, etc.) — the load formula above is Qualcomm-specific; other vendors have different resource managers with different, likely undocumented, logic.</li>
<li class="">Watch <code>adb logcat</code> / kernel debug logs for the vendor's resource manager while creating decoders (<code>ResourceManager</code>, <code>Codec2</code>, <code>OMX</code>, <code>vidc</code>, "insufficient resource" strings) to see the actual summed load per instance and confirm it tracks <code>KEY_PRIORITY</code>/<code>KEY_OPERATING_RATE</code> as expected.</li>
<li class="">Confirm whether the vendor's Codec2 <strong>component</strong> actually applies a live <code>KEY_PRIORITY</code> change sent via <code>setParameters()</code> on an already-running codec. The framework-level mapping (<code>CCodecConfig.cpp</code>) 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.</li>
<li class="">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.</li>
<li class="">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.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="8-caveats">8. Caveats<a href="https://blog.anthonykim.net/blog/preloading-exo-player#8-caveats" class="hash-link" aria-label="Direct link to 8. Caveats" title="Direct link to 8. Caveats" translate="no">​</a></h2>
<ul>
<li class="">
<p>Everything under Sections 3–4 beyond the generic <code>MediaFormat</code> key documentation is grounded in one Qualcomm kernel driver source tree (<code>msm-vidc</code>, Nougat-era <code>omx_vdec</code> / <code>msm_vidc_common.c</code>). Modern devices largely run Codec2 rather than OMX, and the exact parameter path from <code>MediaFormat.KEY_PRIORITY</code> down to <code>is_realtime_session()</code> on a current Codec2-based Qualcomm stack was not directly traced in this session — it needs on-device confirmation.</p>
</li>
<li class="">
<p><strong>MediaTek:</strong> the mainline/upstream open-source MediaTek decoder driver (<code>drivers/media/platform/mediatek/vcodec/</code>, used mainly on ChromeOS devices — MT8173/8183/8186/8188/8192/8195) has no equivalent to Qualcomm's <code>is_realtime_session()</code>/MPS-load-formula anywhere in its structures (<code>mtk_vcodec_dec_drv.h</code>). Its resource model looks architecturally different: a fixed hardware-core count (<code>MTK_VDEC_HW_MAX</code>) with per-core mutexes (<code>dec_mutex[]</code>), 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.</p>
<p>That said, AOSP's own <a href="https://source.android.com/docs/core/media/soc" target="_blank" rel="noopener noreferrer" class="">SoC vendor dependencies for media resource manager</a> documentation specifies <code>OMX_IndexConfigPriority</code> / <code>C2_PARAMKEY_PRIORITY</code> as a hook <strong>every</strong> 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 <code>OMX_ErrorInsufficientResources</code>. 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).</p>
<p>One more thing surfaced by that same AOSP doc, independent of the preload-pool question: it states priority defaults to non-realtime (<code>1</code>) unless explicitly configured to <code>0</code>, and warns implementers not to assume realtime priority unless it's set. Section 5 confirmed ExoPlayer never sets <code>KEY_PRIORITY</code> at all — and Section 3.5 confirms, directly from <code>msm_vdec.c</code>, that Qualcomm's own kernel driver default agrees: the <code>V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY</code> control defaults to <code>DISABLE</code>, which clears <code>VIDC_REALTIME</code>. So on Qualcomm at least, an ExoPlayer decoder that never sets <code>KEY_PRIORITY</code> 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).</p>
</li>
<li class="">
<p>Exynos, Unisoc, and other vendor resource managers were not investigated at all. Assume nothing about their behavior without separately confirming it.</p>
</li>
<li class="">
<p>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.</p>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="9-sources">9. Sources<a href="https://blog.anthonykim.net/blog/preloading-exo-player#9-sources" class="hash-link" aria-label="Direct link to 9. Sources" title="Direct link to 9. Sources" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://github.com/realme-kernel-opensource/realme5-kernel-source/blob/master/drivers/media/platform/msm/vidc/msm_vidc_common.c" target="_blank" rel="noopener noreferrer" class="">msm_vidc_common.c — realme5-kernel-source (Qualcomm msm-vidc driver)</a></li>
<li class=""><a href="https://android.googlesource.com/platform/frameworks/av/+/master/media/codec2/sfplugin/CCodecConfig.cpp" target="_blank" rel="noopener noreferrer" class="">CCodecConfig.cpp — platform/frameworks/av (AOSP, SDK↔Codec2 key mapping)</a></li>
<li class=""><a href="https://android.googlesource.com/platform/frameworks/av/+/master/media/codec2/sfplugin/CCodecConfig.h" target="_blank" rel="noopener noreferrer" class="">CCodecConfig.h — platform/frameworks/av (AOSP, Domain bitmask definitions)</a></li>
<li class=""><a href="https://developer.android.com/reference/android/media/MediaFormat" target="_blank" rel="noopener noreferrer" class="">MediaFormat — Android Developers reference</a></li>
<li class=""><a href="https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/media/java/android/media/MediaFormat.java" target="_blank" rel="noopener noreferrer" class="">MediaFormat.java — platform/frameworks/base (AOSP)</a></li>
<li class=""><a href="https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java" target="_blank" rel="noopener noreferrer" class="">MediaCodecVideoRenderer.java — androidx/media (release branch)</a></li>
<li class=""><a href="https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java" target="_blank" rel="noopener noreferrer" class="">MediaCodecRenderer.java — androidx/media (release branch)</a></li>
<li class=""><a href="https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java" target="_blank" rel="noopener noreferrer" class="">MediaCodecInfo.java — androidx/media (release branch)</a></li>
<li class=""><a href="https://android-developers.googleblog.com/2025/09/introducing-preloading-with-media3.html" target="_blank" rel="noopener noreferrer" class="">Elevating media playback: Introducing preloading with Media3 — Part 1 (Android Developers Blog)</a></li>
<li class=""><a href="https://android-developers.googleblog.com/2025/09/a-deep-dive-into-media3-preloadmanager.html" target="_blank" rel="noopener noreferrer" class="">Elevating media playback: A deep dive into Media3's PreloadManager — Part 2 (Android Developers Blog)</a></li>
<li class=""><a href="https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java" target="_blank" rel="noopener noreferrer" class="">PreloadMediaSource.java — androidx/media (release branch)</a></li>
<li class=""><a href="https://github.com/torvalds/linux/blob/master/drivers/media/platform/mediatek/vcodec/decoder/mtk_vcodec_dec_drv.h" target="_blank" rel="noopener noreferrer" class="">mtk_vcodec_dec_drv.h — torvalds/linux mainline (MediaTek decoder driver)</a></li>
<li class=""><a href="https://source.android.com/docs/core/media/soc" target="_blank" rel="noopener noreferrer" class="">SoC vendor dependencies for media resource manager — Android Open Source Project</a></li>
<li class=""><a href="https://github.com/realme-kernel-opensource/realme5-kernel-source/blob/master/drivers/media/platform/msm/vidc/msm_vdec.c" target="_blank" rel="noopener noreferrer" class="">msm_vdec.c — realme5-kernel-source (Qualcomm msm-vidc driver, V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY control default)</a></li>
<li class=""><a href="https://android.googlesource.com/platform/frameworks/av/+/master/media/codec2/sfplugin/CCodec.cpp" target="_blank" rel="noopener noreferrer" class="">CCodec.cpp — platform/frameworks/av (AOSP, Codec2↔MediaCodec status/error mapping)</a></li>
<li class=""><a href="https://github.com/realme-kernel-opensource/realme5-kernel-source/blob/master/drivers/media/platform/msm/vidc/msm_vidc_clocks.c" target="_blank" rel="noopener noreferrer" class="">msm_vidc_clocks.c — realme5-kernel-source (Qualcomm msm-vidc driver, per-core clock/load admission — <code>msm_vidc_decide_core_and_power_mode()</code>, <code>get_core_load()</code>)</a></li>
</ul>]]></content>
        <author>
            <name>Austin Kim</name>
            <uri>https://blog.anthonykim.net/</uri>
        </author>
        <category label="MediaCodec" term="MediaCodec"/>
        <category label="AOSP" term="AOSP"/>
        <category label="ExoPlayer" term="ExoPlayer"/>
        <category label="video" term="video"/>
        <category label="VPU" term="VPU"/>
        <category label="MPS" term="MPS"/>
    </entry>
</feed>