Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/app/src/services/playback.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ const buildPlaybackUrl = (itemId, mediaSource, playSessionId, playMethod, creden
throw new Error('No playback URL available');
};

const extractAudioStreams = (mediaSource) => {
export const extractAudioStreams = (mediaSource) => {
if (!mediaSource.MediaStreams) return [];
return mediaSource.MediaStreams
.filter(s => s.Type === 'Audio')
Expand Down
14 changes: 5 additions & 9 deletions packages/app/src/views/Details/Details.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {useSettings} from '../../context/SettingsContext';
import {useSeerr} from '../../context/SeerrContext';
import {useSyncPlay} from '../../context/SyncPlayContext';
import * as jellyfinApi from '../../services/jellyfinApi';
import {extractAudioStreams} from '../../services/playback';
import {initialAudioPosition} from '../Player/initialAudio';
import ModernDetailContent from './ModernDetailContent';
import DetailSkeleton from './DetailSkeleton';
import {formatDuration, getImageUrl, getBackdropId, getLogoUrl} from '../../utils/helpers';
Expand Down Expand Up @@ -572,19 +574,13 @@ const Details = ({itemId: itemIdProp, initialItem, onPlay, onSelectItem, onSelec
}
}, [remoteSubtitleResults, item, effectiveApi, selectedVersionIndex, closeModal, tagWithServerInfo, setItem, setSelectedSubtitleIndex]);

const handleSelectVersion = useCallback((e) => {
const handleSelectVersion = useCallback(async (e) => {
const index = parseInt(e.currentTarget.dataset.index, 10);
if (isNaN(index) || !item?.MediaSources?.[index]) return;
setSelectedVersionIndex(index);
const ms = item.MediaSources[index];
const versionAudioStreams = ms.MediaStreams?.filter(s => s.Type === 'Audio') || [];
const versionSubtitleStreams = ms.MediaStreams?.filter(s => s.Type === 'Subtitle') || [];
if (ms.DefaultAudioStreamIndex != null) {
const idx = versionAudioStreams.findIndex(s => s.Index === ms.DefaultAudioStreamIndex);
setSelectedAudioIndex(idx >= 0 ? idx : 0);
} else {
setSelectedAudioIndex(0);
}
setSelectedAudioIndex(await initialAudioPosition(item, extractAudioStreams(ms), settings));
// A different version brings its own tracks, so whatever was picked for the old
// one no longer stands.
subtitleChosenRef.current = false;
Expand All @@ -596,7 +592,7 @@ const Details = ({itemId: itemIdProp, initialItem, onPlay, onSelectItem, onSelec
setSelectedSubtitleIndex(-1);
}
closeModal();
}, [item, closeModal, setSelectedVersionIndex, setSelectedAudioIndex, setSelectedSubtitleIndex]);
}, [item, settings, closeModal, setSelectedVersionIndex, setSelectedAudioIndex, setSelectedSubtitleIndex]);

const handleSeasonSelect = useCallback((ev) => {
const seasonId = ev.currentTarget.dataset.seasonId;
Expand Down
20 changes: 3 additions & 17 deletions packages/app/src/views/Details/useDetailsItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {useState, useEffect, useCallback, useMemo, useRef} from 'react';
import $L from '@enact/i18n/$L';

import * as playback from '../../services/playback';
import {initialAudioPosition} from '../Player/initialAudio';
import {fetchTmdbSeasonRatings, resolveSeriesTmdbId, isRatingSourceAllowed} from '../../services/mdblistApi';
import {getItemSubtitlePref, getSeriesSubtitlePref, getSeriesAudioPref} from '../../services/subtitlePrefs';
import {getItemSubtitlePref, getSeriesSubtitlePref} from '../../services/subtitlePrefs';
import {fromServerStream, matchSeriesTrackIndex} from '../../utils/seriesTrackPrefs';
import {findParentCollections} from './parentCollection';
import {canScoreSeedLocally, getOnlineRecommendations, getRecommendations, mergeRecommendations} from '../../services/homeRecommendations';
Expand Down Expand Up @@ -187,23 +188,8 @@ const useDetailsItem = ({itemId, initialItem, effectiveApi, effectiveServerUrl,
setSelectedVersionIndex(0);
const ms = data.MediaSources?.[0];
if (ms) {
const initAudioStreams = ms.MediaStreams?.filter(s => s.Type === 'Audio') || [];
const initSubtitleStreams = ms.MediaStreams?.filter(s => s.Type === 'Subtitle') || [];
// A track remembered for the series shows as active, and only when there
// is none does the server's own default stand in.
const seriesAudioPref = data.SeriesId ? await getSeriesAudioPref(data.SeriesId) : undefined;
const matchedAudio = seriesAudioPref
? matchSeriesTrackIndex(initAudioStreams.map(fromServerStream), seriesAudioPref)
: null;
const rememberedAudioPos = matchedAudio !== null && matchedAudio >= 0
? initAudioStreams.findIndex(s => s.Index === matchedAudio)
: -1;
if (rememberedAudioPos >= 0) {
setSelectedAudioIndex(rememberedAudioPos);
} else if (ms.DefaultAudioStreamIndex != null) {
const idx = initAudioStreams.findIndex(s => s.Index === ms.DefaultAudioStreamIndex);
if (idx >= 0) setSelectedAudioIndex(idx);
}
setSelectedAudioIndex(await initialAudioPosition(data, playback.extractAudioStreams(ms), settingsRef.current));
// Show the remembered pick as active so it doesn't look like it needs
// reselecting. The per-item index restores the exact track, and an episode
// otherwise inherits its series' remembered language.
Expand Down
10 changes: 10 additions & 0 deletions packages/app/src/views/Player/initialAudio.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {getSeriesAudioPref} from '../../services/subtitlePrefs';
import {selectPreferredAudioStream} from '../../utils/audioTrackSelection';
import {matchSeriesTrackIndex} from '../../utils/seriesTrackPrefs';

/**
Expand All @@ -16,3 +17,12 @@ export const resolveSeriesAudio = async (item, audioStreams) => {

return audioStreams.find((stream) => stream.index === matched) || null;
};

/**
* Where among these audio tracks the player starts when none is picked, so Details
* can show that track.
*/
export const initialAudioPosition = async (item, audioStreams, settings) => {
const start = await resolveSeriesAudio(item, audioStreams) || selectPreferredAudioStream(audioStreams, settings);
return Math.max(0, audioStreams.indexOf(start));
};
26 changes: 26 additions & 0 deletions packages/app/src/views/Player/initialAudio.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {initialAudioPosition} from './initialAudio';
import {getSeriesAudioPref} from '../../services/subtitlePrefs';

jest.mock('../../services/subtitlePrefs', () => ({
getSeriesAudioPref: jest.fn()
}));

const jpn = {index: 1, language: 'jpn', isDefault: true, channels: 2};
const spa = {index: 2, language: 'spa', isDefault: false, channels: 2};
const episode = {Id: 'episode-1', SeriesId: 'series-1'};

beforeEach(() => {
getSeriesAudioPref.mockResolvedValue(undefined);
});

describe('initialAudioPosition', () => {
test('is the fallback language rather than the default track, as the player picks', async () => {
const settings = {audioLanguage: 'cat', fallbackAudioLanguage: 'spa'};
expect(await initialAudioPosition(episode, [jpn, spa], settings)).toBe(1);
});

test('puts the track remembered for the series before the language preferences', async () => {
getSeriesAudioPref.mockResolvedValue({language: 'jpn', title: '', relativeIndex: 0});
expect(await initialAudioPosition(episode, [jpn, spa], {audioLanguage: 'spa'})).toBe(0);
});
});
Loading