2021-07-02 10:07:03 -08:00
|
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
|
import subsonic from '../subsonic'
|
feat(ui): integrate transcode decision into web player (#5155)
* feat(ui): add browser audio profile detection for transcoding
Detect browser codec capabilities via canPlayType() to build a client
profile for the getTranscodeDecision API. Only codecs returning "probably"
are treated as supported for conservative compatibility.
* feat(ui): add transcode decision service with caching and pre-fetch
Standalone service that fetches getTranscodeDecision results, caches
them with an 11-hour TTL (1h buffer before 12h token expiry), and
supports bulk pre-fetching for upcoming queue items. Includes
invalidateAll() for handling stale tokens and getCachedDecision()
for synchronous cache reads.
* feat(ui): add fetch helper for getTranscodeDecision endpoint
POST-based Subsonic API call that sends the browser's codec profile
and returns the transcode decision including the JWT transcodeParams
token for subsequent streaming.
* feat(ui): wire transcode decision service singleton
Module index that creates the service singleton with the real fetch
function and re-exports the browser profile detector.
* feat(ui): add Redux transcoding reducer for browser profile state
Store the detected browser codec profile in Redux so it's available
globally. The profile is set once at startup and used by the decision
service when calling getTranscodeDecision.
* feat(ui): integrate transcode decision into player musicSrc
Replace static stream URLs with lazy musicSrc functions that fetch
a transcode decision before playback. Falls back to the old stream
endpoint if the decision fetch fails or if no browser profile is set.
* feat(ui): detect browser profile and pre-fetch transcode decisions
Run codec detection once when the Player mounts, storing the profile
in both the decision service and Redux. Pre-fetch decisions for the
next 3 songs when the queue or play position changes.
* feat(ui): handle stale tokens and replace audio preload with decision pre-fetch
On audio playback error, invalidate all cached transcode decisions
and pre-fetch fresh decisions for upcoming songs. Replace the old
Audio element preload with decision pre-fetching to warm the cache
for instant playback transitions.
* feat(ui): show transcode format in QualityInfo chip
When transcode decision data is available, QualityInfo now shows
"FLAC → OPUS 128" instead of just the source format. The new props
are optional, so existing usages in song lists, album songs, playlists,
and shares are unaffected.
* feat(ui): display transcode status in player quality badge
AudioTitle now reads the cached transcode decision for the current
track and passes it to QualityInfo, showing "FLAC → OPUS 128" when
transcoding or the normal format when direct playing.
* chore(ui): format and lint transcode decision integration
* refactor(ui): use JWT exp claim for decision cache expiry
Replace the hardcoded 11-hour TTL with actual token expiration
decoded from the JWT's exp claim. Each cache entry is now validated
against its own token's lifetime, adapting automatically to server
configuration changes. Tokens without an exp claim are treated as
expired and re-fetched immediately.
* fix(ui): resolve transcode URLs eagerly on browser refresh
Instead of setting musicSrc to a function on queue refresh (which
breaks the player's identity matching and can't survive JSON
serialization), resolve transcode decisions for the current and
next few tracks before dispatching, passing string URLs to the
reducer.
Also simplifies code: extract makeMusicSrc helper, add
resolveStreamUrl to decisionService, use httpClient instead of
raw fetch, and remove barrel file test.
* chore(ui): fix prettier formatting in Player.jsx
* fix(ui): use ref to avoid stale closure in mount-only transcode effect
Split the mount effect into profile detection + URL resolution, using a
ref for playerState so the effect correctly reads the latest queue without
needing playerState in the dependency array (which would cause it to
re-run on every queue/position change).
* fix(ui): address code review feedback on transcode integration
- Use jwt-decode for JWT parsing instead of manual atob (handles base64url)
- Guard resolveStreamUrl to fall back to direct stream when decision is null
- Fix savedPlayIndex -1 bug in PLAYER_REFRESH_QUEUE (findIndex returns -1)
* docs: improve comments on JWT exp claim decoding in decision service
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-09 07:06:31 -08:00
|
|
|
import { decisionService } from '../transcode'
|
2021-07-02 10:07:03 -08:00
|
|
|
import {
|
|
|
|
|
PLAYER_ADD_TRACKS,
|
|
|
|
|
PLAYER_CLEAR_QUEUE,
|
|
|
|
|
PLAYER_CURRENT,
|
|
|
|
|
PLAYER_PLAY_NEXT,
|
|
|
|
|
PLAYER_PLAY_TRACKS,
|
|
|
|
|
PLAYER_SET_TRACK,
|
|
|
|
|
PLAYER_SET_VOLUME,
|
|
|
|
|
PLAYER_SYNC_QUEUE,
|
2024-09-27 09:13:22 -08:00
|
|
|
PLAYER_SET_MODE,
|
feat(ui): integrate transcode decision into web player (#5155)
* feat(ui): add browser audio profile detection for transcoding
Detect browser codec capabilities via canPlayType() to build a client
profile for the getTranscodeDecision API. Only codecs returning "probably"
are treated as supported for conservative compatibility.
* feat(ui): add transcode decision service with caching and pre-fetch
Standalone service that fetches getTranscodeDecision results, caches
them with an 11-hour TTL (1h buffer before 12h token expiry), and
supports bulk pre-fetching for upcoming queue items. Includes
invalidateAll() for handling stale tokens and getCachedDecision()
for synchronous cache reads.
* feat(ui): add fetch helper for getTranscodeDecision endpoint
POST-based Subsonic API call that sends the browser's codec profile
and returns the transcode decision including the JWT transcodeParams
token for subsequent streaming.
* feat(ui): wire transcode decision service singleton
Module index that creates the service singleton with the real fetch
function and re-exports the browser profile detector.
* feat(ui): add Redux transcoding reducer for browser profile state
Store the detected browser codec profile in Redux so it's available
globally. The profile is set once at startup and used by the decision
service when calling getTranscodeDecision.
* feat(ui): integrate transcode decision into player musicSrc
Replace static stream URLs with lazy musicSrc functions that fetch
a transcode decision before playback. Falls back to the old stream
endpoint if the decision fetch fails or if no browser profile is set.
* feat(ui): detect browser profile and pre-fetch transcode decisions
Run codec detection once when the Player mounts, storing the profile
in both the decision service and Redux. Pre-fetch decisions for the
next 3 songs when the queue or play position changes.
* feat(ui): handle stale tokens and replace audio preload with decision pre-fetch
On audio playback error, invalidate all cached transcode decisions
and pre-fetch fresh decisions for upcoming songs. Replace the old
Audio element preload with decision pre-fetching to warm the cache
for instant playback transitions.
* feat(ui): show transcode format in QualityInfo chip
When transcode decision data is available, QualityInfo now shows
"FLAC → OPUS 128" instead of just the source format. The new props
are optional, so existing usages in song lists, album songs, playlists,
and shares are unaffected.
* feat(ui): display transcode status in player quality badge
AudioTitle now reads the cached transcode decision for the current
track and passes it to QualityInfo, showing "FLAC → OPUS 128" when
transcoding or the normal format when direct playing.
* chore(ui): format and lint transcode decision integration
* refactor(ui): use JWT exp claim for decision cache expiry
Replace the hardcoded 11-hour TTL with actual token expiration
decoded from the JWT's exp claim. Each cache entry is now validated
against its own token's lifetime, adapting automatically to server
configuration changes. Tokens without an exp claim are treated as
expired and re-fetched immediately.
* fix(ui): resolve transcode URLs eagerly on browser refresh
Instead of setting musicSrc to a function on queue refresh (which
breaks the player's identity matching and can't survive JSON
serialization), resolve transcode decisions for the current and
next few tracks before dispatching, passing string URLs to the
reducer.
Also simplifies code: extract makeMusicSrc helper, add
resolveStreamUrl to decisionService, use httpClient instead of
raw fetch, and remove barrel file test.
* chore(ui): fix prettier formatting in Player.jsx
* fix(ui): use ref to avoid stale closure in mount-only transcode effect
Split the mount effect into profile detection + URL resolution, using a
ref for playerState so the effect correctly reads the latest queue without
needing playerState in the dependency array (which would cause it to
re-run on every queue/position change).
* fix(ui): address code review feedback on transcode integration
- Use jwt-decode for JWT parsing instead of manual atob (handles base64url)
- Guard resolveStreamUrl to fall back to direct stream when decision is null
- Fix savedPlayIndex -1 bug in PLAYER_REFRESH_QUEUE (findIndex returns -1)
* docs: improve comments on JWT exp claim decoding in decision service
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-09 07:06:31 -08:00
|
|
|
PLAYER_REFRESH_QUEUE,
|
2021-07-02 10:07:03 -08:00
|
|
|
} from '../actions'
|
|
|
|
|
import config from '../config'
|
|
|
|
|
|
|
|
|
|
const initialState = {
|
|
|
|
|
queue: [],
|
|
|
|
|
current: {},
|
|
|
|
|
clear: false,
|
2022-11-11 12:31:28 -09:00
|
|
|
volume: config.defaultUIVolume / 100,
|
2021-07-03 16:35:21 -08:00
|
|
|
savedPlayIndex: 0,
|
2021-07-02 10:07:03 -08:00
|
|
|
}
|
|
|
|
|
|
2023-12-27 16:20:29 -09:00
|
|
|
const pad = (value) => {
|
|
|
|
|
const str = value.toString()
|
|
|
|
|
if (str.length === 1) {
|
|
|
|
|
return `0${str}`
|
|
|
|
|
} else {
|
|
|
|
|
return str
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-19 18:52:55 -09:00
|
|
|
|
feat(ui): integrate transcode decision into web player (#5155)
* feat(ui): add browser audio profile detection for transcoding
Detect browser codec capabilities via canPlayType() to build a client
profile for the getTranscodeDecision API. Only codecs returning "probably"
are treated as supported for conservative compatibility.
* feat(ui): add transcode decision service with caching and pre-fetch
Standalone service that fetches getTranscodeDecision results, caches
them with an 11-hour TTL (1h buffer before 12h token expiry), and
supports bulk pre-fetching for upcoming queue items. Includes
invalidateAll() for handling stale tokens and getCachedDecision()
for synchronous cache reads.
* feat(ui): add fetch helper for getTranscodeDecision endpoint
POST-based Subsonic API call that sends the browser's codec profile
and returns the transcode decision including the JWT transcodeParams
token for subsequent streaming.
* feat(ui): wire transcode decision service singleton
Module index that creates the service singleton with the real fetch
function and re-exports the browser profile detector.
* feat(ui): add Redux transcoding reducer for browser profile state
Store the detected browser codec profile in Redux so it's available
globally. The profile is set once at startup and used by the decision
service when calling getTranscodeDecision.
* feat(ui): integrate transcode decision into player musicSrc
Replace static stream URLs with lazy musicSrc functions that fetch
a transcode decision before playback. Falls back to the old stream
endpoint if the decision fetch fails or if no browser profile is set.
* feat(ui): detect browser profile and pre-fetch transcode decisions
Run codec detection once when the Player mounts, storing the profile
in both the decision service and Redux. Pre-fetch decisions for the
next 3 songs when the queue or play position changes.
* feat(ui): handle stale tokens and replace audio preload with decision pre-fetch
On audio playback error, invalidate all cached transcode decisions
and pre-fetch fresh decisions for upcoming songs. Replace the old
Audio element preload with decision pre-fetching to warm the cache
for instant playback transitions.
* feat(ui): show transcode format in QualityInfo chip
When transcode decision data is available, QualityInfo now shows
"FLAC → OPUS 128" instead of just the source format. The new props
are optional, so existing usages in song lists, album songs, playlists,
and shares are unaffected.
* feat(ui): display transcode status in player quality badge
AudioTitle now reads the cached transcode decision for the current
track and passes it to QualityInfo, showing "FLAC → OPUS 128" when
transcoding or the normal format when direct playing.
* chore(ui): format and lint transcode decision integration
* refactor(ui): use JWT exp claim for decision cache expiry
Replace the hardcoded 11-hour TTL with actual token expiration
decoded from the JWT's exp claim. Each cache entry is now validated
against its own token's lifetime, adapting automatically to server
configuration changes. Tokens without an exp claim are treated as
expired and re-fetched immediately.
* fix(ui): resolve transcode URLs eagerly on browser refresh
Instead of setting musicSrc to a function on queue refresh (which
breaks the player's identity matching and can't survive JSON
serialization), resolve transcode decisions for the current and
next few tracks before dispatching, passing string URLs to the
reducer.
Also simplifies code: extract makeMusicSrc helper, add
resolveStreamUrl to decisionService, use httpClient instead of
raw fetch, and remove barrel file test.
* chore(ui): fix prettier formatting in Player.jsx
* fix(ui): use ref to avoid stale closure in mount-only transcode effect
Split the mount effect into profile detection + URL resolution, using a
ref for playerState so the effect correctly reads the latest queue without
needing playerState in the dependency array (which would cause it to
re-run on every queue/position change).
* fix(ui): address code review feedback on transcode integration
- Use jwt-decode for JWT parsing instead of manual atob (handles base64url)
- Guard resolveStreamUrl to fall back to direct stream when decision is null
- Fix savedPlayIndex -1 bug in PLAYER_REFRESH_QUEUE (findIndex returns -1)
* docs: improve comments on JWT exp claim decoding in decision service
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-09 07:06:31 -08:00
|
|
|
const makeMusicSrc = (trackId) =>
|
|
|
|
|
decisionService.getProfile()
|
|
|
|
|
? () =>
|
|
|
|
|
decisionService
|
|
|
|
|
.resolveStreamUrl(trackId)
|
|
|
|
|
.catch(() => subsonic.streamUrl(trackId))
|
|
|
|
|
: subsonic.streamUrl(trackId)
|
|
|
|
|
|
2021-07-02 10:07:03 -08:00
|
|
|
const mapToAudioLists = (item) => {
|
|
|
|
|
// If item comes from a playlist, trackId is mediaFileId
|
|
|
|
|
const trackId = item.mediaFileId || item.id
|
2023-01-15 11:11:37 -09:00
|
|
|
|
|
|
|
|
if (item.isRadio) {
|
|
|
|
|
return {
|
|
|
|
|
trackId,
|
|
|
|
|
uuid: uuidv4(),
|
|
|
|
|
name: item.name,
|
|
|
|
|
song: item,
|
|
|
|
|
musicSrc: item.streamUrl,
|
|
|
|
|
cover: item.cover,
|
|
|
|
|
isRadio: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-19 06:21:20 -08:00
|
|
|
const { lyrics } = item
|
2023-12-27 16:20:29 -09:00
|
|
|
let lyricText = ''
|
|
|
|
|
|
|
|
|
|
if (lyrics) {
|
|
|
|
|
const structured = JSON.parse(lyrics)
|
|
|
|
|
for (const structuredLyric of structured) {
|
|
|
|
|
if (structuredLyric.synced) {
|
|
|
|
|
for (const line of structuredLyric.line) {
|
|
|
|
|
let time = Math.floor(line.start / 10)
|
|
|
|
|
const ms = time % 100
|
|
|
|
|
time = Math.floor(time / 100)
|
|
|
|
|
const sec = time % 60
|
|
|
|
|
time = Math.floor(time / 60)
|
|
|
|
|
const min = time % 60
|
|
|
|
|
|
|
|
|
|
ms.toString()
|
|
|
|
|
lyricText += `[${pad(min)}:${pad(sec)}.${pad(ms)}] ${line.value}\n`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-02 10:07:03 -08:00
|
|
|
return {
|
|
|
|
|
trackId,
|
|
|
|
|
uuid: uuidv4(),
|
|
|
|
|
song: item,
|
|
|
|
|
name: item.title,
|
2023-12-27 16:20:29 -09:00
|
|
|
lyric: lyricText,
|
2021-07-02 10:07:03 -08:00
|
|
|
singer: item.artist,
|
|
|
|
|
duration: item.duration,
|
feat(ui): integrate transcode decision into web player (#5155)
* feat(ui): add browser audio profile detection for transcoding
Detect browser codec capabilities via canPlayType() to build a client
profile for the getTranscodeDecision API. Only codecs returning "probably"
are treated as supported for conservative compatibility.
* feat(ui): add transcode decision service with caching and pre-fetch
Standalone service that fetches getTranscodeDecision results, caches
them with an 11-hour TTL (1h buffer before 12h token expiry), and
supports bulk pre-fetching for upcoming queue items. Includes
invalidateAll() for handling stale tokens and getCachedDecision()
for synchronous cache reads.
* feat(ui): add fetch helper for getTranscodeDecision endpoint
POST-based Subsonic API call that sends the browser's codec profile
and returns the transcode decision including the JWT transcodeParams
token for subsequent streaming.
* feat(ui): wire transcode decision service singleton
Module index that creates the service singleton with the real fetch
function and re-exports the browser profile detector.
* feat(ui): add Redux transcoding reducer for browser profile state
Store the detected browser codec profile in Redux so it's available
globally. The profile is set once at startup and used by the decision
service when calling getTranscodeDecision.
* feat(ui): integrate transcode decision into player musicSrc
Replace static stream URLs with lazy musicSrc functions that fetch
a transcode decision before playback. Falls back to the old stream
endpoint if the decision fetch fails or if no browser profile is set.
* feat(ui): detect browser profile and pre-fetch transcode decisions
Run codec detection once when the Player mounts, storing the profile
in both the decision service and Redux. Pre-fetch decisions for the
next 3 songs when the queue or play position changes.
* feat(ui): handle stale tokens and replace audio preload with decision pre-fetch
On audio playback error, invalidate all cached transcode decisions
and pre-fetch fresh decisions for upcoming songs. Replace the old
Audio element preload with decision pre-fetching to warm the cache
for instant playback transitions.
* feat(ui): show transcode format in QualityInfo chip
When transcode decision data is available, QualityInfo now shows
"FLAC → OPUS 128" instead of just the source format. The new props
are optional, so existing usages in song lists, album songs, playlists,
and shares are unaffected.
* feat(ui): display transcode status in player quality badge
AudioTitle now reads the cached transcode decision for the current
track and passes it to QualityInfo, showing "FLAC → OPUS 128" when
transcoding or the normal format when direct playing.
* chore(ui): format and lint transcode decision integration
* refactor(ui): use JWT exp claim for decision cache expiry
Replace the hardcoded 11-hour TTL with actual token expiration
decoded from the JWT's exp claim. Each cache entry is now validated
against its own token's lifetime, adapting automatically to server
configuration changes. Tokens without an exp claim are treated as
expired and re-fetched immediately.
* fix(ui): resolve transcode URLs eagerly on browser refresh
Instead of setting musicSrc to a function on queue refresh (which
breaks the player's identity matching and can't survive JSON
serialization), resolve transcode decisions for the current and
next few tracks before dispatching, passing string URLs to the
reducer.
Also simplifies code: extract makeMusicSrc helper, add
resolveStreamUrl to decisionService, use httpClient instead of
raw fetch, and remove barrel file test.
* chore(ui): fix prettier formatting in Player.jsx
* fix(ui): use ref to avoid stale closure in mount-only transcode effect
Split the mount effect into profile detection + URL resolution, using a
ref for playerState so the effect correctly reads the latest queue without
needing playerState in the dependency array (which would cause it to
re-run on every queue/position change).
* fix(ui): address code review feedback on transcode integration
- Use jwt-decode for JWT parsing instead of manual atob (handles base64url)
- Guard resolveStreamUrl to fall back to direct stream when decision is null
- Fix savedPlayIndex -1 bug in PLAYER_REFRESH_QUEUE (findIndex returns -1)
* docs: improve comments on JWT exp claim decoding in decision service
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-09 07:06:31 -08:00
|
|
|
musicSrc: makeMusicSrc(trackId),
|
2021-07-02 10:07:03 -08:00
|
|
|
cover: subsonic.getCoverArtUrl(
|
|
|
|
|
{
|
2022-12-22 07:47:18 -09:00
|
|
|
id: trackId,
|
2021-07-02 10:07:03 -08:00
|
|
|
updatedAt: item.updatedAt,
|
2022-12-20 06:53:52 -09:00
|
|
|
album: item.album,
|
2021-07-02 10:07:03 -08:00
|
|
|
},
|
2023-12-18 10:56:03 -09:00
|
|
|
300,
|
2021-07-02 10:07:03 -08:00
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reduceClearQueue = () => ({ ...initialState, clear: true })
|
|
|
|
|
|
|
|
|
|
const reducePlayTracks = (state, { data, id }) => {
|
|
|
|
|
let playIndex = 0
|
|
|
|
|
const queue = Object.keys(data).map((key, idx) => {
|
|
|
|
|
if (key === id) {
|
|
|
|
|
playIndex = idx
|
|
|
|
|
}
|
|
|
|
|
return mapToAudioLists(data[key])
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
queue,
|
|
|
|
|
playIndex,
|
|
|
|
|
clear: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reduceSetTrack = (state, { data }) => {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
2021-07-03 18:29:59 -08:00
|
|
|
queue: [mapToAudioLists(data)],
|
2021-07-02 10:07:03 -08:00
|
|
|
playIndex: 0,
|
|
|
|
|
clear: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reduceAddTracks = (state, { data }) => {
|
|
|
|
|
const queue = state.queue
|
|
|
|
|
Object.keys(data).forEach((id) => {
|
|
|
|
|
queue.push(mapToAudioLists(data[id]))
|
|
|
|
|
})
|
|
|
|
|
return { ...state, queue, clear: false }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reducePlayNext = (state, { data }) => {
|
|
|
|
|
const newQueue = []
|
|
|
|
|
const current = state.current || {}
|
|
|
|
|
let foundPos = false
|
2026-02-17 04:34:24 -09:00
|
|
|
let currentIndex = 0
|
2021-07-02 10:07:03 -08:00
|
|
|
state.queue.forEach((item) => {
|
|
|
|
|
newQueue.push(item)
|
|
|
|
|
if (item.uuid === current.uuid) {
|
|
|
|
|
foundPos = true
|
2026-02-17 04:34:24 -09:00
|
|
|
currentIndex = newQueue.length - 1
|
2021-07-02 10:07:03 -08:00
|
|
|
Object.keys(data).forEach((id) => {
|
|
|
|
|
newQueue.push(mapToAudioLists(data[id]))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
if (!foundPos) {
|
|
|
|
|
Object.keys(data).forEach((id) => {
|
|
|
|
|
newQueue.push(mapToAudioLists(data[id]))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
queue: newQueue,
|
2026-02-17 04:34:24 -09:00
|
|
|
playIndex: foundPos ? currentIndex : undefined,
|
2021-07-02 10:07:03 -08:00
|
|
|
clear: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reduceSetVolume = (state, { data: { volume } }) => {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
volume,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-03 16:35:21 -08:00
|
|
|
const reduceSyncQueue = (state, { data: { audioInfo, audioLists } }) => {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
queue: audioLists,
|
2026-03-09 08:44:19 -08:00
|
|
|
// Keep clear and playIndex alive so the music player can still
|
|
|
|
|
// pick up a pending track selection set by PLAYER_PLAY_TRACKS.
|
|
|
|
|
// They will be consumed by the next PLAYER_CURRENT dispatch.
|
|
|
|
|
clear: state.playIndex != null ? state.clear : false,
|
|
|
|
|
playIndex: state.playIndex != null ? state.playIndex : undefined,
|
2021-07-03 16:35:21 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-02 10:07:03 -08:00
|
|
|
const reduceCurrent = (state, { data }) => {
|
2021-07-03 16:35:21 -08:00
|
|
|
const current = data.ended ? {} : data
|
|
|
|
|
const savedPlayIndex = state.queue.findIndex(
|
2023-12-18 10:56:03 -09:00
|
|
|
(item) => item.uuid === current.uuid,
|
2021-07-03 16:35:21 -08:00
|
|
|
)
|
2026-03-09 08:44:19 -08:00
|
|
|
// When a track selection is pending (playIndex is set), keep it alive
|
|
|
|
|
// until the music player confirms it actually switched to the requested
|
|
|
|
|
// track. Without this, a premature onAudioPlay callback for the
|
|
|
|
|
// still-playing old track would overwrite the pending selection.
|
|
|
|
|
const pending = state.playIndex != null && savedPlayIndex !== state.playIndex
|
2021-07-02 10:07:03 -08:00
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
current,
|
2026-03-09 08:44:19 -08:00
|
|
|
playIndex: pending ? state.playIndex : undefined,
|
|
|
|
|
clear: pending ? state.clear : false,
|
|
|
|
|
savedPlayIndex: pending ? state.savedPlayIndex : savedPlayIndex,
|
2021-07-02 10:07:03 -08:00
|
|
|
volume: data.volume,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-27 09:13:22 -08:00
|
|
|
const reduceMode = (state, { data: { mode } }) => {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
mode,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-02 10:07:03 -08:00
|
|
|
export const playerReducer = (previousState = initialState, payload) => {
|
|
|
|
|
const { type } = payload
|
|
|
|
|
switch (type) {
|
|
|
|
|
case PLAYER_CLEAR_QUEUE:
|
|
|
|
|
return reduceClearQueue()
|
|
|
|
|
case PLAYER_PLAY_TRACKS:
|
|
|
|
|
return reducePlayTracks(previousState, payload)
|
|
|
|
|
case PLAYER_SET_TRACK:
|
|
|
|
|
return reduceSetTrack(previousState, payload)
|
|
|
|
|
case PLAYER_ADD_TRACKS:
|
|
|
|
|
return reduceAddTracks(previousState, payload)
|
|
|
|
|
case PLAYER_PLAY_NEXT:
|
|
|
|
|
return reducePlayNext(previousState, payload)
|
|
|
|
|
case PLAYER_SET_VOLUME:
|
|
|
|
|
return reduceSetVolume(previousState, payload)
|
2021-07-03 16:35:21 -08:00
|
|
|
case PLAYER_SYNC_QUEUE:
|
|
|
|
|
return reduceSyncQueue(previousState, payload)
|
2021-07-02 10:07:03 -08:00
|
|
|
case PLAYER_CURRENT:
|
|
|
|
|
return reduceCurrent(previousState, payload)
|
2024-09-27 09:13:22 -08:00
|
|
|
case PLAYER_SET_MODE:
|
|
|
|
|
return reduceMode(previousState, payload)
|
feat(ui): integrate transcode decision into web player (#5155)
* feat(ui): add browser audio profile detection for transcoding
Detect browser codec capabilities via canPlayType() to build a client
profile for the getTranscodeDecision API. Only codecs returning "probably"
are treated as supported for conservative compatibility.
* feat(ui): add transcode decision service with caching and pre-fetch
Standalone service that fetches getTranscodeDecision results, caches
them with an 11-hour TTL (1h buffer before 12h token expiry), and
supports bulk pre-fetching for upcoming queue items. Includes
invalidateAll() for handling stale tokens and getCachedDecision()
for synchronous cache reads.
* feat(ui): add fetch helper for getTranscodeDecision endpoint
POST-based Subsonic API call that sends the browser's codec profile
and returns the transcode decision including the JWT transcodeParams
token for subsequent streaming.
* feat(ui): wire transcode decision service singleton
Module index that creates the service singleton with the real fetch
function and re-exports the browser profile detector.
* feat(ui): add Redux transcoding reducer for browser profile state
Store the detected browser codec profile in Redux so it's available
globally. The profile is set once at startup and used by the decision
service when calling getTranscodeDecision.
* feat(ui): integrate transcode decision into player musicSrc
Replace static stream URLs with lazy musicSrc functions that fetch
a transcode decision before playback. Falls back to the old stream
endpoint if the decision fetch fails or if no browser profile is set.
* feat(ui): detect browser profile and pre-fetch transcode decisions
Run codec detection once when the Player mounts, storing the profile
in both the decision service and Redux. Pre-fetch decisions for the
next 3 songs when the queue or play position changes.
* feat(ui): handle stale tokens and replace audio preload with decision pre-fetch
On audio playback error, invalidate all cached transcode decisions
and pre-fetch fresh decisions for upcoming songs. Replace the old
Audio element preload with decision pre-fetching to warm the cache
for instant playback transitions.
* feat(ui): show transcode format in QualityInfo chip
When transcode decision data is available, QualityInfo now shows
"FLAC → OPUS 128" instead of just the source format. The new props
are optional, so existing usages in song lists, album songs, playlists,
and shares are unaffected.
* feat(ui): display transcode status in player quality badge
AudioTitle now reads the cached transcode decision for the current
track and passes it to QualityInfo, showing "FLAC → OPUS 128" when
transcoding or the normal format when direct playing.
* chore(ui): format and lint transcode decision integration
* refactor(ui): use JWT exp claim for decision cache expiry
Replace the hardcoded 11-hour TTL with actual token expiration
decoded from the JWT's exp claim. Each cache entry is now validated
against its own token's lifetime, adapting automatically to server
configuration changes. Tokens without an exp claim are treated as
expired and re-fetched immediately.
* fix(ui): resolve transcode URLs eagerly on browser refresh
Instead of setting musicSrc to a function on queue refresh (which
breaks the player's identity matching and can't survive JSON
serialization), resolve transcode decisions for the current and
next few tracks before dispatching, passing string URLs to the
reducer.
Also simplifies code: extract makeMusicSrc helper, add
resolveStreamUrl to decisionService, use httpClient instead of
raw fetch, and remove barrel file test.
* chore(ui): fix prettier formatting in Player.jsx
* fix(ui): use ref to avoid stale closure in mount-only transcode effect
Split the mount effect into profile detection + URL resolution, using a
ref for playerState so the effect correctly reads the latest queue without
needing playerState in the dependency array (which would cause it to
re-run on every queue/position change).
* fix(ui): address code review feedback on transcode integration
- Use jwt-decode for JWT parsing instead of manual atob (handles base64url)
- Guard resolveStreamUrl to fall back to direct stream when decision is null
- Fix savedPlayIndex -1 bug in PLAYER_REFRESH_QUEUE (findIndex returns -1)
* docs: improve comments on JWT exp claim decoding in decision service
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-09 07:06:31 -08:00
|
|
|
case PLAYER_REFRESH_QUEUE: {
|
|
|
|
|
const resolvedUrls = payload.data || {}
|
|
|
|
|
return {
|
|
|
|
|
...previousState,
|
|
|
|
|
queue: previousState.queue.map((item) => ({
|
|
|
|
|
...item,
|
|
|
|
|
musicSrc: item.isRadio
|
|
|
|
|
? item.musicSrc
|
|
|
|
|
: resolvedUrls[item.trackId] || subsonic.streamUrl(item.trackId),
|
|
|
|
|
})),
|
|
|
|
|
clear: true,
|
|
|
|
|
autoPlay: false,
|
|
|
|
|
playIndex:
|
|
|
|
|
previousState.savedPlayIndex >= 0 ? previousState.savedPlayIndex : 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-07-02 10:07:03 -08:00
|
|
|
default:
|
|
|
|
|
return previousState
|
|
|
|
|
}
|
|
|
|
|
}
|