Follow-up: a second, distinct webOS audio-selection bug — merged multi-version items
This is a different bug with a different trigger that lives in the same function area (HTMLPlayer._selectAudioTrack). I instrumented it end-to-end the same way — Chrome DevTools on the live app.plex.tv/tv-v5-webos build plus the Plex DB — and traced it to the exact call. The prior bug was about language tags; this one is about merged Versions, and it can short-circuit the language logic entirely.
Setup: LG 49SM9000PLA, webOS 4.10.0; PMS 1.43.2.10687; LG Plex app cdp-30 5.2.1. Repro content: Spider-Noir S01 as a merged multi-version item — each episode has two release files combined into one entry: a FLUX version (1 audio track, English) and a B&W version (2 audio tracks, Italian + English).
In plain English (for anyone hitting this)
If you have two copies of the same movie or episode in Plex and Plex has merged them into a single entry with multiple “Versions,” the audio picker can stop working on one of those copies. You open the audio menu, choose Italian, the menu shows Italian selected — but the sound stays English. It never actually changes.
Why it happens: when you play a merged item, the app needs a list of “which audio tracks this file has.” Because of a bug, it reads that list from the wrong copy — the first/default Version — instead of the copy you’re actually watching. The two copies have a different number of audio tracks (here 1 vs 2), the lists don’t line up, and the app gives up and just plays the default (English) track no matter what you pick.
It’s not your TV’s fault, and it’s not a sound-file problem. Single-copy items switch audio fine in both directions. It only happens on merged items.
Workaround
- Remove the extra version from the library so the item has only one Version.
- Disable “Allow Direct Play” (or lower max quality) so the server transcodes to a single audio track — the app can’t pick the wrong index. Same universal workaround as before.
For Plex devs — full technical detail
Trigger: a merged multi-version metadata_item whose played version has a different audio-stream count than version index 0. Test item: FLUX = 1 audio (en/eac3); B&W = 2 audio (it+en, native element order [en, it]).
Symptom: Direct-Playing the B&W (2-track) version, the picker is ignored — audio stays pinned to English:
Unexpected number of audio tracks available on media element. Found: 2, expected: 1
Proof the native API is fine: in DevTools, audioTracks[1].enabled = true on the <video> element switched the decoder to Italian instantly — no reload, no transcode. The native list [en, it] is correct; only the index the app computes is wrong.
Root cause. _selectAudioTrack (HTMLPlayer.tsx, module 29074) builds its server-stream list from the default version, not the playing one:
_selectAudioTrack() {
const tracks = this.mediaElement?.audioTracks; // native AudioTrackList — correct: [en, it]
const playable = this._playable;
if (!tracks?.length || !playable?.isDirectPlay
|| playable.playableType !== PlayableType.Video) return;
// BUG: no media-version index passed → reads version 0's streams (the FLUX copy)
const serverStreams = getStreamsOfType(playable.metadataItem, StreamType.Audio); // = (0,f.bp)(...)
if (!serverStreams.length) return;
const elementTracks = [...tracks];
if (elementTracks.length === 1) { elementTracks[0].enabled = true; return; }
const idx = this._getSelectedAudioStreamIndex
? this._getSelectedAudioStreamIndex(serverStreams, elementTracks)
: serverStreams.findIndex(s => s.selected);
elementTracks.forEach((t, i) => { t.enabled = (i === idx); });
}
grep:
var i=(0,f.bp)(n.metadataItem,m.jx.Audio);
f.bp (f = n(90606)) is the shared helper D:
// f.bp — "get streams of a type from an item"
function getStreamsOfType(item, streamType, mediaIndex = 0) { // ← 3rd arg, defaults to 0
if (!item.mediaItems) return [];
const version = item.mediaItems[mediaIndex]; // version 0 unless told otherwise
if (!version?.parts) return [];
return (version.parts[0].streams || []).filter(s => s.streamType === streamType);
}
verbatim:
function D(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if(!e.mediaItems)return[];var i=e.mediaItems[n];return null!=i&&i.parts?(i.parts[0].streams||[]).filter(function(e){return e.streamType===t}):[]}
With no index it returns mediaItems[0] = FLUX (1 audio) while the element plays mediaItems[1] = B&W (2 audio) — hence expected: 1 against a 2-track file. (metadataItem is the full multi-version array, the same one appInitializer indexes via mediaItems[t.mediaIndex].)
Downstream (stock code). Lengths differ → getSelectedAudioStreamIndex (getAppConfig.ts, the #939218 function) short-circuits:
function getSelectedAudioStreamIndex(serverStreams, elementTracks) {
const r = serverStreams.findIndex(s => s.selected);
if (serverStreams.length !== elementTracks.length) {
log.warn(`Unexpected number of audio tracks... Found: ${elementTracks.length}, expected: ${serverStreams.length}`);
return r; // ← BUG PATH (1 !== 2): skips the language match + mirror below
}
const aligned = serverStreams.every((s, i) => { // normal path — only when lists line up
const elemLang = elementTracks[i].language;
if (!(s.languageCode || (elemLang && elemLang !== "und"))) return true;
return s.languageCode === elemLang
|| (!!s.languageCode && (s.languageTag || isoMap[s.languageCode]) === elemLang);
});
return aligned ? r : elementTracks.length - r - 1; // ← the #939218 mirror lives here
}
So audioTracks[r].enabled → ~track 0 = English, regardless of the pick. The language/mirror code never runs — the defect is the wrong input handed upstream.
Proposed fix — source from the playing version (playable.decision.choice.{mediaIndex,part}, already read in PlaybackSessionController/appInitializer):
const part = playable.decision?.choice?.part;
const serverStreams = part?.streams
? part.streams.filter(s => s.id != null && s.streamType === StreamType.Audio) // same predicate as PSC._getStreamsOfType
: getStreamsOfType(playable.metadataItem, StreamType.Audio); // fallback
Minified drop-in:
var p=n.decision&&n.decision.choice&&n.decision.choice.part;
var i=p&&p.streams?p.streams.filter(function(e){return null!=e.id&&e.streamType===m.jx.Audio}):(0,f.bp)(n.metadataItem,m.jx.Audio);
(Or minimal — pass the index: (0,f.bp)(n.metadataItem,m.jx.Audio,(n.decision&&n.decision.choice?n.decision.choice.mediaIndex:0)||0).) After it, lists match and the existing language path runs — also re-enabling the en-US/#939218 handling the mismatch was short-circuiting.
TL;DR: _selectAudioTrack reads the audio list from version 0 instead of the playing version; source it from playable.decision.choice.part.streams.