Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app/src/context/defaultSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export const defaultSettings = {
ac3Passthrough: true,
eac3Passthrough: true,
truehdPassthrough: true,
forceTruehdPassthrough: false,
blockedRatings: [],
showSeerrButton: true,
// Synced but not shown anywhere in this app, so null keeps the TV from stamping a default
Expand Down
7 changes: 5 additions & 2 deletions packages/app/src/services/playback.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ const DEFAULT_PASSTHROUGH_SETTINGS = {
eac3Passthrough: true,
dtsPassthrough: true,
dtshdPassthrough: true,
truehdPassthrough: true
truehdPassthrough: true,
// Skip check and passtrough TueHD
forceTruehdPassthrough: false
};

const getPlaybackAudioSettings = async (options = {}) => {
Expand All @@ -58,7 +60,8 @@ const getPlaybackAudioSettings = async (options = {}) => {
eac3Passthrough: options.eac3Passthrough ?? stored.eac3Passthrough ?? DEFAULT_PASSTHROUGH_SETTINGS.eac3Passthrough,
dtsPassthrough: options.dtsPassthrough ?? stored.dtsPassthrough ?? DEFAULT_PASSTHROUGH_SETTINGS.dtsPassthrough,
dtshdPassthrough: options.dtshdPassthrough ?? stored.dtshdPassthrough ?? DEFAULT_PASSTHROUGH_SETTINGS.dtshdPassthrough,
truehdPassthrough: options.truehdPassthrough ?? stored.truehdPassthrough ?? DEFAULT_PASSTHROUGH_SETTINGS.truehdPassthrough
truehdPassthrough: options.truehdPassthrough ?? stored.truehdPassthrough ?? DEFAULT_PASSTHROUGH_SETTINGS.truehdPassthrough,
forceTruehdPassthrough: options.forceTruehdPassthrough ?? stored.forceTruehdPassthrough ?? DEFAULT_PASSTHROUGH_SETTINGS.forceTruehdPassthrough
};
};

Expand Down
38 changes: 36 additions & 2 deletions packages/app/src/views/Player/WebOSPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {getSubtitleOverlayStyle, getSubtitleTextStyle, sanitizeSubtitleHtml, res
import {isHdrOutput} from '../../utils/videoRange';
import {findPreferredAudioStream} from '../../utils/audioLanguage';
import {saveSubtitlePref} from '../../services/subtitlePrefs';
import serverLogger from '../../services/serverLogger';
import {resolveInitialSubtitle} from './initialSubtitle';
import PlayerControls, {usePlayerButtons} from './PlayerControls';
import NextUpOverlay from './NextUpOverlay';
Expand All @@ -54,6 +55,8 @@ import {getVideoDisplayAspectRatio, getZoomDisplayRect} from './aspectRatioUtils

import css from './WebOSPlayer.module.less';

const TRUEHD_CODECS = ['truehd', 'mlp'];

const getWebOSFullscreenRect = () => {
if (typeof window === 'undefined') {
return {width: 1920, height: 1080};
Expand Down Expand Up @@ -625,12 +628,21 @@ const Player = ({item, resume, initialMediaSourceId, initialAudioIndex, initialS
isLiveTV,
hasUserData: !!item.UserData
});
// Force TrueHD passtrough if anabled.
const pinTruehdDirectPlay = Boolean(settings.forceTruehdPassthrough) &&
(item?.MediaSources || []).some((source) => (source?.MediaStreams || []).some(
(s) => s.Type === 'Audio' && TRUEHD_CODECS.includes((s.Codec || '').toLowerCase())
));

const playbackInfoOptions = {
startPositionTicks: startPosition,
maxBitrate: selectedQuality || settings.maxBitrate,
enableDirectPlay: !forceTranscode && !settings.preferTranscode,
enableDirectStream: !forceTranscode && !settings.preferTranscode,
forceDirectPlay: (isLiveTV || forceTranscode) ? false : settings.forceDirectPlay,
forceTruehdPassthrough: settings.forceTruehdPassthrough,
forceDirectPlay: (isLiveTV || forceTranscode)
? false
: (settings.forceDirectPlay || pinTruehdDirectPlay),
mediaSourceId: initialMediaSourceId,
audioStreamIndex: initialAudioIndex != null ? initialAudioIndex : undefined,
subtitleStreamIndex: initialSubtitleIndex,
Expand Down Expand Up @@ -719,6 +731,28 @@ const Player = ({item, resume, initialMediaSourceId, initialAudioIndex, initialS
: null;
}

// For logging purpos to see what the audio path is
const audioCaps = playback.getCurrentSession()?.capabilities;
serverLogger.playback('Playback: media opened', {
itemId: item?.Id,
playMethod: result.playMethod,
container: result.mediaSource?.Container,
forceTruehdPassthrough: Boolean(settings.forceTruehdPassthrough),
pinTruehdDirectPlay,
audioOutputStatus: audioCaps?.audioOutputStatus,
defaultAudioStreamIndex: result.defaultAudioStreamIndex,
selectedAudioStreamIndex: result.selectedAudioStreamIndex,
audioLanguageSetting: settings.audioLanguage,
audioStreams: (result.audioStreams || []).map((s) => ({
index: s.index,
codec: s.codec,
profile: s.profile,
channels: s.channels,
language: s.language,
isDefault: s.isDefault
}))
});

// Load subtitle data or renderer for the selected stream.
const loadSubtitleData = async (sub) => {
disposePgsRenderer(pgsRendererRef.current);
Expand Down Expand Up @@ -897,7 +931,7 @@ const Player = ({item, resume, initialMediaSourceId, initialAudioIndex, initialS
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, resume, videoQueue, onPlayNext, selectedQuality, settings.maxBitrate, settings.preferTranscode, settings.forceDirectPlay, forceTranscode, settings.subtitleMode, settings.introAction, settings.outroAction, initialAudioIndex, initialSubtitleIndex]);
}, [item, resume, videoQueue, onPlayNext, selectedQuality, settings.maxBitrate, settings.preferTranscode, settings.forceDirectPlay, settings.forceTruehdPassthrough, forceTranscode, settings.subtitleMode, settings.introAction, settings.outroAction, initialAudioIndex, initialSubtitleIndex]);

useEffect(() => {
if (mediaUrl) {
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/views/Settings/settingsSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ export const SETTINGS_SCHEMA = [
{kind: KIND.TOGGLE, key: 'passthroughEnabled', label: () => $L('Audio Passthrough'), desc: () => $L('Enable advanced bitstream passthrough for external audio devices'), icon: 'speaker'},
{kind: KIND.TOGGLE, key: 'ac3Passthrough', label: () => $L('AC3 Passthrough'), desc: () => $L('Allow Dolby Digital passthrough when available'), icon: 'speaker'},
{kind: KIND.TOGGLE, key: 'eac3Passthrough', label: () => $L('E-AC3 Passthrough'), desc: () => $L('Allow Dolby Digital Plus passthrough when available'), icon: 'speaker'},
{kind: KIND.TOGGLE, key: 'truehdPassthrough', label: () => $L('TrueHD Passthrough (Experimental)'), desc: () => $L('Allow Dolby TrueHD passthrough when available'), icon: 'speaker'}
{kind: KIND.TOGGLE, key: 'truehdPassthrough', label: () => $L('TrueHD Passthrough (Experimental)'), desc: () => $L('Allow Dolby TrueHD passthrough when available'), icon: 'speaker'},
{kind: KIND.TOGGLE, key: 'forceTruehdPassthrough', label: () => $L('Force TrueHD / Atmos Passthrough'), desc: () => $L('Send Dolby TrueHD and Atmos straight to your receiver. Make sure your receiver supports it.'), icon: 'speaker'}
]
},
{
Expand Down
67 changes: 53 additions & 14 deletions packages/platform-webos/src/deviceProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const DEFAULT_PASSTHROUGH_SETTINGS = {
eac3Passthrough: true,
dtsPassthrough: true,
dtshdPassthrough: true,
truehdPassthrough: true
truehdPassthrough: true,
forceTruehdPassthrough: false
};

const resolvePassthroughSettings = (options = {}) => ({
Expand All @@ -18,26 +19,34 @@ const resolvePassthroughSettings = (options = {}) => ({

const emptyDtsSupport = {mkv: false, mp4: false, ts: false, avi: false};

const LOSSLESS_PASSTHROUGH_OUTPUTS = new Set(['external_arc']);

const getActiveSoundOutput = (audioStatus = {}) =>
(audioStatus?.soundOutput || '').toString().toLowerCase().trim();

/**
* Whether the audio path is an eARC/ARC connection to a receiver.
*/
const isArcSoundOutput = (audioStatus = {}) => /e?arc/.test(getActiveSoundOutput(audioStatus));

/**
* Checks if Bitstream passthrough is active, which requires an eARC/ARC connection to a receiver.
*/
const isLosslessPassthroughOutput = (audioStatus = {}) => {
if (!LOSSLESS_PASSTHROUGH_OUTPUTS.has(getActiveSoundOutput(audioStatus))) return false;
if (!isArcSoundOutput(audioStatus)) return false;
const digital = (audioStatus?.soundOutputDigital || '').toString().toLowerCase();
return digital !== 'pcm';
};

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const queryAudioOutputStatus = async () => {
const lunaRequest = async (service, method, parameters) => {
try {
const LS2Request = (await import('@enact/webos/LS2Request')).default;
return await new Promise((resolve) => {
// eslint-disable-next-line
new LS2Request().send({
service: 'luna://com.webos.service.avoutput',
method: 'audio/getStatus',
service: `luna://${service}`,
method,
...(parameters ? {parameters} : {}),
onSuccess: resolve,
onFailure: () => resolve({})
});
Expand All @@ -47,13 +56,35 @@ const queryAudioOutputStatus = async () => {
}
};

const queryAudioOutputStatus = () =>
lunaRequest('com.webos.service.avoutput', 'audio/getStatus');

/**
* The documented route to the sound path. com.webos.service.avoutput is not a
* public API and answers an empty object on current firmware, which read as "no
* receiver" and quietly disabled every passthrough check.
*/
const querySoundSettings = async () => {
const result = await lunaRequest('com.webos.settingsservice', 'getSystemSettings', {
category: 'sound',
keys: ['soundOutput', 'digitalSoundOutput']
});
return result?.settings || {};
};

const getAudioOutputStatus = async () => {
for (let attempt = 0; attempt < 4; attempt++) {
const status = await queryAudioOutputStatus();
if (getActiveSoundOutput(status)) return status;
const [status, sound] = await Promise.all([queryAudioOutputStatus(), querySoundSettings()]);
// Luna API returns empty objects.
const merged = {
...status,
soundOutput: status?.soundOutput || sound.soundOutput,
soundOutputDigital: status?.soundOutputDigital || sound.digitalSoundOutput
};
if (getActiveSoundOutput(merged)) return merged;
await delay(150);
}
return queryAudioOutputStatus();
return {};
};

const applyAudioOutputPath = async (baseCaps) => {
Expand All @@ -67,8 +98,9 @@ const applyAudioOutputPath = async (baseCaps) => {
return {
...baseCaps,
dts,
truehd: receiverPassthrough,
dtshd: receiverPassthrough && !!(dts.mkv || dts.mp4 || dts.ts),
// TrueHD is fine. Only need to check if channels are preserved.
truehdBitstream: receiverPassthrough,
audioOutputStatus,
externalAudioPathActive: receiverPassthrough
};
Expand All @@ -79,11 +111,16 @@ const applyPassthroughSettings = (caps, options = {}) => {
const passthroughAllowed = prefs.passthroughEnabled;
const dtsSupport = passthroughAllowed && prefs.dtsPassthrough ? (caps.dts || emptyDtsSupport) : emptyDtsSupport;

// Passrough is enabled in settings.
const forceTruehd = !!prefs.forceTruehdPassthrough;
const truehdAllowed = forceTruehd || (passthroughAllowed && !!prefs.truehdPassthrough);

return {
...caps,
ac3: !!caps.ac3 && !!prefs.ac3Passthrough,
eac3: !!caps.eac3 && !!prefs.eac3Passthrough,
truehd: !!caps.truehd && passthroughAllowed && !!prefs.truehdPassthrough,
truehd: forceTruehd || (!!caps.truehd && truehdAllowed),
truehdBitstream: forceTruehd || (!!caps.truehdBitstream && truehdAllowed),
dts: dtsSupport,
dtshd: !!caps.dtshd && passthroughAllowed && !!prefs.dtshdPassthrough
};
Expand Down Expand Up @@ -307,7 +344,8 @@ export const getDeviceCapabilities = async () => {
dtsBase,
ac3: testAc3Support(),
eac3: true,
truehd: false,
// If the reciver is connected via arc/eArc, TrueHD can go trough bitrseam.
truehd: true,
dtshd: false,
// the TV decodes FLAC to LPCM, so it direct plays in a video container
// on modern webOS even though LG only lists it as a standalone format
Expand Down Expand Up @@ -633,7 +671,8 @@ export const getJellyfinDeviceProfile = async (options = {}) => {
const directPlayProfiles = buildDirectPlayProfiles(caps);

const maxStreamingBitrate = 120_000_000;
const maxAudioChannels = caps.dolbyAtmos || caps.truehd || caps.dtshd ? '8' : '6';
// LG says limit is scince WebOS 4,5 8, but it limits it to 6?
const maxAudioChannels = caps.dolbyAtmos || caps.truehdBitstream || caps.dtshd ? '8' : '6';

// fMP4 preserves DV RPU metadata; MPEG-TS strips it. Use fMP4 on webOS 5+.
const hlsContainer = caps.nativeHlsFmp4 ? 'mp4' : 'ts';
Expand Down
7 changes: 5 additions & 2 deletions packages/platform-webos/src/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const DEFAULT_PASSTHROUGH_SETTINGS = {
eac3Passthrough: true,
dtsPassthrough: true,
dtshdPassthrough: true,
truehdPassthrough: true
truehdPassthrough: true,
forceTruehdPassthrough: false
};

const resolvePassthroughSettings = (options = {}) => ({
Expand Down Expand Up @@ -100,7 +101,9 @@ export const getSupportedAudioCodecs = (capabilities, container = '', passthroug
if (dtsOk && capabilities.dtshd) codecs.push('dts-hd', 'dtshd', 'dtsx');
}

if (capabilities.truehd && passthroughAllowed && passthrough.truehdPassthrough) codecs.push('truehd', 'mlp');
const truehdOk = passthrough.forceTruehdPassthrough ||
(capabilities.truehd && passthroughAllowed && passthrough.truehdPassthrough);
if (truehdOk) codecs.push('truehd', 'mlp');
if (capabilities.opus) codecs.push('opus');
codecs.push('vorbis', 'wma', 'amr', 'amrnb', 'amrwb');

Expand Down
Loading