provide transcoding support
This commit is contained in:
parent
da95a644c8
commit
528bef01f0
24 changed files with 347 additions and 69 deletions
|
@ -632,6 +632,13 @@
|
||||||
"themeDark_description": "sets the dark theme to use for the application",
|
"themeDark_description": "sets the dark theme to use for the application",
|
||||||
"themeLight": "theme (light)",
|
"themeLight": "theme (light)",
|
||||||
"themeLight_description": "sets the light theme to use for the application",
|
"themeLight_description": "sets the light theme to use for the application",
|
||||||
|
"transcodeNote": "takes effect after 1 (web) - 2 (mpv) songs",
|
||||||
|
"transcode": "enable transcoding",
|
||||||
|
"transcode_description": "enables transcoding to different formats",
|
||||||
|
"transcodeBitrate": "bitrate to transcode",
|
||||||
|
"transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick",
|
||||||
|
"transcodeFormat": "format to transcode",
|
||||||
|
"transcodeFormat_description": "selects the format to transcode. leave empty to let the server decide",
|
||||||
"useSystemTheme": "use system theme",
|
"useSystemTheme": "use system theme",
|
||||||
"useSystemTheme_description": "follow the system-defined light or dark preference",
|
"useSystemTheme_description": "follow the system-defined light or dark preference",
|
||||||
"volumeWheelStep": "volume wheel step",
|
"volumeWheelStep": "volume wheel step",
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { app, ipcMain } from 'electron';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import MpvAPI from 'node-mpv';
|
import MpvAPI from 'node-mpv';
|
||||||
import { getMainWindow, sendToastToRenderer } from '../../../main';
|
import { getMainWindow, sendToastToRenderer } from '../../../main';
|
||||||
import { PlayerData } from '/@/renderer/store';
|
|
||||||
import { createLog, isWindows } from '../../../utils';
|
import { createLog, isWindows } from '../../../utils';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
|
||||||
|
@ -315,8 +314,8 @@ ipcMain.on('player-seek-to', async (_event, time: number) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
|
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
|
||||||
ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => {
|
ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, pause?: boolean) => {
|
||||||
if (!data.queue.current?.id && !data.queue.next?.id) {
|
if (!current && !next) {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.clearPlaylist();
|
await getMpvInstance()?.clearPlaylist();
|
||||||
await getMpvInstance()?.pause();
|
await getMpvInstance()?.pause();
|
||||||
|
@ -327,15 +326,15 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (data.queue.current?.streamUrl) {
|
if (current) {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.load(data.queue.current.streamUrl, 'replace');
|
await getMpvInstance()?.load(current, 'replace');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await getMpvInstance()?.play();
|
await getMpvInstance()?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.queue.next?.streamUrl) {
|
if (next) {
|
||||||
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
await getMpvInstance()?.load(next, 'append');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,7 +350,7 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replaces the queue in position 1 to the given data
|
// Replaces the queue in position 1 to the given data
|
||||||
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
|
ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
|
||||||
try {
|
try {
|
||||||
const size = await getMpvInstance()?.getPlaylistSize();
|
const size = await getMpvInstance()?.getPlaylistSize();
|
||||||
|
|
||||||
|
@ -363,8 +362,8 @@ ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
|
||||||
await getMpvInstance()?.playlistRemove(1);
|
await getMpvInstance()?.playlistRemove(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.queue.next?.streamUrl) {
|
if (url) {
|
||||||
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
await getMpvInstance()?.load(url, 'append');
|
||||||
}
|
}
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: NodeMpvError | any) {
|
||||||
mpvLog({ action: `Failed to set play queue` }, err);
|
mpvLog({ action: `Failed to set play queue` }, err);
|
||||||
|
@ -372,7 +371,7 @@ ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sets the next song in the queue when reaching the end of the queue
|
// Sets the next song in the queue when reaching the end of the queue
|
||||||
ipcMain.on('player-auto-next', async (_event, data: PlayerData) => {
|
ipcMain.on('player-auto-next', async (_event, url?: string) => {
|
||||||
// Always keep the current song as position 0 in the mpv queue
|
// Always keep the current song as position 0 in the mpv queue
|
||||||
// This allows us to easily set update the next song in the queue without
|
// This allows us to easily set update the next song in the queue without
|
||||||
// disturbing the currently playing song
|
// disturbing the currently playing song
|
||||||
|
@ -383,8 +382,8 @@ ipcMain.on('player-auto-next', async (_event, data: PlayerData) => {
|
||||||
getMpvInstance()?.pause();
|
getMpvInstance()?.pause();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.queue.next?.streamUrl) {
|
if (url) {
|
||||||
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
await getMpvInstance()?.load(url, 'append');
|
||||||
}
|
}
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: NodeMpvError | any) {
|
||||||
mpvLog({ action: `Failed to load next song` }, err);
|
mpvLog({ action: `Failed to load next song` }, err);
|
||||||
|
|
|
@ -25,8 +25,8 @@ const setProperties = (data: Record<string, any>) => {
|
||||||
ipcRenderer.send('player-set-properties', data);
|
ipcRenderer.send('player-set-properties', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const autoNext = (data: PlayerData) => {
|
const autoNext = (url?: string) => {
|
||||||
ipcRenderer.send('player-auto-next', data);
|
ipcRenderer.send('player-auto-next', url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentTime = () => {
|
const currentTime = () => {
|
||||||
|
@ -61,12 +61,12 @@ const seekTo = (seconds: number) => {
|
||||||
ipcRenderer.send('player-seek-to', seconds);
|
ipcRenderer.send('player-seek-to', seconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setQueue = (data: PlayerData, pause?: boolean) => {
|
const setQueue = (current?: string, next?: string, pause?: boolean) => {
|
||||||
ipcRenderer.send('player-set-queue', data, pause);
|
ipcRenderer.send('player-set-queue', current, next, pause);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setQueueNext = (data: PlayerData) => {
|
const setQueueNext = (url?: string) => {
|
||||||
ipcRenderer.send('player-set-queue-next', data);
|
ipcRenderer.send('player-set-queue-next', url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
|
|
|
@ -59,6 +59,7 @@ import type {
|
||||||
ShareItemResponse,
|
ShareItemResponse,
|
||||||
MoveItemArgs,
|
MoveItemArgs,
|
||||||
DownloadArgs,
|
DownloadArgs,
|
||||||
|
TranscodingArgs,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
||||||
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
|
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||||
|
@ -102,6 +103,7 @@ export type ControllerEndpoint = Partial<{
|
||||||
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
|
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
|
||||||
getStructuredLyrics: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
getStructuredLyrics: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
||||||
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
||||||
|
getTranscodingUrl: (args: TranscodingArgs) => string;
|
||||||
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
|
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
|
||||||
movePlaylistItem: (args: MoveItemArgs) => Promise<void>;
|
movePlaylistItem: (args: MoveItemArgs) => Promise<void>;
|
||||||
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
||||||
|
@ -152,6 +154,7 @@ const endpoints: ApiController = {
|
||||||
getSongList: jfController.getSongList,
|
getSongList: jfController.getSongList,
|
||||||
getStructuredLyrics: undefined,
|
getStructuredLyrics: undefined,
|
||||||
getTopSongs: jfController.getTopSongList,
|
getTopSongs: jfController.getTopSongList,
|
||||||
|
getTranscodingUrl: jfController.getTranscodingUrl,
|
||||||
getUserList: undefined,
|
getUserList: undefined,
|
||||||
movePlaylistItem: jfController.movePlaylistItem,
|
movePlaylistItem: jfController.movePlaylistItem,
|
||||||
removeFromPlaylist: jfController.removeFromPlaylist,
|
removeFromPlaylist: jfController.removeFromPlaylist,
|
||||||
|
@ -194,6 +197,7 @@ const endpoints: ApiController = {
|
||||||
getSongList: ndController.getSongList,
|
getSongList: ndController.getSongList,
|
||||||
getStructuredLyrics: ssController.getStructuredLyrics,
|
getStructuredLyrics: ssController.getStructuredLyrics,
|
||||||
getTopSongs: ssController.getTopSongList,
|
getTopSongs: ssController.getTopSongList,
|
||||||
|
getTranscodingUrl: ssController.getTranscodingUrl,
|
||||||
getUserList: ndController.getUserList,
|
getUserList: ndController.getUserList,
|
||||||
movePlaylistItem: ndController.movePlaylistItem,
|
movePlaylistItem: ndController.movePlaylistItem,
|
||||||
removeFromPlaylist: ndController.removeFromPlaylist,
|
removeFromPlaylist: ndController.removeFromPlaylist,
|
||||||
|
@ -233,6 +237,7 @@ const endpoints: ApiController = {
|
||||||
getSongList: undefined,
|
getSongList: undefined,
|
||||||
getStructuredLyrics: ssController.getStructuredLyrics,
|
getStructuredLyrics: ssController.getStructuredLyrics,
|
||||||
getTopSongs: ssController.getTopSongList,
|
getTopSongs: ssController.getTopSongList,
|
||||||
|
getTranscodingUrl: ssController.getTranscodingUrl,
|
||||||
getUserList: undefined,
|
getUserList: undefined,
|
||||||
scrobble: ssController.scrobble,
|
scrobble: ssController.scrobble,
|
||||||
search: ssController.search3,
|
search: ssController.search3,
|
||||||
|
@ -568,6 +573,15 @@ const getDownloadUrl = (args: DownloadArgs) => {
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTranscodingUrl = (args: TranscodingArgs) => {
|
||||||
|
return (
|
||||||
|
apiController(
|
||||||
|
'getTranscodingUrl',
|
||||||
|
args.apiClientProps.server?.type,
|
||||||
|
) as ControllerEndpoint['getTranscodingUrl']
|
||||||
|
)?.(args);
|
||||||
|
};
|
||||||
|
|
||||||
export const controller = {
|
export const controller = {
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -594,6 +608,7 @@ export const controller = {
|
||||||
getSongList,
|
getSongList,
|
||||||
getStructuredLyrics,
|
getStructuredLyrics,
|
||||||
getTopSongList,
|
getTopSongList,
|
||||||
|
getTranscodingUrl,
|
||||||
getUserList,
|
getUserList,
|
||||||
movePlaylistItem,
|
movePlaylistItem,
|
||||||
removeFromPlaylist,
|
removeFromPlaylist,
|
||||||
|
|
|
@ -55,6 +55,7 @@ import {
|
||||||
Song,
|
Song,
|
||||||
MoveItemArgs,
|
MoveItemArgs,
|
||||||
DownloadArgs,
|
DownloadArgs,
|
||||||
|
TranscodingArgs,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
||||||
import { jfNormalize } from './jellyfin-normalize';
|
import { jfNormalize } from './jellyfin-normalize';
|
||||||
|
@ -1050,6 +1051,20 @@ const getDownloadUrl = (args: DownloadArgs) => {
|
||||||
return `${apiClientProps.server?.url}/items/${query.id}/download?api_key=${apiClientProps.server?.credential}`;
|
return `${apiClientProps.server?.url}/items/${query.id}/download?api_key=${apiClientProps.server?.credential}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTranscodingUrl = (args: TranscodingArgs) => {
|
||||||
|
const { base, format, bitrate } = args.query;
|
||||||
|
let url = base.replace('transcodingProtocol=hls', 'transcodingProtocol=http');
|
||||||
|
if (format) {
|
||||||
|
url = url.replace('audioCodec=aac', `audioCodec=${format}`);
|
||||||
|
url = url.replace('transcodingContainer=ts', `transcodingContainer=${format}`);
|
||||||
|
}
|
||||||
|
if (bitrate !== undefined) {
|
||||||
|
url += `&maxStreamingBitrate=${bitrate * 1000}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
export const jfController = {
|
export const jfController = {
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -1075,6 +1090,7 @@ export const jfController = {
|
||||||
getSongDetail,
|
getSongDetail,
|
||||||
getSongList,
|
getSongList,
|
||||||
getTopSongList,
|
getTopSongList,
|
||||||
|
getTranscodingUrl,
|
||||||
movePlaylistItem,
|
movePlaylistItem,
|
||||||
removeFromPlaylist,
|
removeFromPlaylist,
|
||||||
scrobble,
|
scrobble,
|
||||||
|
|
|
@ -34,7 +34,7 @@ const getStreamUrl = (args: {
|
||||||
`&playSessionId=${deviceId}` +
|
`&playSessionId=${deviceId}` +
|
||||||
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
|
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
|
||||||
'&transcodingContainer=ts' +
|
'&transcodingContainer=ts' +
|
||||||
'&transcodingProtocol=hls'
|
'&transcodingProtocol=http'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { ndType } from './navidrome-types';
|
||||||
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
|
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { ServerListItem } from '/@/renderer/api/types';
|
import { ServerListItem } from '/@/renderer/api/types';
|
||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components/toast';
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
SimilarSongsArgs,
|
SimilarSongsArgs,
|
||||||
Song,
|
Song,
|
||||||
DownloadArgs,
|
DownloadArgs,
|
||||||
|
TranscodingArgs,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { randomString } from '/@/renderer/utils';
|
import { randomString } from '/@/renderer/utils';
|
||||||
import { ServerFeatures } from '/@/renderer/api/features-types';
|
import { ServerFeatures } from '/@/renderer/api/features-types';
|
||||||
|
@ -495,6 +496,19 @@ const getDownloadUrl = (args: DownloadArgs) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTranscodingUrl = (args: TranscodingArgs) => {
|
||||||
|
const { base, format, bitrate } = args.query;
|
||||||
|
let url = base;
|
||||||
|
if (format) {
|
||||||
|
url += `&format=${format}`;
|
||||||
|
}
|
||||||
|
if (bitrate !== undefined) {
|
||||||
|
url += `&maxBitRate=${bitrate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
export const ssController = {
|
export const ssController = {
|
||||||
authenticate,
|
authenticate,
|
||||||
createFavorite,
|
createFavorite,
|
||||||
|
@ -506,6 +520,7 @@ export const ssController = {
|
||||||
getSimilarSongs,
|
getSimilarSongs,
|
||||||
getStructuredLyrics,
|
getStructuredLyrics,
|
||||||
getTopSongList,
|
getTopSongList,
|
||||||
|
getTranscodingUrl,
|
||||||
removeFavorite,
|
removeFavorite,
|
||||||
scrobble,
|
scrobble,
|
||||||
search3,
|
search3,
|
||||||
|
|
|
@ -1211,3 +1211,13 @@ export type DownloadQuery = {
|
||||||
export type DownloadArgs = {
|
export type DownloadArgs = {
|
||||||
query: DownloadQuery;
|
query: DownloadQuery;
|
||||||
} & BaseEndpointArgs;
|
} & BaseEndpointArgs;
|
||||||
|
|
||||||
|
export type TranscodingQuery = {
|
||||||
|
base: string;
|
||||||
|
bitrate?: number;
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TranscodingArgs = {
|
||||||
|
query: TranscodingQuery;
|
||||||
|
} & BaseEndpointArgs;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import isElectron from 'is-electron';
|
||||||
import semverCoerce from 'semver/functions/coerce';
|
import semverCoerce from 'semver/functions/coerce';
|
||||||
import semverGte from 'semver/functions/gte';
|
import semverGte from 'semver/functions/gte';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components/toast';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { ServerListItem } from '/@/renderer/api/types';
|
import { ServerListItem } from '/@/renderer/api/types';
|
||||||
import { ServerFeature } from '/@/renderer/api/features-types';
|
import { ServerFeature } from '/@/renderer/api/features-types';
|
||||||
|
|
|
@ -28,6 +28,7 @@ import i18n from '/@/i18n/i18n';
|
||||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||||
|
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
|
|
||||||
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||||
|
|
||||||
|
@ -185,7 +186,7 @@ export const App = () => {
|
||||||
utils.onRestoreQueue((_event: any, data) => {
|
utils.onRestoreQueue((_event: any, data) => {
|
||||||
const playerData = restoreQueue(data);
|
const playerData = restoreQueue(data);
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
setQueue(playerData, true);
|
||||||
}
|
}
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { useImperativeHandle, forwardRef, useRef, useState, useCallback, useEffect } from 'react';
|
import {
|
||||||
|
useImperativeHandle,
|
||||||
|
forwardRef,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import type { ReactPlayerProps } from 'react-player';
|
import type { ReactPlayerProps } from 'react-player';
|
||||||
import ReactPlayer from 'react-player/lazy';
|
import ReactPlayer from 'react-player/lazy';
|
||||||
|
@ -10,16 +18,17 @@ import {
|
||||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
import type { CrossfadeStyle } from '/@/renderer/types';
|
import type { CrossfadeStyle } from '/@/renderer/types';
|
||||||
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
||||||
import { useSpeed } from '/@/renderer/store';
|
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
|
||||||
import { toast } from '/@/renderer/components/toast';
|
import { toast } from '/@/renderer/components/toast';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
|
||||||
interface AudioPlayerProps extends ReactPlayerProps {
|
interface AudioPlayerProps extends ReactPlayerProps {
|
||||||
crossfadeDuration: number;
|
crossfadeDuration: number;
|
||||||
crossfadeStyle: CrossfadeStyle;
|
crossfadeStyle: CrossfadeStyle;
|
||||||
currentPlayer: 1 | 2;
|
currentPlayer: 1 | 2;
|
||||||
playbackStyle: PlaybackStyle;
|
playbackStyle: PlaybackStyle;
|
||||||
player1: Song;
|
player1?: Song;
|
||||||
player2: Song;
|
player2?: Song;
|
||||||
status: PlayerStatus;
|
status: PlayerStatus;
|
||||||
volume: number;
|
volume: number;
|
||||||
}
|
}
|
||||||
|
@ -48,6 +57,44 @@ type WebAudio = {
|
||||||
const EMPTY_SOURCE =
|
const EMPTY_SOURCE =
|
||||||
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
|
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
|
||||||
|
|
||||||
|
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): string | null => {
|
||||||
|
const prior = useRef(['', '']);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (song?.serverId) {
|
||||||
|
// If we are the current track, we do not want a transcoding
|
||||||
|
// reconfiguration to force a restart.
|
||||||
|
if (current && prior.current[0] === song.uniqueId) {
|
||||||
|
return prior.current[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transcode.enabled) {
|
||||||
|
// transcoding disabled; save the result
|
||||||
|
prior.current = [song.uniqueId, song.streamUrl];
|
||||||
|
return song.streamUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = api.controller.getTranscodingUrl({
|
||||||
|
apiClientProps: {
|
||||||
|
server: getServerById(song.serverId),
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
base: song.streamUrl,
|
||||||
|
...transcode,
|
||||||
|
},
|
||||||
|
})!;
|
||||||
|
|
||||||
|
// transcoding enabled; save the updated result
|
||||||
|
prior.current = [song.uniqueId, result];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no track; clear result
|
||||||
|
prior.current = ['', ''];
|
||||||
|
return null;
|
||||||
|
}, [current, song?.uniqueId, song?.serverId, song?.streamUrl, transcode]);
|
||||||
|
};
|
||||||
|
|
||||||
export const AudioPlayer = forwardRef(
|
export const AudioPlayer = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
@ -72,6 +119,10 @@ export const AudioPlayer = forwardRef(
|
||||||
const useWebAudio = useSettingsStore((state) => state.playback.webAudio);
|
const useWebAudio = useSettingsStore((state) => state.playback.webAudio);
|
||||||
const { resetSampleRate } = useSettingsStoreActions();
|
const { resetSampleRate } = useSettingsStoreActions();
|
||||||
const playbackSpeed = useSpeed();
|
const playbackSpeed = useSpeed();
|
||||||
|
const { transcode } = usePlaybackSettings();
|
||||||
|
|
||||||
|
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
|
||||||
|
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
|
||||||
|
|
||||||
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
|
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
|
||||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
|
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
|
||||||
|
@ -374,11 +425,11 @@ export const AudioPlayer = forwardRef(
|
||||||
playbackRate={playbackSpeed}
|
playbackRate={playbackSpeed}
|
||||||
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
||||||
progressInterval={isTransitioning ? 10 : 250}
|
progressInterval={isTransitioning ? 10 : 250}
|
||||||
url={player1?.streamUrl || EMPTY_SOURCE}
|
url={stream1 || EMPTY_SOURCE}
|
||||||
volume={webAudio ? 1 : volume}
|
volume={webAudio ? 1 : volume}
|
||||||
width={0}
|
width={0}
|
||||||
// If there is no stream url, we do not need to handle when the audio finishes
|
// If there is no stream url, we do not need to handle when the audio finishes
|
||||||
onEnded={player1?.streamUrl ? handleOnEnded : undefined}
|
onEnded={stream1 ? handleOnEnded : undefined}
|
||||||
onProgress={
|
onProgress={
|
||||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
||||||
}
|
}
|
||||||
|
@ -394,10 +445,10 @@ export const AudioPlayer = forwardRef(
|
||||||
playbackRate={playbackSpeed}
|
playbackRate={playbackSpeed}
|
||||||
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
||||||
progressInterval={isTransitioning ? 10 : 250}
|
progressInterval={isTransitioning ? 10 : 250}
|
||||||
url={player2?.streamUrl || EMPTY_SOURCE}
|
url={stream2 || EMPTY_SOURCE}
|
||||||
volume={webAudio ? 1 : volume}
|
volume={webAudio ? 1 : volume}
|
||||||
width={0}
|
width={0}
|
||||||
onEnded={player2?.streamUrl ? handleOnEnded : undefined}
|
onEnded={stream2 ? handleOnEnded : undefined}
|
||||||
onProgress={
|
onProgress={
|
||||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ import { ItemDetailsModal } from '/@/renderer/features/item-details/components/i
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
import { controller } from '/@/renderer/api/controller';
|
import { controller } from '/@/renderer/api/controller';
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
|
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
|
|
||||||
type ContextMenuContextProps = {
|
type ContextMenuContextProps = {
|
||||||
closeContextMenu: () => void;
|
closeContextMenu: () => void;
|
||||||
|
@ -92,7 +93,6 @@ const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating', 'shareI
|
||||||
// const NAVIDROME_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
// const NAVIDROME_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
||||||
// const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
// const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
|
||||||
const utils = isElectron() ? window.electron.utils : null;
|
const utils = isElectron() ? window.electron.utils : null;
|
||||||
|
|
||||||
export interface ContextMenuProviderProps {
|
export interface ContextMenuProviderProps {
|
||||||
|
@ -610,7 +610,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
const playerData = moveToBottomOfQueue(uniqueIds);
|
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
}, [ctx.dataNodes, moveToBottomOfQueue, playbackType]);
|
}, [ctx.dataNodes, moveToBottomOfQueue, playbackType]);
|
||||||
|
|
||||||
|
@ -621,7 +621,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
const playerData = moveToTopOfQueue(uniqueIds);
|
const playerData = moveToTopOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
}, [ctx.dataNodes, moveToTopOfQueue, playbackType]);
|
}, [ctx.dataNodes, moveToTopOfQueue, playbackType]);
|
||||||
|
|
||||||
|
@ -651,9 +651,9 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
if (isCurrentSongRemoved) {
|
if (isCurrentSongRemoved) {
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
} else {
|
} else {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,7 +687,9 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
},
|
},
|
||||||
query: { albumArtistIds: item.albumArtistIds, songId: item.id },
|
query: { albumArtistIds: item.albumArtistIds, songId: item.id },
|
||||||
});
|
});
|
||||||
handlePlayQueueAdd?.({ byData: [ctx.data[0], ...songs], playType: Play.NOW });
|
if (songs) {
|
||||||
|
handlePlayQueueAdd?.({ byData: [ctx.data[0], ...songs], playType: Play.NOW });
|
||||||
|
}
|
||||||
}, [ctx, handlePlayQueueAdd]);
|
}, [ctx, handlePlayQueueAdd]);
|
||||||
|
|
||||||
const handleDownload = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store';
|
import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store';
|
||||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
const playerData = moveToBottomOfQueue(uniqueIds);
|
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
const playerData = moveToTopOfQueue(uniqueIds);
|
const playerData = moveToTopOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,9 +73,9 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
if (isCurrentSongRemoved) {
|
if (isCurrentSongRemoved) {
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
} else {
|
} else {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
const playerData = clearQueue();
|
const playerData = clearQueue();
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
mpvPlayer!.pause();
|
mpvPlayer!.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +102,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
const playerData = shuffleQueue();
|
const playerData = shuffleQueue();
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-gr
|
||||||
import { useAppFocus } from '/@/renderer/hooks';
|
import { useAppFocus } from '/@/renderer/hooks';
|
||||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
|
@ -86,7 +87,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.volume(volume);
|
mpvPlayer!.volume(volume);
|
||||||
mpvPlayer!.setQueue(playerData, false);
|
setQueue(playerData, false);
|
||||||
} else {
|
} else {
|
||||||
const player =
|
const player =
|
||||||
playerData.current.player === 1
|
playerData.current.player === 1
|
||||||
|
@ -117,7 +118,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
|
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'sideDrawerQueue') {
|
if (type === 'sideDrawerQueue') {
|
||||||
|
|
|
@ -137,7 +137,9 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
if (!isElectron() || playbackType === PlaybackType.WEB) {
|
if (!isElectron() || playbackType === PlaybackType.WEB) {
|
||||||
// Update twice a second for slightly better performance
|
// Update twice a second for slightly better performance
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
setCurrentTime(currentPlayerRef.getCurrentTime());
|
if (currentPlayerRef) {
|
||||||
|
setCurrentTime(currentPlayerRef.getCurrentTime());
|
||||||
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import debounce from 'lodash/debounce';
|
||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
import { setAutoNext, setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
||||||
|
@ -131,30 +132,30 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
if (shuffleStatus === PlayerShuffle.NONE) {
|
if (shuffleStatus === PlayerShuffle.NONE) {
|
||||||
const playerData = setShuffle(PlayerShuffle.TRACK);
|
const playerData = setShuffle(PlayerShuffle.TRACK);
|
||||||
remote?.updateShuffle(true);
|
remote?.updateShuffle(true);
|
||||||
return mpvPlayer?.setQueueNext(playerData);
|
return setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerData = setShuffle(PlayerShuffle.NONE);
|
const playerData = setShuffle(PlayerShuffle.NONE);
|
||||||
remote?.updateShuffle(false);
|
remote?.updateShuffle(false);
|
||||||
return mpvPlayer?.setQueueNext(playerData);
|
return setQueueNext(playerData);
|
||||||
}, [setShuffle, shuffleStatus]);
|
}, [setShuffle, shuffleStatus]);
|
||||||
|
|
||||||
const handleToggleRepeat = useCallback(() => {
|
const handleToggleRepeat = useCallback(() => {
|
||||||
if (repeatStatus === PlayerRepeat.NONE) {
|
if (repeatStatus === PlayerRepeat.NONE) {
|
||||||
const playerData = setRepeat(PlayerRepeat.ALL);
|
const playerData = setRepeat(PlayerRepeat.ALL);
|
||||||
remote?.updateRepeat(PlayerRepeat.ALL);
|
remote?.updateRepeat(PlayerRepeat.ALL);
|
||||||
return mpvPlayer?.setQueueNext(playerData);
|
return setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repeatStatus === PlayerRepeat.ALL) {
|
if (repeatStatus === PlayerRepeat.ALL) {
|
||||||
const playerData = setRepeat(PlayerRepeat.ONE);
|
const playerData = setRepeat(PlayerRepeat.ONE);
|
||||||
remote?.updateRepeat(PlayerRepeat.ONE);
|
remote?.updateRepeat(PlayerRepeat.ONE);
|
||||||
return mpvPlayer?.setQueueNext(playerData);
|
return setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerData = setRepeat(PlayerRepeat.NONE);
|
const playerData = setRepeat(PlayerRepeat.NONE);
|
||||||
remote?.updateRepeat(PlayerRepeat.NONE);
|
remote?.updateRepeat(PlayerRepeat.NONE);
|
||||||
return mpvPlayer?.setQueueNext(playerData);
|
return setQueueNext(playerData);
|
||||||
}, [repeatStatus, setRepeat]);
|
}, [repeatStatus, setRepeat]);
|
||||||
|
|
||||||
const checkIsLastTrack = useCallback(() => {
|
const checkIsLastTrack = useCallback(() => {
|
||||||
|
@ -172,7 +173,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.autoNext(playerData);
|
setAutoNext(playerData);
|
||||||
play();
|
play();
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
|
@ -186,12 +187,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
setQueue(playerData, true);
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.autoNext(playerData);
|
setAutoNext(playerData);
|
||||||
play();
|
play();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -211,7 +212,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.autoNext(playerData);
|
setAutoNext(playerData);
|
||||||
play();
|
play();
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
|
@ -257,7 +258,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
|
@ -270,12 +271,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
setQueue(playerData, true);
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
|
@ -297,7 +298,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
if (!isLastTrack) {
|
if (!isLastTrack) {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
|
@ -358,11 +359,11 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
if (!isFirstTrack) {
|
if (!isFirstTrack) {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
} else {
|
} else {
|
||||||
const playerData = setCurrentIndex(queue.length - 1);
|
const playerData = setCurrentIndex(queue.length - 1);
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
|
@ -383,12 +384,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
if (isFirstTrack) {
|
if (isFirstTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
setQueue(playerData, true);
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
|
@ -407,7 +408,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
updateSong(playerData.current.song);
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
setQueue(playerData);
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
|
|
||||||
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
||||||
let queryKey;
|
let queryKey;
|
||||||
|
@ -180,9 +181,9 @@ export const useHandlePlayQueueAdd = () => {
|
||||||
|
|
||||||
if (playType === Play.NOW || !hadSong) {
|
if (playType === Play.NOW || !hadSong) {
|
||||||
mpvPlayer!.pause();
|
mpvPlayer!.pause();
|
||||||
mpvPlayer!.setQueue(playerData, false);
|
setQueue(playerData, false);
|
||||||
} else {
|
} else {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const player =
|
const player =
|
||||||
|
|
|
@ -10,8 +10,7 @@ import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types';
|
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
|
||||||
|
|
||||||
const getAudioDevice = async () => {
|
const getAudioDevice = async () => {
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
@ -62,7 +61,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||||
setSettings({ playback: { ...settings, type: e as PlaybackType } });
|
setSettings({ playback: { ...settings, type: e as PlaybackType } });
|
||||||
if (isElectron() && e === PlaybackType.LOCAL) {
|
if (isElectron() && e === PlaybackType.LOCAL) {
|
||||||
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
||||||
mpvPlayer!.setQueue(queueData);
|
setQueue(queueData);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import isElectron from 'is-electron';
|
||||||
import { LyricSettings } from '/@/renderer/features/settings/components/playback/lyric-settings';
|
import { LyricSettings } from '/@/renderer/features/settings/components/playback/lyric-settings';
|
||||||
import { useSettingsStore } from '/@/renderer/store';
|
import { useSettingsStore } from '/@/renderer/store';
|
||||||
import { PlaybackType } from '/@/renderer/types';
|
import { PlaybackType } from '/@/renderer/types';
|
||||||
|
import { TranscodeSettings } from '/@/renderer/features/settings/components/playback/transcode-settings';
|
||||||
|
|
||||||
const MpvSettings = lazy(() =>
|
const MpvSettings = lazy(() =>
|
||||||
import('/@/renderer/features/settings/components/playback/mpv-settings').then((module) => {
|
import('/@/renderer/features/settings/components/playback/mpv-settings').then((module) => {
|
||||||
|
@ -28,6 +29,7 @@ export const PlaybackTab = () => {
|
||||||
<Stack spacing="md">
|
<Stack spacing="md">
|
||||||
<AudioSettings hasFancyAudio={hasFancyAudio} />
|
<AudioSettings hasFancyAudio={hasFancyAudio} />
|
||||||
<Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense>
|
<Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense>
|
||||||
|
<TranscodeSettings />
|
||||||
<ScrobbleSettings />
|
<ScrobbleSettings />
|
||||||
<LyricSettings />
|
<LyricSettings />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { NumberInput, Switch, TextInput } from '/@/renderer/components';
|
||||||
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
import { SettingOption, SettingsSection } from '../settings-section';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const TranscodeSettings = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { transcode } = usePlaybackSettings();
|
||||||
|
const { setTranscodingConfig } = useSettingsStoreActions();
|
||||||
|
const note = t('setting.transcodeNote', { postProcess: 'sentenceCase' });
|
||||||
|
|
||||||
|
const transcodeOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Toggle transcode"
|
||||||
|
defaultChecked={transcode.enabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTranscodingConfig({
|
||||||
|
...transcode,
|
||||||
|
enabled: e.currentTarget.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.transcode', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
note,
|
||||||
|
title: t('setting.transcode', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
aria-label="Transcode bitrate"
|
||||||
|
defaultValue={transcode.bitrate}
|
||||||
|
min={0}
|
||||||
|
placeholder="mp3, opus"
|
||||||
|
w={100}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setTranscodingConfig({
|
||||||
|
...transcode,
|
||||||
|
bitrate: e.currentTarget.value
|
||||||
|
? Number(e.currentTarget.value)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.transcodeBitrate', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !transcode.enabled,
|
||||||
|
note,
|
||||||
|
title: t('setting.transcodeBitrate', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<TextInput
|
||||||
|
aria-label="transcoding format"
|
||||||
|
defaultValue={transcode.format}
|
||||||
|
width={100}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setTranscodingConfig({
|
||||||
|
...transcode,
|
||||||
|
format: e.currentTarget.value || undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.transcodeFormat', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !transcode.enabled,
|
||||||
|
note,
|
||||||
|
title: t('setting.transcodeFormat', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
divider
|
||||||
|
options={transcodeOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -992,6 +992,9 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
},
|
},
|
||||||
repeat: PlayerRepeat.NONE,
|
repeat: PlayerRepeat.NONE,
|
||||||
shuffle: PlayerShuffle.NONE,
|
shuffle: PlayerShuffle.NONE,
|
||||||
|
transcode: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
volume: 50,
|
volume: 50,
|
||||||
})),
|
})),
|
||||||
{ name: 'store_player' },
|
{ name: 'store_player' },
|
||||||
|
|
|
@ -177,6 +177,12 @@ export enum GenreTarget {
|
||||||
TRACK = 'track',
|
TRACK = 'track',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TranscodingConfig = {
|
||||||
|
bitrate?: number;
|
||||||
|
enabled: boolean;
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState {
|
||||||
css: {
|
css: {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -264,6 +270,7 @@ export interface SettingsState {
|
||||||
scrobbleAtPercentage: number;
|
scrobbleAtPercentage: number;
|
||||||
};
|
};
|
||||||
style: PlaybackStyle;
|
style: PlaybackStyle;
|
||||||
|
transcode: TranscodingConfig;
|
||||||
type: PlaybackType;
|
type: PlaybackType;
|
||||||
webAudio: boolean;
|
webAudio: boolean;
|
||||||
};
|
};
|
||||||
|
@ -300,6 +307,7 @@ export interface SettingsSlice extends SettingsState {
|
||||||
setSettings: (data: Partial<SettingsState>) => void;
|
setSettings: (data: Partial<SettingsState>) => void;
|
||||||
setSidebarItems: (items: SidebarItemType[]) => void;
|
setSidebarItems: (items: SidebarItemType[]) => void;
|
||||||
setTable: (type: TableType, data: DataTableProps) => void;
|
setTable: (type: TableType, data: DataTableProps) => void;
|
||||||
|
setTranscodingConfig: (config: TranscodingConfig) => void;
|
||||||
toggleContextMenuItem: (item: ContextMenuItemType) => void;
|
toggleContextMenuItem: (item: ContextMenuItemType) => void;
|
||||||
toggleSidebarCollapseShare: () => void;
|
toggleSidebarCollapseShare: () => void;
|
||||||
};
|
};
|
||||||
|
@ -439,6 +447,9 @@ const initialState: SettingsState = {
|
||||||
scrobbleAtPercentage: 75,
|
scrobbleAtPercentage: 75,
|
||||||
},
|
},
|
||||||
style: PlaybackStyle.GAPLESS,
|
style: PlaybackStyle.GAPLESS,
|
||||||
|
transcode: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
type: PlaybackType.WEB,
|
type: PlaybackType.WEB,
|
||||||
webAudio: true,
|
webAudio: true,
|
||||||
},
|
},
|
||||||
|
@ -662,6 +673,11 @@ export const useSettingsStore = create<SettingsSlice>()(
|
||||||
state.tables[type] = data;
|
state.tables[type] = data;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setTranscodingConfig: (config) => {
|
||||||
|
set((state) => {
|
||||||
|
state.playback.transcode = config;
|
||||||
|
});
|
||||||
|
},
|
||||||
toggleContextMenuItem: (item: ContextMenuItemType) => {
|
toggleContextMenuItem: (item: ContextMenuItemType) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.general.disabledContextMenu[item] =
|
state.general.disabledContextMenu[item] =
|
||||||
|
|
47
src/renderer/utils/set-transcoded-queue-data.ts
Normal file
47
src/renderer/utils/set-transcoded-queue-data.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { getServerById, PlayerData, useSettingsStore } from '/@/renderer/store';
|
||||||
|
import type { QueueSong } from '/@/renderer/api/types';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
|
||||||
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
|
const modifyUrl = (song: QueueSong): string => {
|
||||||
|
const transcode = useSettingsStore.getState().playback.transcode;
|
||||||
|
if (transcode.enabled) {
|
||||||
|
const streamUrl = api.controller.getTranscodingUrl({
|
||||||
|
apiClientProps: {
|
||||||
|
server: getServerById(song.serverId),
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
base: song.streamUrl,
|
||||||
|
...transcode,
|
||||||
|
},
|
||||||
|
})!;
|
||||||
|
|
||||||
|
return streamUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return song.streamUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setQueue = (data: PlayerData, pause?: boolean): void => {
|
||||||
|
const current = data.queue.current ? modifyUrl(data.queue.current) : undefined;
|
||||||
|
const next = data.queue.next ? modifyUrl(data.queue.next) : undefined;
|
||||||
|
mpvPlayer?.setQueue(current, next, pause);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setQueueNext = (data: PlayerData): void => {
|
||||||
|
if (data.queue.next) {
|
||||||
|
mpvPlayer?.setQueueNext(modifyUrl(data.queue.next));
|
||||||
|
} else {
|
||||||
|
mpvPlayer?.setQueueNext(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setAutoNext = (data: PlayerData): void => {
|
||||||
|
if (data.queue.next) {
|
||||||
|
mpvPlayer?.autoNext(modifyUrl(data.queue.next));
|
||||||
|
} else {
|
||||||
|
mpvPlayer?.autoNext(undefined);
|
||||||
|
}
|
||||||
|
};
|
Reference in a new issue