diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 083aff5eb..0013eb654 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -11,6 +11,7 @@ import { appHost } from '../apphost'; import Screenfull from 'screenfull'; import ServerConnections from '../ServerConnections'; import alert from '../alert'; +import { includesAny } from '../../utils/container.ts'; const UNLIMITED_ITEMS = -1; @@ -1287,6 +1288,7 @@ class PlaybackManager { return false; } + const container = mediaSource.Container.toLowerCase(); const codec = (mediaStream.Codec || '').toLowerCase(); if (!codec) { @@ -1295,22 +1297,11 @@ class PlaybackManager { const profiles = deviceProfile.DirectPlayProfiles || []; - return profiles.filter(function (p) { - if (p.Type === 'Video') { - if (!p.AudioCodec) { - return true; - } - - // This is an exclusion filter - if (p.AudioCodec.indexOf('-') === 0) { - return p.AudioCodec.toLowerCase().indexOf(codec) === -1; - } - - return p.AudioCodec.toLowerCase().indexOf(codec) !== -1; - } - - return false; - }).length > 0; + return profiles.some(function (p) { + return p.Type === 'Video' + && includesAny((p.Container || '').toLowerCase(), container) + && includesAny((p.AudioCodec || '').toLowerCase(), codec); + }); } self.setAudioStreamIndex = function (index, player) { diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 249e27328..af136e918 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -27,10 +27,11 @@ import itemHelper from '../../components/itemHelper'; import Screenfull from 'screenfull'; import globalize from '../../scripts/globalize'; import ServerConnections from '../../components/ServerConnections'; -import profileBuilder from '../../scripts/browserDeviceProfile'; +import profileBuilder, { canPlaySecondaryAudio } from '../../scripts/browserDeviceProfile'; import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings'; import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/backdrop/backdrop'; import Events from '../../utils/events.ts'; +import { includesAny } from '../../utils/container.ts'; /** * Returns resolved URL. @@ -593,7 +594,7 @@ function tryRemoveElement(elem) { /** * @private */ - isAudioStreamSupported(stream, deviceProfile) { + isAudioStreamSupported(stream, deviceProfile, container) { const codec = (stream.Codec || '').toLowerCase(); if (!codec) { @@ -607,17 +608,11 @@ function tryRemoveElement(elem) { const profiles = deviceProfile.DirectPlayProfiles || []; - return profiles.filter(function (p) { - if (p.Type === 'Video') { - if (!p.AudioCodec) { - return true; - } - - return p.AudioCodec.toLowerCase().includes(codec); - } - - return false; - }).length > 0; + return profiles.some(function (p) { + return p.Type === 'Video' + && includesAny((p.Container || '').toLowerCase(), container) + && includesAny((p.AudioCodec || '').toLowerCase(), codec); + }); } /** @@ -626,8 +621,11 @@ function tryRemoveElement(elem) { getSupportedAudioStreams() { const profile = this.#lastProfile; - return getMediaStreamAudioTracks(this._currentPlayOptions.mediaSource).filter((stream) => { - return this.isAudioStreamSupported(stream, profile); + const mediaSource = this._currentPlayOptions.mediaSource; + const container = mediaSource.Container.toLowerCase(); + + return getMediaStreamAudioTracks(mediaSource).filter((stream) => { + return this.isAudioStreamSupported(stream, profile, container); }); } @@ -1545,12 +1543,12 @@ function tryRemoveElement(elem) { } canSetAudioStreamIndex() { - if (browser.tizen || browser.orsay) { - return true; + const video = this.#mediaElement; + if (video) { + return canPlaySecondaryAudio(video); } - const video = this.#mediaElement; - return !!video?.audioTracks; + return false; } static onPictureInPictureError(err) { diff --git a/src/scripts/browserDeviceProfile.js b/src/scripts/browserDeviceProfile.js index 5225981e5..8eddeb6ba 100644 --- a/src/scripts/browserDeviceProfile.js +++ b/src/scripts/browserDeviceProfile.js @@ -334,6 +334,23 @@ import browser from './browser'; return 2; } +/** + * Checks if the web engine supports secondary audio. + * @param {HTMLVideoElement} videoTestElement The video test element + * @returns {boolean} _true_ if the web engine supports secondary audio. + */ +export function canPlaySecondaryAudio(videoTestElement) { + // We rely on HTMLMediaElement.audioTracks + // It works in Chrome 79+ with "Experimental Web Platform features" enabled + return !!videoTestElement.audioTracks + // It doesn't work in Firefox 108 even with "media.track.enabled" enabled (it only sees the first audio track) + && !browser.firefox + // It seems to work on Tizen 5.5+ (2020, Chrome 69+). See https://developer.tizen.org/forums/web-application-development/video-tag-not-work-audiotracks + && (browser.tizenVersion >= 5.5 || !browser.tizen) + // Assume webOS 5+ (2020, Chrome 68+) supports secondary audio like Tizen 5.5+ + && (browser.web0sVersion >= 5.0 || !browser.web0sVersion); +} + export default function (options) { options = options || {}; @@ -742,13 +759,7 @@ import browser from './browser'; profile.CodecProfiles = []; - // We rely on HTMLMediaElement.audioTracks - // It works in Chrome 79+ with "Experimental Web Platform features" enabled - // It doesn't work in Firefox 108 even with "media.track.enabled" enabled (it only sees the first audio track) - // It seems to work on Tizen 5.5+ (Chrome 69+). See https://developer.tizen.org/forums/web-application-development/video-tag-not-work-audiotracks - const supportsSecondaryAudio = !!videoTestElement.audioTracks - && !browser.firefox - && (browser.tizenVersion >= 5.5 || !browser.tizen); + const supportsSecondaryAudio = canPlaySecondaryAudio(videoTestElement); const aacCodecProfileConditions = []; diff --git a/src/utils/container.ts b/src/utils/container.ts new file mode 100644 index 000000000..170c357d6 --- /dev/null +++ b/src/utils/container.ts @@ -0,0 +1,42 @@ +/** + * Checks if the list includes any value from the search. + * @param list The list to search in. + * @param search The values to search. + * @returns _true_ if the list includes any value from the search. + * @remarks The list (string) can start with '-', in which case the logic is inverted. + */ +export function includesAny(list: string | string[] | null | undefined, search: string | string[]): boolean { + if (!list) { + return true; + } + + let inverseMatch = false; + + if (typeof list === 'string') { + if (list.startsWith('-')) { + inverseMatch = true; + list = list.substring(1); + } + + list = list.split(','); + } + + list = list.filter(i => i); + + if (!list.length) { + return true; + } + + if (typeof search === 'string') { + search = search.split(','); + } + + search = search.filter(i => i); + + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + if (search.some(s => list!.includes(s))) { + return !inverseMatch; + } + + return inverseMatch; +}