Skip to content

fix(upnp): stop the volume overlay jumping on hardware key presses - #256

Open
tbrackbill wants to merge 1 commit into
dddevid:masterfrom
tbrackbill:fix/upnp-hardware-volume
Open

tbrackbill wants to merge 1 commit into
dddevid:masterfrom
tbrackbill:fix/upnp-hardware-volume

Conversation

@tbrackbill

@tbrackbill tbrackbill commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Problem

With a DLNA renderer connected, the Android volume overlay jumped a second after every hardware
volume key press.

Android answers a volume key by showing an optimistic currentVolume ± 1 for about a second
before the real value arrives. The VolumeProvider was published with maxVolume 100 while each
press adjusted by 5, so the optimistic value moved by 1 and the real one by 5.

A second problem: the handler added 5 to whatever percentage the renderer last reported, so an
off-step value stayed off-step. upmpdcli reported 33%, and presses went 28, 23, 28, 33 while the
system slider showed 35%.

Fix

  • Publish the provider on a scale of one unit per step (0–20), so Android's ±1 is exactly the
    change we make. Everything the handler passes to callers stays in UPnP percent; only the scale
    published to the media session changes.
  • Step in provider units, so the first press lands on the slider's scale and the renderer and
    slider agree from then on.
  • Document why direction 0 is ignored: Android sends ADJUST_SAME on key-up, and acting on it sent
    a redundant SetVolume.

Trade-off: dragging the system volume slider now moves in 5% steps (21 stops) instead of 1%.
This applies to Chromecast too, since it shares setRemotePlayback. It is inherent to the fix:
Android's ±1 only matches our adjustment if one provider unit is one step.

Testing

  • 8 new unit tests for the percent/unit conversions and step behaviour. The instance methods are
    gated on Platform.isAndroid, so the wiring was checked on device instead.
  • flutter test: master's 118 passing tests plus the 8 new ones pass. The 13 tests that fail on
    master fail the same way here, and there are no new analyzer issues.
  • Pixel 7 Pro + upmpdcli, on a build with this and my other pending UPnP fixes:
    on every key press the renderer's GetVolume equals the provider value × 5, and an external
    33% snaps to 30/35 on the next press.
  • The overlay itself can't be observed through dumpsys (the optimistic value lives inside
    MediaSessionRecord), so the scale relationship is pinned by the unit tests.

Independent of the other UPnP PRs; they merge cleanly in any order.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Android remote volume now changes in consistent 5% steps across the full 0–100% range.
    • When adjusting a volume that falls between steps, it first rounds to the nearest 5% increment, then applies the change.
    • Volume updates continue to be reported as percentages, with adjustments kept within the 0–100% range.

Android answers a hardware volume key by setting MediaSessionRecord's
mOptimisticVolume to currentVolume ± 1 and displaying that for one second
before the real value arrives. The VolumeProvider was published with
maxVolume 100 while each press adjusted by 5, so the optimistic value moved
by 1 and ours by 5. A second after every press the system volume overlay
visibly jumped from one to the other.

Publish the provider on a scale of one unit per step instead, so Android's
±1 is exactly the adjustment we make and there is nothing to correct.
Everything the handler hands to callers stays in UPnP percent; only the
scale published to the media session changes.

Verified against a Pixel 7 Pro and upmpdcli: dumpsys media_session reported
volumeType=REMOTE, max=100 before this change. Note that the overlay itself
is not observable through dumpsys — mOptimisticVolume lives inside
MediaSessionRecord and is never published to the provider value — so the
scale relationship is pinned by unit tests rather than by instrumentation.

Also documents why direction 0 is ignored: Android sends ADJUST_SAME on
key-up, and acting on it issued a redundant SOAP SetVolume for no change.

The same applies to the percentage the renderer last reported. The handler
used to add 5 to it, so an off-step value (upmpdcli reported 33%) stayed
off-step on every press: 28, 23, 28, 33, while the system slider showed
round(x/5), i.e. 35%. Presses now step in provider units, so the first one
lands on the slider's scale and the renderer and slider agree from then on.

Trade-off: dragging the system volume slider now moves in 5% steps (21
stops) rather than 1%. This applies to Chromecast as well, which shares
setRemotePlayback. It is inherent to the fix: Android's optimistic ±1 only
matches our adjustment if one provider unit is one step. The unit tests
cover the percent/unit conversions; the instance methods are gated on
Platform.isAndroid, so the wiring is covered by the device checks above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a3b710f3-9a74-4383-88a1-bfa8383097ba

📥 Commits

Reviewing files that changed from the base of the PR and between 44726ed and b517d79.

📒 Files selected for processing (2)
  • lib/services/audio_handler.dart
  • test/services/audio_handler_remote_volume_test.dart

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

Remote Android volume now uses provider units on a 0–20 scale. Conversion helpers map between those units and percentages. Playback updates, callbacks, and hardware adjustments use the scaled values.

Changes

Remote volume scaling

Layer / File(s) Summary
Provider-unit conversion and publication
lib/services/audio_handler.dart
Conversions map between percentages and provider units. Remote playback information publishes the provider maximum and current volume in provider units. Volume callbacks convert provider units to percentages.
Hardware adjustment and validation
lib/services/audio_handler.dart, test/services/audio_handler_remote_volume_test.dart
Hardware adjustments operate on rounded provider units. Tests cover conversion, rounding, clamping, and adjustment boundaries.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: dddevid

Merge Risk: ⚪ Minimal · up to b517d

No concrete issue remains that requires a fix before merging. The handler-level test coverage question remains open.

Security Architecture Review

Security architecture risk: 🔵 Low · up to b517d

The new scale aligns hardware-key presses with five-percent volume steps. Callers still receive percentages, and the reviewed paths show no new access route. Android callback behavior and rapid or failed remote updates remain less fully verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed Android-facing scale affects volume control for the active remote playback path, including UPnP and Chromecast. The reviewed callback path does not grant access to another renderer or service.

Trust Boundaries and Controls

  • observed — The handler retains its active-remote-playback checks and clamps values at the Android-provider-to-percentage boundary. The downstream consumer retains its existing connected-service selection.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preventing the UPnP volume overlay from jumping during hardware key presses. It is concise, specific, and consistent with the pull request objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant