Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -599,15 +599,14 @@
"plugin_path": "plugins/ledmatrix-music",
"stars": 0,
"downloads": 0,
"last_updated": "2026-10-02",
"last_updated": "2026-10-03",
"verified": true,
"screenshot": "",
"latest_version": "1.5.4",
"latest_version": "1.5.5",
"ledmatrix_min_version": "2.0.0",
"aliases": [
"ledmatrix-music"
],
"commit": "886d2038d22a35c962beac0ea261543339cffec9"
]
},
{
"id": "news",
Expand Down
11 changes: 11 additions & 0 deletions plugins/ledmatrix-music/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [1.5.5] - 2026-10-03

### Fixed
- Scrolling no longer stutters while YouTube Music is playing. websocket-client
validates every text frame's UTF-8 in pure Python (unless wsaccel is
installed), holding the GIL; the companion pushes its whole player state
every few seconds, and on a 512x64 Pi 4 that check was ~16% of the display
process's GIL time and stalled ~8% of scroll frames (0.6% without it). The
socket now skips it via `websocket_extra_options`; the payload is still
decoded and parsed as JSON, which rejects anything malformed.

## [1.5.4] - 2026-10-02

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion plugins/ledmatrix-music/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ledmatrix-music",
"name": "Music Player - Now Playing",
"version": "1.5.4",
"version": "1.5.5",
"description": "Real-time now playing display for Spotify and YouTube Music with album art, scrolling text, and progress bars",
"author": "ChuckBuilds",
"entry_point": "manager.py",
Expand Down Expand Up @@ -65,6 +65,12 @@
}
],
"versions": [
{
"version": "1.5.5",
"released": "2026-10-03",
"ledmatrix_min_version": "2.0.0",
"notes": "Scrolling no longer stutters while YouTube Music is playing. The YTM connection checked every incoming message's text in slow pure-Python code, blocking the display for tens of milliseconds every few seconds; it now skips that check (the message is still parsed as JSON, which rejects anything malformed)."
},
{
"version": "1.5.4",
"released": "2026-10-02",
Expand Down
42 changes: 42 additions & 0 deletions plugins/ledmatrix-music/test_ytm_skip_utf8_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Tests that the YTM websocket skips websocket-client's UTF-8 validation.

Regression under test: without wsaccel, websocket-client validates every text
frame's UTF-8 byte by byte in pure Python while holding the GIL. The YTM
Companion pushes its full player state every few seconds while music plays;
on a 512x64 Pi 4 rig that validation took ~16% of the display process's GIL
time and stalled ~8% of scroll frames (0.6% without it), visible as the
scroll pausing and jumping.

Run: <core-venv>/bin/python plugins/ledmatrix-music/test_ytm_skip_utf8_validation.py
"""

import sys
from pathlib import Path

PLUGIN_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(PLUGIN_DIR))

try:
import socketio # noqa: F401
except ImportError:
print("SKIP: python-socketio not installed (see requirements.txt)")
sys.exit(2)

import ytm_client # noqa: E402


def test_websocket_is_opened_without_utf8_validation():
client = ytm_client.YTMClient()
options = client.sio.eio.websocket_extra_options
assert options.get('skip_utf8_validation') is True, options


def main():
test_websocket_is_opened_without_utf8_validation()
print("OK")
return 0


if __name__ == "__main__":
sys.exit(main())
9 changes: 9 additions & 0 deletions plugins/ledmatrix-music/ytm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,19 @@ def __init__(self, update_callback=None, logger=None):
# class had no say in. connect_client() owns retry timing; the poll
# loop calls it, and the backoff applies uniformly whether the
# connection never came up or came up and went away.
#
# skip_utf8_validation: websocket-client checks every text frame's
# UTF-8 in pure Python unless wsaccel is installed, holding the GIL
# throughout. The companion pushes its whole player state every few
# seconds while music plays, and on a Pi 4 that check was ~16% of the
# display process's GIL time and stalled ~8% of scroll frames. The
# payload is decoded and parsed as JSON right after, which rejects
# anything malformed anyway.
self.sio = socketio.Client(
logger=False,
engineio_logger=False,
reconnection=False,
websocket_extra_options={'skip_utf8_validation': True},
)
self.last_known_track_data = None
self.is_connected = False
Expand Down
Loading