diff --git a/js/webui/src/5-seconds-of-silence.mp3 b/js/webui/src/5-seconds-of-silence.mp3 new file mode 100644 index 00000000..866abc7e Binary files /dev/null and b/js/webui/src/5-seconds-of-silence.mp3 differ diff --git a/js/webui/src/general_settings.js b/js/webui/src/general_settings.js index e6cb465a..3e36bcb3 100644 --- a/js/webui/src/general_settings.js +++ b/js/webui/src/general_settings.js @@ -34,6 +34,7 @@ class GeneralSettings extends React.PureComponent + ); } diff --git a/js/webui/src/index.html b/js/webui/src/index.html index 1a4adb52..e96cf458 100644 --- a/js/webui/src/index.html +++ b/js/webui/src/index.html @@ -11,4 +11,7 @@
+ diff --git a/js/webui/src/index.js b/js/webui/src/index.js index 899651f6..04130c50 100644 --- a/js/webui/src/index.js +++ b/js/webui/src/index.js @@ -15,6 +15,7 @@ import urls, { getPathFromUrl } from './urls' import { playlistTableKey } from './playlist_content'; import { PlaybackState } from 'beefweb-client/src'; import { SettingsView, View } from './navigation_model'; +import MediaSessionController from './mediasession_controller'; const client = new PlayerClient(new RequestHandler()); const settingsStore = new SettingsStore(); @@ -34,6 +35,7 @@ const touchModeController = new TouchModeController(settingsModel); const cssSettingsController = new CssSettingsController(settingsModel); const windowController = new WindowController(playerModel); const router = new Navigo(null, true); +const mediaSessionController = new MediaSessionController(playerModel); router.on({ '/': () => { @@ -119,7 +121,6 @@ playerModel.on('trackSwitch', () => { }); playlistModel.on('playlistsChange', () => { - if (navigationModel.view !== View.playlist) return; @@ -129,12 +130,24 @@ playlistModel.on('playlistsChange', () => { router.navigate(urls.viewCurrentPlaylist); }); +settingsModel.on('enableNotificationChange', () => { + if (settingsModel.enableNotification) { + mediaSessionController.start(); + } + else { + mediaSessionController.stop(); + } +}); + appModel.load(); mediaSizeController.start(); touchModeController.start(); cssSettingsController.start(); appModel.start(); windowController.start(); +if (settingsModel.enableNotification) { + mediaSessionController.start(); +} router.resolve(); const appComponent = ( diff --git a/js/webui/src/mediasession_controller.js b/js/webui/src/mediasession_controller.js new file mode 100644 index 00000000..d619a9f9 --- /dev/null +++ b/js/webui/src/mediasession_controller.js @@ -0,0 +1,98 @@ +import PlayerModel from './player_model'; +import silenceMp3 from './5-seconds-of-silence.mp3'; + +export default class MediaSessionController { + /** + * @param {PlayerModel} playerModel + */ + constructor(playerModel) { + this.playerModel = playerModel; + } + + isSupported() { + return 'mediaSession' in navigator; + } + + /** + * @returns {HTMLAudioElement} + */ + getAudioElement() { + return document.getElementById('silence'); + } + + start() { + if (!this.isSupported()) { + return; + } + + this.isStopped = false; + + navigator.mediaSession.setActionHandler('play', () => + this.playerModel.play() + ); + navigator.mediaSession.setActionHandler('pause', () => + this.playerModel.pause() + ); + navigator.mediaSession.setActionHandler('previoustrack', () => + this.playerModel.previous() + ); + navigator.mediaSession.setActionHandler('nexttrack', () => + this.playerModel.next() + ); + + this.playerModel.on('change', () => this.update()); + + this.update(); + } + + stop() { + if (!this.isSupported()) { + return; + } + + this.isStopped = true; + + navigator.mediaSession.playbackState = 'none'; + this.getAudioElement().pause(); + + navigator.mediaSession.setActionHandler('play', null); + navigator.mediaSession.setActionHandler('pause', null); + navigator.mediaSession.setActionHandler('previoustrack', null); + navigator.mediaSession.setActionHandler('nexttrack', null); + } + + update() { + if (this.isStopped) { + return true; + } + + const playbackStateMap = { + playing: 'playing', + paused: 'paused', + stopped: 'none', + }; + + switch (this.playerModel.playbackState) { + case 'playing': + this.getAudioElement().play(); + break; + case 'paused': + this.getAudioElement().pause(); + break; + default: + break; + } + + navigator.mediaSession.playbackState = + playbackStateMap[this.playerModel.playbackState]; + + const { activeItem } = this.playerModel; + const [, , artist, album, title] = activeItem.columns; + + navigator.mediaSession.metadata = new MediaMetadata({ + artist, + album, + title, + }); + } +} diff --git a/js/webui/src/settings_model.js b/js/webui/src/settings_model.js index f4e88149..127e0109 100644 --- a/js/webui/src/settings_model.js +++ b/js/webui/src/settings_model.js @@ -171,6 +171,14 @@ export default class SettingsModel extends EventEmitter persistent: true, }); + this.define({ + key: 'enableNotification', + title: 'Enable playback control notification', + type: SettingType.bool, + defaultValue: false, + persistent: true, + }); + Object.freeze(this.metadata); } diff --git a/js/webui/webpack.config.js b/js/webui/webpack.config.js index 3da5163c..a2c787d5 100644 --- a/js/webui/webpack.config.js +++ b/js/webui/webpack.config.js @@ -21,7 +21,7 @@ function configCommon(config, params) }); config.module.rules.push({ - test: /(\.svg|\.png)$/, + test: /(\.svg|\.png|\.mp3)$/, loader: 'url-loader', options: { name: '[name].[ext]', @@ -38,7 +38,7 @@ function configApp(config, params) if (params.buildType === 'release') { - const limit = 300 * 1024; + const limit = 350 * 1024; config.performance.hints = 'error'; config.performance.maxEntrypointSize = limit; config.performance.maxAssetSize = limit;