Flow
Pinning the player to the TV
1Grant one permission
The helper lets the page choose speakers and never gives it the microphone.
2Find the TV
Refuse the generic default output and require exactly one output with the TV's name.
3Bind before play
Every play waits until the video is bound to the TV with setSinkId.
4Ignore device changes
The player's reactions to device changes and media keys are switched off.
5Keep the backup
Built-in speakers stay muted, and the helper finds the TV and speakers by saved IDs instead of listing every device.
Every time a pair of wireless earbuds woke up near the box behind my TV, my code and the operating system fought over where the sound should go. My code kept losing.
The box is a small computer that plays movies in a browser on the TV. My rule is that sound comes out of the TV and nowhere else. Not the computer's speakers, and not someone's earbuds.
First try: guard the system setting
The computer has one system-wide setting for where sound goes. When I pressed Play, my helper set it to the TV, muted the built-in speakers, and started a guard that checked it a couple of times a second. The player had a short lease the helper kept renewing. If anything looked wrong, the lease stopped and the player paused and muted until the next Play. I also told the guard to switch back to the TV if earbuds grabbed the setting.
That worked until the earbuds and the guard started racing.
The race
The guard read the list of devices, then read the setting. If earbuds connected between those two reads, the list didn't have them but the setting pointed at them. The guard thought the route was broken and stopped the movie for good, until someone pressed Play.
I patched it a few times. Read the setting before and after the list, and retry if it moved. Skip a device that vanished mid-read, but only if it's really gone. Let the guard fix a setting that was briefly empty, as long as the speakers were muted. Each patch was right, and each added more reads, which meant more gaps to lose in.
Second try: pin the player
So I stopped guarding the setting. Browsers let a page pick the output for each video, so the player now picks the TV and ignores the system setting completely:
That one call is all the player needs to say where its sound goes.
The helper gives the page one permission, choosing speakers, and never the microphone. The page refuses the generic "default" output and needs exactly one output with the TV's name. Every play waits until the video is bound to the TV. The player's reactions to device changes and media keys are switched off, so earbuds can't pause it either. The speakers stay muted as a backup. And the helper now looks up the TV and speakers by saved IDs instead of checking every device, since reading each device's name and properties is what stalls while earbuds connect.
My favorite test keeps firing device changes and keeps trying to move the movie to the earbuds. The movie never stops and never leaves the TV. That same change deleted most of my race patches.
Not tried on real earbuds yet
I've only tested this with fake devices, not a real unplug with real earbuds. It protects one app. Anything else on the box that makes sound has only the speaker mute standing between it and the room.
Two follow-up fixes
Two follow-up commits cleaned up the edges. The new guard replaces an old one that might have already muted the movie, and it could have kept that mute as if I'd chosen it. Now it lets the old guard save the real state first. Then some browser builds rejected an ID they had handed out themselves, which killed the permission setup. The helper now records which step failed and retries once without that ID.