Skip to content

Commit 196e627

Browse files
chore(docs): update code docs
1 parent 72bfd7a commit 196e627

9 files changed

Lines changed: 135 additions & 28 deletions

docs/components_ItemGrid_LoadVideoContentTask.bs.html

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@
358358
return
359359
end if
360360

361-
addAudioStreamsToVideo(video)
361+
addAudioStreamsToVideo(video, meta, mediaSourceId)
362362

363363
audioIndexList = []
364364
for each audioTrackEntry in video.fullAudioData
@@ -540,18 +540,48 @@
540540
end if
541541
end sub
542542

543-
' addAudioStreamsToVideo: Add audio stream data to video
543+
' addAudioStreamsToVideo: Populate video.fullAudioData with the item's audio tracks.
544544
'
545-
' @param {dynamic} video component to add fullAudioData to
546-
sub addAudioStreamsToVideo(video)
547-
audioStreams = []
548-
mediaStreams = m.playbackInfo.MediaSources[0].MediaStreams
549-
550-
for i = 0 to mediaStreams.Count() - 1
551-
if LCase(mediaStreams[i].Type) = "audio"
552-
audioStreams.push(mediaStreams[i])
545+
' Sources from the original item metadata (meta.mediaSourcesData) — NOT from the
546+
' PlaybackInfo response. The /PlaybackInfo response can return a transcode-shaped
547+
' MediaSource carrying only the chosen audio stream, which would shrink the OSD picker
548+
' to a single entry (issue #500). The OSD picker must always list every original track
549+
' so the user can switch.
550+
'
551+
' @param {object} video - video AA receiving the fullAudioData field
552+
' @param {dynamic} meta - item metadata (from ItemMetaData) holding mediaSourcesData
553+
' @param {dynamic} mediaSourceId - chosen MediaSource ID (matches one entry under meta.mediaSourcesData.mediaSources)
554+
sub addAudioStreamsToVideo(video as object, meta as dynamic, mediaSourceId as dynamic)
555+
mediaStreams = invalid
556+
557+
if isValid(meta) and isValid(meta.mediaSourcesData) and isValid(meta.mediaSourcesData.mediaSources)
558+
sources = meta.mediaSourcesData.mediaSources
559+
if isValidAndNotEmpty(mediaSourceId)
560+
for each source in sources
561+
if isValid(source) and source.Id = mediaSourceId and isValid(source.MediaStreams)
562+
mediaStreams = source.MediaStreams
563+
exit for
564+
end if
565+
end for
553566
end if
554-
end for
567+
if not isValid(mediaStreams) and isValid(sources[0]) and isValid(sources[0].MediaStreams)
568+
mediaStreams = sources[0].MediaStreams
569+
end if
570+
end if
571+
572+
' Defensive fallback if metadata is unavailable (shouldn't happen — ItemMetaData ran above)
573+
if not isValid(mediaStreams) and isValid(m.playbackInfo) and isValid(m.playbackInfo.MediaSources) and isValid(m.playbackInfo.MediaSources[0])
574+
mediaStreams = m.playbackInfo.MediaSources[0].MediaStreams
575+
end if
576+
577+
audioStreams = []
578+
if isValid(mediaStreams)
579+
for i = 0 to mediaStreams.Count() - 1
580+
if LCase(mediaStreams[i].Type) = "audio"
581+
audioStreams.push(mediaStreams[i])
582+
end if
583+
end for
584+
end if
555585

556586
video.fullAudioData = audioStreams
557587
end sub

docs/components_video_VideoPlayerView.bs.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -607,20 +607,25 @@
607607
' Event handler for when audioIndex changes.
608608
' For direct play: switches audio track directly using Roku's availableAudioTracks
609609
' (no stop/reload needed since all tracks are in the container).
610-
' For transcoded: must reload since the server needs to re-encode with the selected track.
610+
' For transcoded — or when the newly-selected stream exceeds the device's audio
611+
' capability (e.g. switching from stereo back to an 8ch track on a stereo Roku) —
612+
' must reload so the server transcodes the new selection.
611613
sub onAudioIndexChange()
612614
m.log.info("Audio index changed by user", "newIndex", m.top.audioIndex, "isTranscoded", m.top.isTranscoded, "position", m.top.position, "currentAudioTrack", m.top.audioTrack)
613615

614-
' Direct play: all audio tracks are in the container — just switch the Roku track
615-
if not m.top.isTranscoded and isValid(m.top.availableAudioTracks)
616+
' Direct play: all audio tracks are in the container — just switch the Roku track,
617+
' but only if the newly-selected stream is actually decodable by the device.
618+
' Roku's native audioTrack switch can't make an undecodable track playable, so
619+
' picking e.g. an 8ch track on a stereo device must go through the reload path.
620+
if not m.top.isTranscoded and isValid(m.top.availableAudioTracks) and isSelectedAudioStreamDirectPlayable(m.top.audioIndex, m.top.fullAudioData)
616621
newTrack = getRokuAudioTrackPosition(m.top.audioIndex, m.top.fullAudioData, m.top.availableAudioTracks)
617622
m.log.info("Direct play audio switch", "jellyfinIndex", m.top.audioIndex, "rokuTrack", newTrack)
618623
m.top.audioTrack = newTrack
619624
reportPlayback()
620625
return
621626
end if
622627

623-
' Transcoded or availableAudioTracks not available: must reload
628+
' Transcoded, availableAudioTracks not available, or stream not direct-playable: must reload
624629
m.log.info("Audio change requires reload", "isTranscoded", m.top.isTranscoded)
625630

626631
' Save the current video position

docs/data/search.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-LoadVideoContentTask.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-VideoPlayerView.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-misc.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-streamSelection.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/source_utils_misc.bs.html

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -799,16 +799,18 @@
799799
' and also counts all track types, so Roku Track = Jellyfin Index + 1.
800800
'
801801
' When availableAudioTracks is provided (populated by Roku during active playback),
802-
' matches by language for robustness against non-standard MKV track numbering.
803-
' Falls back to Index + 1 mapping when availableAudioTracks is not available
804-
' (e.g., before playback starts or for non-MKV containers).
802+
' matches by language with ordinal disambiguation: among same-language Jellyfin streams
803+
' (in original order), find the position N of the chosen index, then return the Nth
804+
' same-language Roku track. This avoids returning the wrong track when multiple streams
805+
' share a language (e.g. main + commentary, both English — issue #500).
806+
' Falls back to Index + 1 mapping when availableAudioTracks is not available or no
807+
' ordinal match can be found (e.g., before playback starts or for non-MKV containers).
805808
'
806809
' @param {integer} jellyfinAudioIndex - The Jellyfin audio stream index
807810
' @param {dynamic} audioStreamsArray - Jellyfin fullAudioData array (must have .index and .language fields)
808811
' @param {dynamic} [availableAudioTracks=invalid] - Roku's availableAudioTracks array (has .track and .language)
809812
' @returns {string} - Roku track identifier string for audioTrack field
810813
function getRokuAudioTrackPosition(jellyfinAudioIndex as integer, audioStreamsArray as dynamic, availableAudioTracks = invalid as dynamic) as string
811-
' When Roku's availableAudioTracks is populated, match by language for accuracy
812814
if isValid(availableAudioTracks) and isValid(audioStreamsArray)
813815
' Find the language of the selected Jellyfin stream
814816
selectedLanguage = ""
@@ -822,13 +824,29 @@
822824
end for
823825

824826
if selectedLanguage <> ""
825-
for each rokuTrack in availableAudioTracks
826-
if isValid(rokuTrack.track) and isValid(rokuTrack.language)
827-
if LCase(rokuTrack.language) = selectedLanguage
828-
return rokuTrack.track
827+
' Position of the chosen stream among same-language Jellyfin streams
828+
jfPosition = -1
829+
jfSeen = 0
830+
for each stream in audioStreamsArray
831+
if isValid(stream.language) and LCase(stream.language) = selectedLanguage
832+
if isValid(stream.index) and stream.index = jellyfinAudioIndex
833+
jfPosition = jfSeen
834+
exit for
829835
end if
836+
jfSeen += 1
830837
end if
831838
end for
839+
840+
' Take the Nth same-language Roku track
841+
if jfPosition >= 0
842+
rokuSeen = 0
843+
for each rokuTrack in availableAudioTracks
844+
if isValid(rokuTrack.track) and isValid(rokuTrack.language) and LCase(rokuTrack.language) = selectedLanguage
845+
if rokuSeen = jfPosition then return rokuTrack.track
846+
rokuSeen += 1
847+
end if
848+
end for
849+
end if
832850
end if
833851
end if
834852

docs/source_utils_streamSelection.bs.html

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,43 @@
185185
return ""
186186
end function
187187

188+
' isSelectedAudioStreamDirectPlayable: Whether the audio stream at jellyfinAudioIndex
189+
' inside audioStreams is decodable by the current device.
190+
'
191+
' Used by mid-playback audio switching to decide whether Roku's native track-switch is
192+
' viable, or whether a server reload is required (e.g. switching back to an 8ch track
193+
' on a stereo device — Roku can't decode it natively, so a transcode reload is needed).
194+
'
195+
' @param {integer} jellyfinAudioIndex - Jellyfin index of the chosen audio stream
196+
' @param {dynamic} audioStreams - Array of audio MediaStream objects (e.g. fullAudioData)
197+
' @returns {boolean} - true if the chosen stream can be direct-played on this device
198+
function isSelectedAudioStreamDirectPlayable(jellyfinAudioIndex as integer, audioStreams as dynamic) as boolean
199+
if not isValid(audioStreams) then return false
200+
selected = invalid
201+
for each stream in audioStreams
202+
if isValid(stream.index) and stream.index = jellyfinAudioIndex
203+
selected = stream
204+
exit for
205+
end if
206+
end for
207+
if not isValid(selected) then return false
208+
return isStreamDirectPlayable(selected, getDeviceAudioCapabilities())
209+
end function
210+
211+
' isCommentaryAudioStream: Determine whether a stream is a commentary/secondary track.
212+
'
213+
' Detection: case-insensitive substring match for "commentary" in the stream Title.
214+
' Used by findBestAudioStreamIndex() to deprioritize commentary tracks during auto-selection
215+
' so a director's-commentary stereo track doesn't outrank the main multichannel track on a
216+
' stereo device (issue #500).
217+
'
218+
' @param {object} stream - MediaStream object from Jellyfin metadata
219+
' @returns {boolean} - true if the stream looks like a commentary track
220+
function isCommentaryAudioStream(stream as object) as boolean
221+
if not isValid(stream) or not isValid(stream.Title) then return false
222+
return inStr(1, LCase(stream.Title), "commentary") > 0
223+
end function
224+
188225
' findBestAudioStreamIndex: Primary function for selecting the best audio stream
189226
'
190227
' Selection priority when playDefault = true (Jellyfin: "Play default audio track regardless of language"):
@@ -250,6 +287,23 @@
250287
return 0
251288
end if
252289

290+
' Deprioritize commentary tracks (issue #500): when at least one non-commentary track
291+
' exists, hide commentary entries from all subsequent selection logic so a stereo
292+
' commentary track can't outrank the multichannel main track on a stereo device.
293+
' Files with only commentary tracks fall through with the original list.
294+
mainStreams = []
295+
for i = 0 to audioStreams.Count() - 1
296+
if not isCommentaryAudioStream(audioStreams[i])
297+
mainStreams.push(audioStreams[i])
298+
end if
299+
end for
300+
if mainStreams.Count() > 0
301+
audioStreams = mainStreams
302+
if audioStreams.Count() = 1 and isValid(audioStreams[0].index)
303+
return audioStreams[0].index
304+
end if
305+
end if
306+
253307
' Multiple audio tracks - apply selection logic
254308

255309
' BRANCH 1: "Play default track" setting is enabled

0 commit comments

Comments
 (0)