From 3ee13cb41d00f9f7571f6b542b3c44c585c88a67 Mon Sep 17 00:00:00 2001 From: Joe Ipson Date: Sun, 23 Aug 2020 01:37:07 -0600 Subject: [PATCH] Fixed mpris duration and position on linux. Added duration and position for MediaSessionService --- package-lock.json | 2 +- package.json | 2 +- src/background.js | 29 +++++++++++------ src/services/player.js | 74 ++++++++++++++++++++++++++++-------------- 4 files changed, 70 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c65351..7ce586c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jellyamp", - "version": "0.9.7", + "version": "0.9.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 174cee6..3dd7e5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jellyamp", - "version": "0.9.7", + "version": "0.9.8", "private": true, "description": "Desktop client for listening to music from a Jellyfin server", "license": "MIT", diff --git a/src/background.js b/src/background.js index 7e1a0ab..9959788 100644 --- a/src/background.js +++ b/src/background.js @@ -1,4 +1,4 @@ -import { app, protocol, BrowserWindow, ipcMain, globalShortcut } from 'electron'; +import { app, protocol, BrowserWindow, ipcMain } from 'electron'; import { createProtocol, installVueDevtools, @@ -8,6 +8,7 @@ import path from 'path'; let player; let Player; let playerHandler; +let currentPlaytime = 0; const isDevelopment = process.env.NODE_ENV !== 'production'; const isLinux = process.platform === 'linux'; @@ -109,14 +110,18 @@ const setupPlayer = ev => { playerHandler = ev; - player.on('next', () => playerHandler.reply('skip')); - player.on('previous', () => playerHandler.reply('prev')); - player.on('pause', () => playerHandler.reply('playPause')); - player.on('playpause', () => playerHandler.reply('playPause')); - player.on('play', () => playerHandler.reply('playPause')); - player.on('stop', () => playerHandler.reply('stop')); + player.on('next', () => { currentPlaytime = 0; playerHandler.reply('skip'); }); + player.on('previous', () => { currentPlaytime = 0; playerHandler.reply('prev'); }); + player.on('pause', () => { currentPlaytime = 0; playerHandler.reply('playPause'); }); + player.on('playpause', () => { currentPlaytime = 0; playerHandler.reply('playPause'); }); + player.on('play', () => { currentPlaytime = 0; playerHandler.reply('playPause'); }); + player.on('stop', () => { currentPlaytime = 0; playerHandler.reply('stop'); }); player.on('quit', () => app.quit()); player.on('raise', () => win.restore()); + + player.getPosition = () => { + return currentPlaytime; + } } }; @@ -128,7 +133,7 @@ if (isLinux) { player.metadata = { 'mpris:trackid': player.objectPath('track/0'), - 'mpris:length': 0, + 'mpris:length': data.duration, 'mpris:artUrl': data.img, 'xesam:title': data.name, 'xesam:album': data.album, @@ -138,7 +143,7 @@ if (isLinux) { player.playbackStatus = Player.PLAYBACK_STATUS_PLAYING; }); - ipcMain.on('pause', () => { + ipcMain.on('pause', ev => { if (!player) { setupPlayer(ev); } @@ -146,13 +151,17 @@ if (isLinux) { player.playbackStatus = Player.PLAYBACK_STATUS_PAUSED; }); - ipcMain.on('stop', () => { + ipcMain.on('stop', ev => { if (!player) { setupPlayer(ev); } player.playbackStatus = Player.PLAYBACK_STATUS_STOPPED; }); + + ipcMain.on('updateTime', (_ev, data) => { + currentPlaytime = data; + }); } // Exit cleanly on request from parent process in development mode. diff --git a/src/services/player.js b/src/services/player.js index 4eb7ccd..54db7cc 100644 --- a/src/services/player.js +++ b/src/services/player.js @@ -6,6 +6,10 @@ import JellyfinService from './jellyfin'; import placeholderImg from '../assets/logo.png'; +const ticksInSecond = 10000000; +const microSecondsinSecond = 1000000; +const ticksInMs = 10; + Vue.filter('duration', value => { if (!value) { value = 0; @@ -30,7 +34,7 @@ class Player { updateProgress = _.throttle(ticks => { const data = { IsPaused: false, - PositionTicks: ticks, // Convert to ticks/ns + PositionTicks: ticks, PlayMethod: 'Transcode', PlaySessionId: this.queue[this.index].params.PlaySessionId, ItemId: this.queue[this.index].Id, @@ -40,11 +44,22 @@ class Player { JellyfinService.updateProgress(data); }, 10000); - updateProgressMpris = _.throttle(ticks => { + updateProgressMpris = _.throttle(seconds => { if (window.ipcRenderer) { - window.ipcRenderer.send('updateTime', Math.floor(ticks / 1000)); // nanoseconds to microseconds + window.ipcRenderer.send('updateTime', seconds * microSecondsinSecond); // seconds to microseconds + } + + if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) { + // Sometimes there is a race condition on skip where the position is greater than the duration + try { + navigator.mediaSession.setPositionState({ + duration: this.player.duration(), + playbackRate: 1, + position: seconds, + }); + } catch {} } - }, 1000); + }, 1000, {leading: false, trailing: true}); // Make it a singleton constructor() { @@ -169,23 +184,27 @@ class Player { artist: item.Artists, album: item.Album, img: item.thumbnailImage, - duration: Math.floor(item.RunTimeTicks / 1000), // nanoseconds to microseconds + duration: Math.floor(item.RunTimeTicks / ticksInMs), // nanoseconds to microseconds }; window.ipcRenderer.send('play', data); } if ('mediaSession' in navigator) { - setTimeout(() => { - navigator.mediaSession.playbackState = 'playing'; - }); - navigator.mediaSession.metadata = new MediaMetadata({ title: item.Name, artist: item.Artists, album: item.Album, artwork: [{ src: item.thumbnailImage }], }); + + navigator.mediaSession.playbackState = 'playing'; + + if ('setPositionState' in navigator.mediaSession) { + navigator.mediaSession.setPositionState({ + duration: Math.floor(item.RunTimeTicks / ticksInSecond), + }); + } } }, onend: () => { @@ -219,6 +238,10 @@ class Player { window.ipcRenderer.send('stop'); } + if ('mediaSession' in navigator) { + navigator.mediaSession.playbackState = 'none'; + } + JellyfinService.stopPlaying({ IsPaused: false, PlayMethod: 'Transcode', @@ -321,11 +344,11 @@ class Player { // requestAnimationFrame(() => this.step()); // This binds up the CPU setTimeout(() => this.step(), 250); - const ticks = Math.round(seek * 10000000); + const ticks = Math.round(seek * ticksInSecond); this.queue[this.index].progressInTicks = ticks; this.updateProgress(ticks); - this.updateProgressMpris(ticks); + this.updateProgressMpris(seek); } } @@ -398,21 +421,22 @@ if (window.ipcRenderer) { if ('mediaSession' in navigator) { navigator.mediaSession.playbackState = 'none'; - navigator.mediaSession.setActionHandler('play', () => { - PlayerService.playPause(); - }); - - navigator.mediaSession.setActionHandler('pause', () => { - PlayerService.playPause(); - }); - - navigator.mediaSession.setActionHandler('previoustrack', () => { - PlayerService.handleBack(); - }); + const actionHandlers = [ + ['play', () => PlayerService.playPause()], + ['pause', () => PlayerService.playPause()], + ['previoustrack', () => PlayerService.handleBack()], + ['nexttrack', () => PlayerService.skip('next')], + ['stop', () => PlayerService.stop()], + // ['seekto', (details) => { /* ... */ }], + ]; - navigator.mediaSession.setActionHandler('nexttrack', () => { - PlayerService.skip('next'); - }); + for (const [action, handler] of actionHandlers) { + try { + navigator.mediaSession.setActionHandler(action, handler); + } catch (error) { + console.log(`The media session action "${action}" is not supported yet.`); + } + } }