Skip to content

Commit

Permalink
Update mpris implementation
Browse files Browse the repository at this point in the history
Add a basic mpris implementation by updating to mpris-service 2.0 and
uncommenting old mpris code. Add additional methods to respond to mpris
queries and methods to get and set the shuffle status, loop status, and
position of the player.

fixes #98
  • Loading branch information
Tony Crisci committed Mar 16, 2019
1 parent 45edc75 commit dad078f
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 49 deletions.
5 changes: 4 additions & 1 deletion app/actions/player.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Sound from 'react-sound';
import { sendPaused, sendPlay } from '../mpris';
import { sendPaused, sendPlay, sendVolume, sendPlaybackProgress, sendSeek } from '../mpris';

export const START_PLAYBACK = 'START_PLAYBACK';
export const PAUSE_PLAYBACK = 'PAUSE_PLAYBACK';
Expand Down Expand Up @@ -37,6 +37,7 @@ export function togglePlayback(currentState) {
}

export function updatePlaybackProgress(progress, seek) {
sendPlaybackProgress(progress, seek);
return {
type: UPDATE_PLAYBACK_PROGRESS,
payload: {
Expand All @@ -47,13 +48,15 @@ export function updatePlaybackProgress(progress, seek) {
}

export function updateSeek(seek) {
sendSeek(seek);
return {
type: UPDATE_SEEK,
payload: seek
};
}

export function updateVolume(volume) {
sendVolume(volume);
return {
type: UPDATE_VOLUME,
payload: volume
Expand Down
16 changes: 16 additions & 0 deletions app/mpris.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,19 @@ export function sendPlayingStatus(event, playerState, queueState) {
export function sendQueueItems(event, queueItems) {
ipcRenderer.send('queue', queueItems);
}

export function sendPlaybackProgress(progress) {
ipcRenderer.send('playbackProgress', progress);
}

export function sendVolume(volume) {
ipcRenderer.send('volume', volume);
}

export function sendSeek(position) {
ipcRenderer.send('seek', position);
}

export function sendSetOption(key, value) {
ipcRenderer.send('set-option', {key, value});
}
4 changes: 3 additions & 1 deletion app/persistence/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _ from 'lodash';
import electronStore from 'electron-store';

import options from '../constants/settings';
import { restartApi, stopApi } from '../mpris';
import { restartApi, stopApi, sendSetOption } from '../mpris';

const store = new electronStore();

Expand Down Expand Up @@ -50,6 +50,8 @@ function setOption (key, value) {
} else if (key === 'api.enabled' && !value) {
stopApi();
}

sendSetOption(key, value);
}

export { store, getOption, setOption };
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@
"uuid": "^3.2.1",
"webpack-cli": "^3.0.8",
"youtube-playlist": "^1.0.2",
"ytdl-core": "^0.24.0"
"ytdl-core": "^0.24.0",
"mpris-service": "^2.0.0"
},
"devDependencies": {
"babel-core": "^6.26.0",
Expand Down
194 changes: 148 additions & 46 deletions server/main.dev.linux.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ const path = require('path');
const url = require('url');
const getOption = require('./store').getOption;
const { runHttpServer, closeHttpServer } = require('./http/server');
const mpris = require('./mpris');
var Player;

// GNU/Linux-specific
if (!platform.isDarwin && !platform.isWin32) {
// Player = require('mpris-service');
Player = require('mpris-service');
}

let win;
Expand Down Expand Up @@ -112,59 +114,159 @@ function createWindow() {

// GNU/Linux-specific
if (!platform.isDarwin && !platform.isWin32) {
// player = Player({
// name: 'nuclear',
// identity: 'nuclear music player',
// supportedUriSchemes: ['file'],
// supportedMimeTypes: ['audio/mpeg', 'application/ogg'],
// supportedInterfaces: ['player'],
// desktopEntry: 'nuclear'
// });

// player.on('quit', function () {
// win = null;
// });

// player.on('next', mpris.onNext);
// player.on('previous', mpris.onPrevious);
// player.on('pause', mpris.onPause);
// player.on('playpause', mpris.onPlayPause);
// player.on('stop', mpris.onStop);
// player.on('play', mpris.onPlay);
let hashCode = function(str) {
str = str.toString();
let hash = 0;
if (str.length == 0) {
return hash;
}
for (var i = 0; i < str.length; i++) {
var char = str.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}

let secToUs = function(sec) {
return Math.floor(Number(sec) * 1e6);
}

let positionSec = 0.0;

let player = Player({
name: 'nuclear',
identity: 'nuclear music player',
supportedUriSchemes: ['file'],
supportedMimeTypes: ['audio/mpeg', 'application/ogg'],
supportedInterfaces: ['player'],
desktopEntry: 'nuclear'
});

if (getOption('loopAfterQueueEnd')) {
player.loopStatus = 'Track';
} else {
player.loopStatus = 'None';
}

player.shuffle = getOption('shuffleQueue');

player.volume = 1.0;

player.getPosition = function() {
return secToUs(positionSec);
};

player.on('quit', function () {
win = null;
});

player.on('next', mpris.onNext);
player.on('previous', mpris.onPrevious);
player.on('pause', mpris.onPause);
player.on('playpause', mpris.onPlayPause);
player.on('stop', mpris.onStop);
player.on('play', mpris.onPlay);
player.on('volume', function(volume) {
mpris.onVolume(volume * 100);
});
player.on('position', function(e) {
let {trackId, position} = e;
if (player.metadata && player.metadata['mpris:trackid'] === trackId) {
mpris.onSeek(position / 1e3);
}
});
player.on('seek', function(seek) {
let seekTo = (positionSec * 1e3) + (seek / 1e3);
mpris.onSeek(seekTo);
});
player.on('shuffle', function(shuffle) {
mpris.onSettings({shuffleQueue: shuffle});
});
player.on('loopStatus', function(status) {
if (status === 'None') {
mpris.onSettings({ loopAfterQueueEnd: false});
} else if (status === 'Track') {
mpris.onSettings({loopAfterQueueEnd: true});
} else {
// XXX 'Playlist' loop status is not supported, just do the closest
// thing.
mpris.onSettings({loopAfterQueueEnd: true});
}
});

let lastId = null;
ipcMain.on('songChange', (event, arg) => {
if (arg === null) {
return;
}

changeWindowTitle(arg.artist, arg.name);

// player.metadata = {
// 'mpris:trackid': player.objectPath('track/0'),
// 'mpris:artUrl': arg.thumbnail,
// 'xesam:title': arg.name,
// 'xesam:artist': arg.artist
// };

// if (arg.streams && arg.streams.length > 0) {
// player.metadata['mpris:length'] = arg.streams[0].duration * 1000 * 1000; // In microseconds
// }
});

// ipcMain.on('play', (event, arg) => {
// player.playbackStatus = 'Playing';
// });

// ipcMain.on('paused', (event, arg) => {
// player.playbackStatus = 'Paused';
// });
// } else {
// ipcMain.on('songChange', (event, arg) => {
// if (arg === null) {
// return;
// }
// changeWindowTitle(arg.artist, arg.name);
// });
if (arg.streams && arg.streams.length > 0) {
let id = arg.streams[0].id;
if (id !== lastId) {
lastId = id;
let metadata = {
'mpris:trackid': player.objectPath(`track/${Math.abs(hashCode(id))}`),
'mpris:artUrl': arg.thumbnail || '',
'xesam:title': arg.name || '',
'xesam:artist': arg.artist || ''
};
if (arg.streams[0].source === 'Youtube') {
metadata['mpris:length'] = secToUs(Number(arg.streams[0].duration));
} else {
// XXX: Soundcloud is in ms, and I think this is reasonble, but I
// don't know what other duration formats to expect here.
metadata['mpris:length'] = Math.floor(Number(arg.streams[0].duration) * 1e3);
}
player.positionSec = 0;
player.metadata = metadata;
}
}
});

ipcMain.on('play', (event, arg) => {
player.playbackStatus = 'Playing';
});

ipcMain.on('paused', (event, arg) => {
player.playbackStatus = 'Paused';
});

ipcMain.on('volume', (event, volume) => {
player.volume = volume / 100;
});

ipcMain.on('playbackProgress', (event, progress) => {
positionSec = progress;
});

ipcMain.on('seek', (event, seek) => {
// this is in miliseconds
player.positionSec = seek / 1e3;
player.seeked(Math.floor(seek * 1e3));
});

ipcMain.on('set-option', (event, kv) => {
let {key, value} = kv;
if (key === 'loopAfterQueueEnd') {
if (value) {
player.loopStatus = 'Track';
} else {
player.loopStatus = 'None';
}
} else if (key === 'shuffleQueue') {
player.shuffle = value;
}
});
} else {
ipcMain.on('songChange', (event, arg) => {
if (arg === null) {
return;
}
changeWindowTitle(arg.artist, arg.name);
});
}
}

Expand Down
6 changes: 6 additions & 0 deletions webpack.config.electron.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/* eslint-env node */
const webpack = require('webpack');
const HappyPack = require('happypack');
const path = require('path');

module.exports = env => {
const entry = env && env.LINUX ? './server/main.dev.linux.js' : './server/main.dev.js';

return {
entry: entry,
resolve: {
alias: {
jsbi: path.resolve(__dirname, 'node_modules', 'jsbi', 'dist', 'jsbi-cjs.js')
}
},
output: {
path: __dirname,
filename: 'bundle.electron.js'
Expand Down

0 comments on commit dad078f

Please sign in to comment.