feat(media_session): map Android PlaybackState 6 to buffering (t55)
The media session parser only understood states 2 (paused) and 3 (playing), so a session refilling its buffer (Android state 6, which the Shield's own ExoPlayer/MediaSession sessions report exactly like stock Android) read as "idle" or vanished into the foreground tier. MediaSessionInfo gains an independent `buffering` flag (playing/paused/ buffering are now three flags off one integer), the session sort puts buffering right behind playing — it is mid-playback, so it outranks a paused session when the ladder picks one — and the running-apps enrichment names a buffering live/last-known session "buffering" instead of falling into "idle". The MediaPlayerState stub gains BUFFERING and STANDBY so an in-suite reference to a member real HA does not have fails here (real HA 2026.10 exposes off/on/idle/playing/paused/standby/buffering).
This commit is contained in:
1 parent
49bc23ae34
commit
07682ff082
4 files changed
+121
-8
No files matched your search
@@ -17,6 +17,12 @@ class MediaSessionInfo:
|
||||
**device uptime-ms** timestamp (verified on Shield Android 11, R9), not a
|
||||
wallclock value. Use :meth:`position_at_uptime` with the device's current
|
||||
uptime to extrapolate the live position.
|
||||
|
||||
``playing``/``paused``/``buffering`` are independent flags mapped from the
|
||||
Android PlaybackState integer: 2 = paused, 3 = playing, 6 = buffering
|
||||
(t55). The Shield's own ExoPlayer/MediaSession sessions report state 6
|
||||
while buffering just as stock Android does, so no vendor-specific state
|
||||
integer is treated as buffering.
|
||||
"""
|
||||
|
||||
title: str | None = None
|
||||
@@ -25,6 +31,7 @@ class MediaSessionInfo:
|
||||
package: str | None = None
|
||||
playing: bool = False
|
||||
paused: bool = False
|
||||
buffering: bool = False
|
||||
position: float | None = None
|
||||
speed: float | None = None
|
||||
updated: int | None = None
|
||||
@@ -81,7 +88,8 @@ def parse_all_media_sessions(dump: str) -> list[MediaSessionInfo]:
|
||||
"""Parse `dumpsys media_session` output for ALL active media sessions.
|
||||
|
||||
Returns a list of every active session that has metadata or a known
|
||||
playback state, sorted: playing first, then paused, then others.
|
||||
playback state, sorted: playing first, then buffering, then paused, then
|
||||
others (t55).
|
||||
"""
|
||||
if not dump:
|
||||
return []
|
||||
@@ -89,7 +97,8 @@ def parse_all_media_sessions(dump: str) -> list[MediaSessionInfo]:
|
||||
sessions: list[MediaSessionInfo] = []
|
||||
current_pkg: str | None = None
|
||||
in_active = False
|
||||
current_state = 0 # 0=unknown, 2=paused, 3=playing
|
||||
# Android PlaybackState: 0=unknown, 2=paused, 3=playing, 6=buffering
|
||||
current_state = 0
|
||||
current_position: float | None = None
|
||||
current_speed: float | None = None
|
||||
current_updated: int | None = None
|
||||
@@ -138,6 +147,7 @@ def parse_all_media_sessions(dump: str) -> list[MediaSessionInfo]:
|
||||
info = MediaSessionInfo()
|
||||
info.package = current_pkg
|
||||
info.playing = current_state == 3
|
||||
info.buffering = current_state == 6
|
||||
info.paused = current_state == 2
|
||||
info.position = current_position
|
||||
info.speed = current_speed
|
||||
@@ -158,13 +168,16 @@ def parse_all_media_sessions(dump: str) -> list[MediaSessionInfo]:
|
||||
current_meta.package = current_pkg
|
||||
sessions.append(current_meta)
|
||||
|
||||
# Sort: playing first, then paused, then others
|
||||
# Sort: playing first, then buffering (mid-playback), then paused, then
|
||||
# others — a buffering session outranks a paused one (t55).
|
||||
def sort_key(s: MediaSessionInfo) -> int:
|
||||
if s.playing:
|
||||
return 0
|
||||
if s.paused:
|
||||
if s.buffering:
|
||||
return 1
|
||||
if s.paused:
|
||||
return 2
|
||||
return 3
|
||||
|
||||
sessions.sort(key=sort_key)
|
||||
return sessions
|
||||
@@ -206,8 +219,9 @@ def parse_media_session(
|
||||
Returns the best match from all active sessions:
|
||||
1. The session whose package matches the foreground app (if provided)
|
||||
2. The session that is currently playing (state=3)
|
||||
3. The session that is paused (state=2)
|
||||
4. Any active session with metadata
|
||||
3. The session that is buffering (state=6)
|
||||
4. The session that is paused (state=2)
|
||||
5. Any active session with metadata
|
||||
"""
|
||||
sessions = parse_all_media_sessions(dump)
|
||||
if not sessions:
|
||||
@@ -218,6 +232,5 @@ def parse_media_session(
|
||||
for s in sessions:
|
||||
if s.package == foreground_package:
|
||||
return s
|
||||
|
||||
# Already sorted: playing > paused > other
|
||||
# Already sorted: playing > buffering > paused > other
|
||||
return sessions[0]
|
||||
@@ -51,8 +51,11 @@ def running_apps_from_tasks(
|
||||
if session:
|
||||
if session.playing:
|
||||
app["state"] = "playing"
|
||||
elif session.buffering:
|
||||
app["state"] = "buffering"
|
||||
elif session.paused:
|
||||
app["state"] = "paused"
|
||||
app["state"] = "paused"
|
||||
else:
|
||||
app["state"] = "idle"
|
||||
if session.title:
|
||||
@@ -66,6 +69,8 @@ def running_apps_from_tasks(
|
||||
if cached is not None:
|
||||
if cached.playing:
|
||||
app["last_known_state"] = "playing"
|
||||
elif cached.buffering:
|
||||
app["last_known_state"] = "buffering"
|
||||
elif cached.paused:
|
||||
app["last_known_state"] = "paused"
|
||||
if cached.title:
|
||||
|
||||
@@ -79,6 +79,8 @@ class MediaPlayerState(enum.StrEnum):
|
||||
IDLE = "idle"
|
||||
PLAYING = "playing"
|
||||
PAUSED = "paused"
|
||||
STANDBY = "standby"
|
||||
BUFFERING = "buffering"
|
||||
|
||||
|
||||
class MediaPlayerEntityFeature(enum.IntFlag):
|
||||
|
||||
@@ -125,6 +125,76 @@ class TestPlaybackStatePosition:
|
||||
assert sessions[0].position is None
|
||||
|
||||
|
||||
class TestPlaybackStateBufferingT55:
|
||||
"""t55: Android PlaybackState 6 is BUFFERING, mapped in the parser.
|
||||
|
||||
The Shield's own sessions report the same integer stock Android does (6),
|
||||
so no vendor-specific variant is treated as buffering — a device variant
|
||||
would be a template field on the same PlaybackState line, never a
|
||||
different integer.
|
||||
"""
|
||||
|
||||
def _buffering_dump(self, state=6, speed="0.0"):
|
||||
return (
|
||||
"Sessions Stack - have 1 sessions:\n"
|
||||
" Some app session com.netflix.ninja/Session (userId=0)\n"
|
||||
" package=com.netflix.ninja\n"
|
||||
" active=true\n"
|
||||
f" state=PlaybackState {{state={state}, position=0, "
|
||||
f"buffered position=2000, speed={speed}, updated=900, actions=1, "
|
||||
"custom actions=[], active item id=0, error=null}\n"
|
||||
" metadata: size=2, description=Movie Title, Some Artist\n"
|
||||
)
|
||||
|
||||
def test_state_6_is_buffering(self):
|
||||
sessions = parse_all_media_sessions(self._buffering_dump())
|
||||
assert len(sessions) == 1
|
||||
session = sessions[0]
|
||||
assert session.buffering is True
|
||||
assert session.playing is False
|
||||
assert session.paused is False
|
||||
assert session.title == "Movie Title"
|
||||
|
||||
def test_state_3_and_2_stay_playing_and_paused(self):
|
||||
for state, attr in ((3, "playing"), (2, "paused")):
|
||||
sessions = parse_all_media_sessions(self._buffering_dump(state=state))
|
||||
session = sessions[0]
|
||||
assert session.buffering is False
|
||||
assert getattr(session, attr) is True
|
||||
|
||||
def test_state_6_wins_when_the_dump_says_speed_zero_too(self):
|
||||
"""The buffering signal is the integer, not the speed field."""
|
||||
sessions = parse_all_media_sessions(self._buffering_dump(state=6))
|
||||
assert sessions[0].buffering is True
|
||||
|
||||
def test_buffering_outranks_a_paused_session_in_the_pick_order(self):
|
||||
sessions = parse_all_media_sessions(
|
||||
self._buffering_dump()
|
||||
+ " Other app session com.example.player/Session (userId=0)\n"
|
||||
" package=com.example.player\n"
|
||||
" active=true\n"
|
||||
" state=PlaybackState {state=2, position=1, speed=0.0, "
|
||||
"updated=1, actions=1, custom actions=[], active item id=0, "
|
||||
"error=null}\n"
|
||||
" metadata: size=1, description=Paused Tune\n"
|
||||
)
|
||||
assert [s.package for s in sessions] == [
|
||||
"com.netflix.ninja",
|
||||
"com.example.player",
|
||||
]
|
||||
# and parse_media_session (no foreground preference) picks buffering
|
||||
dump = self._buffering_dump() + (
|
||||
" Other app session com.example.player/Session (userId=0)\n"
|
||||
" package=com.example.player\n"
|
||||
" active=true\n"
|
||||
" state=PlaybackState {state=2, position=1, speed=0.0, "
|
||||
"updated=1, actions=1, custom actions=[], active item id=0, "
|
||||
"error=null}\n"
|
||||
" metadata: size=1, description=Paused Tune\n"
|
||||
)
|
||||
assert parse_media_session(dump).package == "com.netflix.ninja"
|
||||
|
||||
|
||||
class TestAudioPlaybackSection:
|
||||
"""The "Audio playback (lastly played comes first)" section lists apps
|
||||
that recently played audio even with no MediaSession (R2)."""
|
||||
@@ -160,6 +230,29 @@ class TestAudioPlaybackSection:
|
||||
assert parse_audio_playback("") == []
|
||||
|
||||
|
||||
def test_running_apps_buffering_state_t55():
|
||||
"""t55: a buffering session (and a buffering last-known) is named buffering."""
|
||||
from nsh.ha_adb_common import MediaSessionInfo, running_apps_from_tasks
|
||||
|
||||
tasks = [
|
||||
{"package": "com.netflix.ninja"},
|
||||
{"package": "ar.tvplayer.tv"},
|
||||
]
|
||||
sessions = [MediaSessionInfo(package="com.netflix.ninja", buffering=True)]
|
||||
last_known = {
|
||||
"ar.tvplayer.tv": MediaSessionInfo(package="ar.tvplayer.tv", buffering=True)
|
||||
}
|
||||
|
||||
apps = running_apps_from_tasks(
|
||||
tasks, sessions, foreground_package=None, last_known=last_known
|
||||
)
|
||||
|
||||
netflix = next(a for a in apps if a["package"] == "com.netflix.ninja")
|
||||
tivi = next(a for a in apps if a["package"] == "ar.tvplayer.tv")
|
||||
assert netflix["state"] == "buffering"
|
||||
assert tivi["last_known_state"] == "buffering"
|
||||
|
||||
|
||||
def test_running_apps_last_known_enrichment():
|
||||
"""P3-3b: apps without a live session carry their TTL-capped last-known state."""
|
||||
from nsh.ha_adb_common import MediaSessionInfo, running_apps_from_tasks
|
||||
|
||||
Reference in new issue
Block a user