fix(mpris): fix mpris invalid position

- fix #1726
This commit is contained in:
JellyBrick
2024-02-18 10:26:45 +09:00
parent fbc02a494a
commit 8457115105
3 changed files with 114 additions and 40 deletions

View File

@ -56,7 +56,7 @@ declare module '@jellybrick/mpris-service' {
playbackStatus: string; playbackStatus: string;
loopStatus: string; loopStatus: string;
shuffle: boolean; shuffle: boolean;
metadata: object; metadata: Track;
volume: number; volume: number;
canControl: boolean; canControl: boolean;
canPause: boolean; canPause: boolean;

View File

@ -1,37 +1,98 @@
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
import mpris, { Track } from '@jellybrick/mpris-service'; import MprisPlayer, { Track } from '@jellybrick/mpris-service';
import registerCallback from '@/providers/song-info'; import registerCallback, { type SongInfo } from '@/providers/song-info';
import getSongControls from '@/providers/song-controls'; import getSongControls from '@/providers/song-controls';
import config from '@/config'; import config from '@/config';
import { LoggerPrefix } from '@/utils';
class YTPlayer extends MprisPlayer {
/**
* @type {number} The current position in microseconds
* @private
*/
private currentPosition: number;
constructor(opts: {
name: string;
identity: string;
supportedMimeTypes?: string[];
supportedInterfaces?: string[];
}) {
super(opts);
this.currentPosition = 0;
}
setPosition(t: number) {
this.currentPosition = t;
}
getPosition(): number {
return this.currentPosition;
}
setLoopStatus(status: string) {
this.loopStatus = status;
}
isPlaying(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PLAYING;
}
isPaused(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PAUSED;
}
isStopped(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_STOPPED;
}
setPlaybackStatus(status: string) {
this.playbackStatus = status;
}
}
function setupMPRIS() { function setupMPRIS() {
const instance = new mpris({ const instance = new YTPlayer({
name: 'youtube-music', name: 'youtube-music',
identity: 'YouTube Music', identity: 'YouTube Music',
supportedMimeTypes: ['audio/mpeg'], supportedMimeTypes: ['audio/mpeg'],
supportedInterfaces: ['player'], supportedInterfaces: ['player'],
}); });
instance.canRaise = true; instance.canRaise = true;
instance.supportedUriSchemes = ['https']; instance.supportedUriSchemes = ['http', 'https'];
instance.desktopEntry = 'youtube-music'; instance.desktopEntry = 'youtube-music';
return instance; return instance;
} }
function registerMPRIS(win: BrowserWindow) { function registerMPRIS(win: BrowserWindow) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = const {
songControls; playPause,
next,
previous,
volumeMinus10,
volumePlus10,
shuffle,
switchRepeat,
} = songControls;
try { try {
// TODO: "Typing" for this arguments let currentSongInfo: SongInfo | null = null;
const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6); const secToMicro = (n: number) => Math.round(Number(n) * 1e6);
const microToSec = (n: unknown) => Math.round(Number(n) / 1e6); const microToSec = (n: number) => Math.round(Number(n) / 1e6);
const seekTo = (e: { position: unknown }) => const seekTo = (event: {
win.webContents.send('ytmd:seek-to', microToSec(e.position)); trackId: string;
const seekBy = (o: unknown) => position: number;
win.webContents.send('ytmd:seek-by', microToSec(o)); }) => {
if (event.trackId === currentSongInfo?.videoId) {
win.webContents.send('ytmd:seek-to', microToSec(event.position ?? 0));
}
};
const seekBy = (offset: number) =>
win.webContents.send('ytmd:seek-by', microToSec(offset));
const player = setupMPRIS(); const player = setupMPRIS();
@ -44,21 +105,22 @@ function registerMPRIS(win: BrowserWindow) {
ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t))); ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t)));
let currentSeconds = 0; ipcMain.on('ytmd:time-changed', (_, t: number) => {
ipcMain.on('ytmd:time-changed', (_, t: number) => (currentSeconds = t)); player.setPosition(secToMicro(t));
});
ipcMain.on('ytmd:repeat-changed', (_, mode: string) => { ipcMain.on('ytmd:repeat-changed', (_, mode: string) => {
switch (mode) { switch (mode) {
case 'NONE': { case 'NONE': {
player.loopStatus = mpris.LOOP_STATUS_NONE; player.setLoopStatus(YTPlayer.LOOP_STATUS_NONE);
break; break;
} }
case 'ONE': { case 'ONE': {
player.loopStatus = mpris.LOOP_STATUS_TRACK; player.setLoopStatus(YTPlayer.LOOP_STATUS_TRACK);
break; break;
} }
case 'ALL': { case 'ALL': {
player.loopStatus = mpris.LOOP_STATUS_PLAYLIST; player.setLoopStatus(YTPlayer.LOOP_STATUS_PLAYLIST);
// No default // No default
break; break;
} }
@ -67,18 +129,17 @@ function registerMPRIS(win: BrowserWindow) {
player.on('loopStatus', (status: string) => { player.on('loopStatus', (status: string) => {
// SwitchRepeat cycles between states in that order // SwitchRepeat cycles between states in that order
const switches = [ const switches = [
mpris.LOOP_STATUS_NONE, YTPlayer.LOOP_STATUS_NONE,
mpris.LOOP_STATUS_PLAYLIST, YTPlayer.LOOP_STATUS_PLAYLIST,
mpris.LOOP_STATUS_TRACK, YTPlayer.LOOP_STATUS_TRACK,
]; ];
const currentIndex = switches.indexOf(player.loopStatus); const currentIndex = switches.indexOf(player.loopStatus);
const targetIndex = switches.indexOf(status); const targetIndex = switches.indexOf(status);
// Get a delta in the range [0,2] // Get a delta in the range [0,2]
const delta = (targetIndex - currentIndex + 3) % 3; const delta = (targetIndex - currentIndex + 3) % 3;
songControls.switchRepeat(delta); switchRepeat(delta);
}); });
player.getPosition = () => secToMicro(currentSeconds);
player.on('raise', () => { player.on('raise', () => {
win.setSkipTaskbar(false); win.setSkipTaskbar(false);
@ -86,22 +147,23 @@ function registerMPRIS(win: BrowserWindow) {
}); });
player.on('play', () => { player.on('play', () => {
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) { if (!player.isPlaying()) {
player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING; player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PLAYING);
playPause(); playPause();
} }
}); });
player.on('pause', () => { player.on('pause', () => {
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) { if (player.playbackStatus !== YTPlayer.PLAYBACK_STATUS_PAUSED) {
player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED; player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PAUSED);
playPause(); playPause();
} }
}); });
player.on('playpause', () => { player.on('playpause', () => {
player.playbackStatus = player.setPlaybackStatus(
player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING player.isPlaying()
? mpris.PLAYBACK_STATUS_PAUSED ? YTPlayer.PLAYBACK_STATUS_PAUSED
: mpris.PLAYBACK_STATUS_PLAYING; : YTPlayer.PLAYBACK_STATUS_PLAYING
);
playPause(); playPause();
}); });
@ -170,21 +232,32 @@ function registerMPRIS(win: BrowserWindow) {
'xesam:title': songInfo.title, 'xesam:title': songInfo.title,
'xesam:url': songInfo.url, 'xesam:url': songInfo.url,
'xesam:artist': [songInfo.artist], 'xesam:artist': [songInfo.artist],
'mpris:trackid': '/', 'mpris:trackid': songInfo.videoId,
}; };
if (songInfo.album) { if (songInfo.album) {
data['xesam:album'] = songInfo.album; data['xesam:album'] = songInfo.album;
} }
currentSongInfo = songInfo;
player.metadata = data; player.metadata = data;
player.seeked(secToMicro(songInfo.elapsedSeconds));
player.playbackStatus = songInfo.isPaused const currentElapsedMicroSeconds = secToMicro(songInfo.elapsedSeconds ?? 0);
? mpris.PLAYBACK_STATUS_PAUSED player.setPosition(currentElapsedMicroSeconds);
: mpris.PLAYBACK_STATUS_PLAYING; player.seeked(currentElapsedMicroSeconds);
player.setPlaybackStatus(
songInfo.isPaused ?
YTPlayer.PLAYBACK_STATUS_PAUSED :
YTPlayer.PLAYBACK_STATUS_PLAYING
);
} }
}); });
} catch (error) { } catch (error) {
console.warn('Error in MPRIS', error); console.error(
LoggerPrefix,
'Error in MPRIS'
);
console.trace(error);
} }
} }

View File

@ -29,8 +29,9 @@ export const setupTimeChangedListener = singleton(() => {
const progressObserver = new MutationObserver((mutations) => { const progressObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) { for (const mutation of mutations) {
const target = mutation.target as Node & { value: string }; const target = mutation.target as Node & { value: string };
window.ipcRenderer.send('ytmd:time-changed', target.value); const numberValue = Number(target.value);
songInfo.elapsedSeconds = Number(target.value); window.ipcRenderer.send('ytmd:time-changed', numberValue);
songInfo.elapsedSeconds = numberValue;
} }
}); });
const progressBar = document.querySelector('#progress-bar'); const progressBar = document.querySelector('#progress-bar');