From 28c1876b81b31f2531fb2bfefcd241cfa833f199 Mon Sep 17 00:00:00 2001 From: aidewoode Date: Wed, 3 Feb 2021 21:35:34 +0800 Subject: [PATCH] fix issue #69, add support for Media Session API --- .eslintrc.js | 4 +- .../controllers/media_session_controller.js | 118 ++++++++++++++++++ .../controllers/player_controller.js | 6 +- app/frontend/javascripts/player.js | 4 +- app/views/shared/_player.html.erb | 2 +- app/views/songs/show.json.jbuilder | 6 +- 6 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 app/frontend/javascripts/controllers/media_session_controller.js diff --git a/.eslintrc.js b/.eslintrc.js index 313ea556..0dfe77d8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,6 +28,8 @@ module.exports = { "App": true, "Turbolinks": true, "CustomEvent": true, - "IntersectionObserver": true + "IntersectionObserver": true, + "MediaMetadata": true, + "navigator": true } }; diff --git a/app/frontend/javascripts/controllers/media_session_controller.js b/app/frontend/javascripts/controllers/media_session_controller.js new file mode 100644 index 00000000..8bc594d8 --- /dev/null +++ b/app/frontend/javascripts/controllers/media_session_controller.js @@ -0,0 +1,118 @@ +import { Controller } from 'stimulus'; + +export default class extends Controller { + DEFAULT_SKIP_TIME = 10; + + connect() { + if (!('mediaSession' in navigator)) { return; } + + document.addEventListener('player:playing', this._setPlayingStatus); + + Object.entries(this.mediaSessionActions).forEach(([actionName, actionHandler]) => { + try { + navigator.mediaSession.setActionHandler(actionName, actionHandler); + } catch (error) { + // The media session ation is not supported. + } + }); + } + + disconnect() { + if (!('mediaSession' in navigator)) { return; } + + document.removeEventListener('player:playing', this._setPlayingStatus); + } + + + _setPlayingStatus = () => { + this._updateMetadata(); + this._updatePositionState(); + } + + _updateMetadata = () => { + navigator.mediaSession.metadata = new MediaMetadata({ + title: this.currentSong.name, + artist: this.currentSong.artist_name, + album: this.currentSong.album_name, + artwork: [ + { src: this.currentSong.album_image_url.small, sizes: '200x200' }, + { src: this.currentSong.album_image_url.medium, sizes: '300x300' }, + { src: this.currentSong.album_image_url.large, sizes: '400x400' }, + ] + }); + } + + _updatePositionState = () => { + if (!('setPositionState' in navigator.mediaSession)) { return; } + + navigator.mediaSession.setPositionState({ + duration: this.currentSong.length, + playbackRate: this.currentSong.howl.rate(), + position: this.currentSong.howl.seek() + }); + } + + _play = () => { + this.player.play(this.currentIndex); + } + + _pause = () => { + this.player.pause(); + } + + _next = () => { + this.player.next(); + } + + _previous = () => { + this.player.previous(); + } + + _stop = () => { + this.player.stop(); + } + + _seekBackward = (event) => { + const skipTime = event.seekOffset || this.DEFAULT_SKIP_TIME; + + this.player.seek(this.currentSong.howl.seek() - skipTime); + this._updatePositionState(); + } + + _seekForward = (event) => { + const skipTime = event.seekOffset || this.DEFAULT_SKIP_TIME; + + this.player.seek(this.currentSong.howl.seek() + skipTime); + this._updatePositionState(); + } + + _seekTo = (event) => { + this.player.seek(event.seekTime); + this._updatePositionState(); + } + + get mediaSessionActions() { + return { + play: this._play, + pause: this._pause, + previoustrack: this._previous, + nexttrack: this._next, + stop: this._stop, + seekbackward: this._seekBackward, + seekforward: this._seekForward, + seekto: this._seekTo + }; + } + + get player() { + return App.player; + } + + get currentSong() { + return this.player.currentSong; + } + + get currentIndex() { + return this.player.currentIndex; + } +} diff --git a/app/frontend/javascripts/controllers/player_controller.js b/app/frontend/javascripts/controllers/player_controller.js index 846ac488..80d41ed1 100644 --- a/app/frontend/javascripts/controllers/player_controller.js +++ b/app/frontend/javascripts/controllers/player_controller.js @@ -91,7 +91,7 @@ export default class extends Controller { } seek(event) { - this.player.seek(event.offsetX / event.target.offsetWidth); + this.player.seek((event.offsetX / event.target.offsetWidth) * this.currentSong.length); window.requestAnimationFrame(this._setProgress.bind(this)); } @@ -119,8 +119,8 @@ export default class extends Controller { _setPlayingStatus = () => { const { currentSong } = this; - this.imageTarget.src = currentSong.album_image_url; - this.backgroundImageTarget.style.backgroundImage = `url(${currentSong.album_image_url})`; + this.imageTarget.src = currentSong.album_image_url.small; + this.backgroundImageTarget.style.backgroundImage = `url(${currentSong.album_image_url.small})`; this.songNameTarget.textContent = currentSong.name; this.artistNameTarget.textContent = currentSong.artist_name; this.albumNameTarget.textContent = currentSong.album_name; diff --git a/app/frontend/javascripts/player.js b/app/frontend/javascripts/player.js index c8c8d0b5..bf1384f0 100644 --- a/app/frontend/javascripts/player.js +++ b/app/frontend/javascripts/player.js @@ -73,8 +73,8 @@ class Player { this.play(index); } - seek(percent) { - this.currentSong.howl.seek(this.currentSong.length * percent); + seek(seconds) { + this.currentSong.howl.seek(seconds); } } diff --git a/app/views/shared/_player.html.erb b/app/views/shared/_player.html.erb index 703e7bf4..8368d579 100644 --- a/app/views/shared/_player.html.erb +++ b/app/views/shared/_player.html.erb @@ -1,4 +1,4 @@ -
+
diff --git a/app/views/songs/show.json.jbuilder b/app/views/songs/show.json.jbuilder index 2b6f444e..4f746051 100644 --- a/app/views/songs/show.json.jbuilder +++ b/app/views/songs/show.json.jbuilder @@ -4,6 +4,10 @@ json.call(@song, :id, :name, :length) json.url new_stream_path(song_id: @song.id) json.album_name @song.album.title json.artist_name @song.artist.title -json.album_image_url image_url_for(@song.album, size: 'small') json.is_favorited Current.user.favorited? @song json.format @song.format +json.album_image_url do + json.small image_url_for(@song.album, size: 'small') + json.medium image_url_for(@song.album, size: 'medium') + json.large image_url_for(@song.album, size: 'large') +end