Quality-of-life additions for the GL.iNet GLKVM web interface — speaker/mic device selection, Picture-in-Picture, live data totals, and a tidier toolbar.
A single, dependency-free MV3 content script for Chromium browsers (Chrome / Edge / Brave).
The GLKVM web UI is great, but it always uses your system's default speaker and microphone, has no Picture-in-Picture, and the toolbar takes up space. Its frontend is closed-source (shipped as a minified bundle), so these gaps can't be patched directly. This extension layers the missing controls on top of the live page without touching its code.
| Feature | How it works | |
|---|---|---|
| 🔊 | Speaker picker | Route KVM audio to any output device via setSinkId, re-applied whenever audio (re)starts. |
| 🎤 | Microphone picker | Forces the chosen input by hooking getUserMedia; hot-swaps the live track if the mic is already streaming. |
| 🖼️ | Picture-in-Picture | Pops the remote screen into a floating, always-on-top window — with a "Bring video back" overlay so you can exit PiP from the otherwise-black page. |
| 📊 | Data totals | Shows total downloaded / uploaded for the session, read from WebRTC getStats(), next to the bitrate. |
| 🧹 | Auto-collapse toolbar | Collapses the top toolbar on load and centers the expand handle. |
| 🔈 | Speaker / mic on by default | Optionally enable the remote speaker and/or microphone automatically when the page loads. |
| ⌨️ | Capture keyboard shortcuts | Uses the Keyboard Lock API in fullscreen to forward Cmd+R/W/T and other shortcuts to the remote instead of the local browser. |
| 🖥️ | Status bar in fullscreen | Keeps the bottom bar (speaker, mic, stats) and toolbar visible in fullscreen by intercepting GLKVM's fullscreen button and suppressing its fullscreenchange handler. |
| 🎨 | Active-state icon | The toolbar icon shows in color only on tabs where the extension is running, and stays monotone everywhere else. |
| ⚙️ | Options page | Toggle each behavior on/off; preferences sync across your Chrome profile. |
The device pickers appear as small chevrons next to the existing speaker/mic icons in the bottom status bar, plus a Picture-in-Picture button:
The session data totals sit inline with the stream info:
This is an unpacked extension (not on the Web Store).
- Download or clone this repository.
- Open
chrome://extensions(oredge://extensions). - Enable Developer mode.
- Click Load unpacked and select the project folder.
- Open / refresh your GLKVM tab. On first use, click a device chevron and allow microphone access once so Chrome reveals device names.
Running GLKVM as a window? ⋮ → Cast, Save, and Share → Create Shortcut… → "Open as window" gives an app-like window. The content script injects there too.
By default the script runs on glkvm.local, any Tailscale (*.ts.net) host, and the GL.iNet remote-access domain (*.glkvm.com). If you reach your KVM another way (LAN IP, custom hostname), add it to content_scripts[0].matches in manifest.json:
"matches": [
"https://glkvm.local/*",
"https://*.ts.net/*",
"https://*.glkvm.com/*",
"https://192.168.1.50/*"
]Then reload the extension.
Click the toolbar icon to open the settings popup. Each feature can be toggled independently:
Display
- Collapse toolbar by default
- Show network consumption
- Show status bar in fullscreen — keeps the bottom bar and toolbar visible while in fullscreen
Audio
- Enable speaker by default
- Enable microphone by default (off by default — turning this on prompts for mic access and starts capturing on every load)
Keyboard
- Capture keyboard shortcuts — in fullscreen, sends Cmd+R/W/T and similar shortcuts to the remote instead of the local browser (uses the Keyboard Lock API; Cmd+Q and Cmd+Tab remain OS-reserved)
Settings are stored in chrome.storage.sync and apply on the next page load; the network-stats toggle applies live. The popup talks to chrome.storage, while the main script runs in the page's MAIN world (no chrome.* access) — a small isolated-world bridge mirrors the settings between them.
The script runs in the page's MAIN world at document_start, so it is in place before the SPA initializes WebRTC:
- Wraps
navigator.mediaDevices.getUserMediato inject the selected microphonedeviceId. - Wraps the
RTCPeerConnectionconstructor to track connections (the KVM video stream is receive-only, soaddTrackis never called) — used for live mic swapping and reading transfer stats. - Manipulates the existing
<video>/<audio>elements directly for output routing and PiP. - Re-injects its UI via a
MutationObserverif the Vue-rendered status bar re-mounts.
Selections persist in localStorage. No data leaves your browser — there are no network requests, analytics, or external dependencies, and the extension requests no special permissions beyond the host match.
- Chromium browsers (Chrome 111+, Edge, Brave): full support — MV3
world: "MAIN"content scripts are required. - Safari: the script logic is portable, but Safari needs an Xcode wrapper and its
setSinkIdsupport is limited; not packaged here.
- Output (speaker) selection requires the page to be actively playing audio; it's applied on the next
playevent. - Microphone selection applies live if a mic is already active, otherwise on the next time the mic is enabled.
- Data totals reflect the current WebRTC connection and reset on reconnect.
Not affiliated with or endorsed by GL.iNet. "GLKVM" is a trademark of its respective owner.


