add media session integration
This commit is contained in:
		
							parent
							
								
									c0dfb5f853
								
							
						
					
					
						commit
						c7a3e98e91
					
				
							
								
								
									
										55
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| declare module '*.vue' { | ||||
|   import Vue from 'vue' | ||||
|   export default Vue | ||||
| } | ||||
| 
 | ||||
| declare module "md5-es"; | ||||
| 
 | ||||
| interface Navigator { | ||||
|   readonly mediaSession?: MediaSession; | ||||
| } | ||||
| 
 | ||||
| interface Window { | ||||
|   MediaSession?: MediaSession; | ||||
| } | ||||
| 
 | ||||
| type MediaSessionPlaybackState = 'none' | 'paused' | 'playing'; | ||||
| 
 | ||||
| type MediaSessionAction = 'play' | 'pause' | 'seekbackward' | 'seekforward' | 'seekto' | 'previoustrack' | 'nexttrack' | 'skipad' | 'stop'; | ||||
| 
 | ||||
| interface MediaSessionActionDetails { | ||||
|   action: MediaSessionAction; | ||||
|   fastSeek?: boolean; | ||||
|   seekOffset?: number; | ||||
|   seekTime?: number; | ||||
| } | ||||
| 
 | ||||
| interface MediaPositionState { | ||||
|   duration?: number; | ||||
|   playbackRate?: number; | ||||
|   position?: number; | ||||
| } | ||||
| 
 | ||||
| interface MediaSession { | ||||
|   playbackState: MediaSessionPlaybackState; | ||||
|   metadata: MediaMetadata | null; | ||||
|   setActionHandler(action: MediaSessionAction, listener: ((details: MediaSessionActionDetails) => void)): void; | ||||
|   setPositionState?(arg: MediaPositionState): void; | ||||
| } | ||||
| 
 | ||||
| interface MediaImage { | ||||
|   src: string; | ||||
|   sizes?: string; | ||||
|   type?: string; | ||||
| } | ||||
| 
 | ||||
| interface MediaMetadataInit { | ||||
|   title?: string; | ||||
|   artist?: string; | ||||
|   album?: string; | ||||
|   artwork?: MediaImage[]; | ||||
| } | ||||
| 
 | ||||
| declare class MediaMetadata { | ||||
|   constructor(init?: MediaMetadataInit); | ||||
| } | ||||
| @ -97,15 +97,14 @@ export default Vue.extend({ | ||||
|     }), | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapMutations({ | ||||
|     ...mapActions({ | ||||
|       playPause: "player/playPause", | ||||
|     }), | ||||
|     play(index: number) { | ||||
|       if ((this.tracks as any)[index].id === this.playingTrackId) { | ||||
|         this.$store.commit("player/playPause") | ||||
|         return; | ||||
|         return this.$store.dispatch("player/playPause") | ||||
|       } | ||||
|       this.$store.dispatch('player/play', { | ||||
|       return this.$store.dispatch('player/playQueue', { | ||||
|         index, | ||||
|         queue: this.tracks, | ||||
|       }) | ||||
|  | ||||
| @ -10,7 +10,7 @@ import {setupRouter} from '@/shared/router' | ||||
| import {setupStore} from '@/shared/store' | ||||
| import { API } from '@/shared/api'; | ||||
| import { AuthService } from '@/auth/service'; | ||||
| import { connectAudioToStore } from './player/store' | ||||
| import { setupAudio } from './player/store' | ||||
| 
 | ||||
| declare module 'vue/types/vue' { | ||||
|   interface Vue { | ||||
| @ -28,7 +28,7 @@ const authService = new AuthService(); | ||||
| const api = new API(authService); | ||||
| const router = setupRouter(authService); | ||||
| const store = setupStore(authService, api); | ||||
| connectAudioToStore(store); | ||||
| setupAudio(store); | ||||
| 
 | ||||
| Vue.prototype.$auth = authService; | ||||
| Vue.prototype.$api = api; | ||||
|  | ||||
| @ -75,20 +75,16 @@ export default Vue.extend({ | ||||
|     ]), | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapMutations("player", [ | ||||
|       "playPause", | ||||
|     ]), | ||||
|     ...mapActions("player", [ | ||||
|       "playPause", | ||||
|       "playNext", | ||||
|       "playPrevious", | ||||
|     ]), | ||||
|     seek(event: any) { | ||||
|       if (event.target) { | ||||
|         const width = event.target.clientWidth; | ||||
|         const value = event.offsetX / width | ||||
|         this.$store.commit("player/seek", value) | ||||
|       // this.internalValue = e.offsetX / width * 100 | ||||
| 
 | ||||
|         const value = event.offsetX / width; | ||||
|         return this.$store.dispatch("player/seek", value); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -11,7 +11,7 @@ | ||||
| </template> | ||||
| <script lang="ts"> | ||||
| import Vue from "vue"; | ||||
| import { mapState, mapMutations } from 'vuex'; | ||||
| import { mapState, mapMutations, mapActions } from 'vuex'; | ||||
| import TrackList from "@/library/TrackList.vue"; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| @ -25,7 +25,6 @@ export default Vue.extend({ | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapMutations("player", { | ||||
|       play: "playQueueIndex", | ||||
|       remove: "removeFromQueue", | ||||
|     }), | ||||
|   } | ||||
|  | ||||
| @ -1,5 +1,12 @@ | ||||
| import { Store, Module } from 'vuex' | ||||
| 
 | ||||
| const audio = new Audio(); | ||||
| const storedQueue = JSON.parse(localStorage.getItem("queue") || "[]"); | ||||
| const storedQueueIndex = parseInt(localStorage.getItem("queueIndex") || "-1"); | ||||
| if (storedQueueIndex > -1 && storedQueueIndex < storedQueue.length) { | ||||
|   audio.src = storedQueue[storedQueueIndex].url; | ||||
| } | ||||
| const mediaSession: MediaSession | undefined = navigator.mediaSession; | ||||
| 
 | ||||
| interface State { | ||||
|   queue: any[]; | ||||
| @ -9,13 +16,6 @@ interface State { | ||||
|   currentTime: number; // position of current track in seconds
 | ||||
| } | ||||
| 
 | ||||
| const audio = new Audio(); | ||||
| const storedQueue = JSON.parse(localStorage.getItem("queue") || "[]"); | ||||
| const storedQueueIndex = parseInt(localStorage.getItem("queueIndex") || "-1"); | ||||
| if (storedQueueIndex > -1 && storedQueueIndex < storedQueue.length) { | ||||
|   audio.src = storedQueue[storedQueueIndex].url; | ||||
| } | ||||
| 
 | ||||
| export const playerModule: Module<State, any> = { | ||||
|   namespaced: true, | ||||
|   state: { | ||||
| @ -27,40 +27,44 @@ export const playerModule: Module<State, any> = { | ||||
|   }, | ||||
| 
 | ||||
|   mutations: { | ||||
|     playPause(state: State) { | ||||
|       if (state.isPlaying) { | ||||
|     setPlaying(state) { | ||||
|       state.isPlaying = true; | ||||
|       if (mediaSession) { | ||||
|         mediaSession.playbackState = "playing"; | ||||
|       } | ||||
|     }, | ||||
|     setPaused(state) { | ||||
|       state.isPlaying = false; | ||||
|         audio.pause(); | ||||
|       } else { | ||||
|         state.isPlaying = true; | ||||
|         audio.play(); | ||||
|       if (mediaSession) { | ||||
|         mediaSession.playbackState = "paused"; | ||||
|       } | ||||
|     }, | ||||
|     seek(state, value: number) { | ||||
|       console.log("seek: " + value); | ||||
|       if (state.queueIndex != -1) { | ||||
|         const seconds = state.duration * value; | ||||
|         audio.currentTime = seconds; | ||||
|       } | ||||
|     setPosition(state, time: number) { | ||||
|       audio.currentTime = time; | ||||
|     }, | ||||
|     setQueueAndPlay(state, { queue, index }) { | ||||
|     setQueue(state, queue) { | ||||
|       state.queue = queue; | ||||
|       state.queueIndex = index; | ||||
|       state.queueIndex = -1; | ||||
|       localStorage.setItem("queue", JSON.stringify(queue)); | ||||
|       localStorage.setItem("queueIndex", index); | ||||
|       state.isPlaying = true; | ||||
|       audio.src = queue[index].url; | ||||
|       audio.play(); | ||||
|     }, | ||||
|     playQueueIndex(state, index) { | ||||
|     setQueueIndex(state, index) { | ||||
|       if (state.queue.length === 0) { | ||||
|         return; | ||||
|       } | ||||
|       index = Math.max(0, index); | ||||
|       index = index < state.queue.length ? index : 0; | ||||
|       state.isPlaying = true; | ||||
|       state.queueIndex = index; | ||||
|       audio.src = state.queue[index].url; | ||||
|       audio.play(); | ||||
|       localStorage.setItem("queueIndex", index); | ||||
|       const track = state.queue[index]; | ||||
|       audio.src = track.url; | ||||
|       if (mediaSession) { | ||||
|         mediaSession.metadata = new MediaMetadata({ | ||||
|           title: track.title, | ||||
|           artist: track.artist, | ||||
|           album: track.album, | ||||
|           artwork: track.image ? [{ src: track.image}] : undefined, | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|     removeFromQueue(state, index) { | ||||
|       state.queue.splice(index, 1); | ||||
| @ -68,23 +72,47 @@ export const playerModule: Module<State, any> = { | ||||
|         state.queueIndex--; | ||||
|       } | ||||
|     }, | ||||
|     setProgress(state: State, value: any) { | ||||
|     setProgress(state, value: any) { | ||||
|       state.currentTime = value; | ||||
|     }, | ||||
|     setDuration(state: State, value: any) { | ||||
|     setDuration(state, value: any) { | ||||
|       state.duration = value; | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   actions: { | ||||
|     play({ commit }, { queue, index }) { | ||||
|       commit('setQueueAndPlay', { index, queue }); | ||||
|     async playQueue({ commit }, { queue, index }) { | ||||
|       commit("setQueue", queue); | ||||
|       commit("setQueueIndex", index); | ||||
|       commit("setPlaying"); | ||||
|       await audio.play(); | ||||
|     }, | ||||
|     playNext({ commit, state }) { | ||||
|       commit("playQueueIndex", state.queueIndex + 1); | ||||
|     async play({ commit }) { | ||||
|       commit("setPlaying"); | ||||
|       await audio.play(); | ||||
|     }, | ||||
|     playPrevious({ commit, state }) { | ||||
|       commit("playQueueIndex", state.queueIndex - 1); | ||||
|     pause({ commit }) { | ||||
|       audio.pause(); | ||||
|       commit("setPaused"); | ||||
|     }, | ||||
|     async playNext({ commit, state }) { | ||||
|       commit("setQueueIndex", state.queueIndex + 1); | ||||
|       commit("setPlaying"); | ||||
|       await audio.play(); | ||||
|     }, | ||||
|     async playPrevious({ commit, state }) { | ||||
|       commit("setQueueIndex", state.queueIndex - 1); | ||||
|       commit("setPlaying"); | ||||
|       await audio.play(); | ||||
|     }, | ||||
|     playPause({ state, dispatch }) { | ||||
|       if (state.isPlaying) { | ||||
|         return dispatch("pause"); | ||||
|       } | ||||
|       return dispatch("play"); | ||||
|     }, | ||||
|     seek({ commit, state }, value) { | ||||
|       commit("setPosition", state.duration * value); | ||||
|     }, | ||||
|   }, | ||||
|    | ||||
| @ -114,7 +142,7 @@ export const playerModule: Module<State, any> = { | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| export function connectAudioToStore(store: Store<any>) { | ||||
| export function setupAudio(store: Store<any>) { | ||||
|   audio.ontimeupdate = (event) => { | ||||
|     store.commit("player/setProgress", audio.currentTime) | ||||
|   }; | ||||
| @ -125,6 +153,49 @@ export function connectAudioToStore(store: Store<any>) { | ||||
|     store.dispatch("player/playNext"); | ||||
|   } | ||||
|   audio.onerror = (event) => { | ||||
|     store.commit("player/setPaused"); | ||||
|     store.commit("setError", audio.error); | ||||
|   } | ||||
|   audio.onwaiting = (event) => { | ||||
|     console.log('audio is waiting for more data.'); | ||||
|   }; | ||||
| 
 | ||||
|   if (mediaSession) { | ||||
|     mediaSession.setActionHandler('play', () => { | ||||
|       store.dispatch("player/play"); | ||||
|     }); | ||||
|     mediaSession.setActionHandler('pause', () => { | ||||
|       store.dispatch("player/pause"); | ||||
|     }); | ||||
|     mediaSession.setActionHandler('nexttrack', () => { | ||||
|       store.dispatch("player/playNext"); | ||||
|     }); | ||||
|     mediaSession.setActionHandler('previoustrack', () => { | ||||
|       store.dispatch("player/playPrevious"); | ||||
|     }); | ||||
|     mediaSession.setActionHandler('stop', () => { | ||||
|       store.dispatch("player/pause"); | ||||
|     }); | ||||
|     mediaSession.setActionHandler("seekto", (details) => { | ||||
|       store.commit("player/setPosition", details.seekTime); | ||||
|     }); | ||||
|     mediaSession.setActionHandler("seekforward", (details) => { | ||||
|       const offset = details.seekOffset || 10; | ||||
|       store.commit("player/setPosition", Math.min(audio.currentTime + offset, audio.duration)); | ||||
|     }); | ||||
|     mediaSession.setActionHandler("seekbackward", (details) => { | ||||
|       const offset = details.seekOffset || 10; | ||||
|       store.commit("player/setPosition", Math.max(audio.currentTime - offset, 0)); | ||||
|     }); | ||||
|     // FIXME
 | ||||
|     // function updatePositionState() {
 | ||||
|     //   if (mediaSession && mediaSession.setPositionState) {
 | ||||
|     //     mediaSession.setPositionState({
 | ||||
|     //       duration: audio.duration || 0,
 | ||||
|     //       playbackRate: audio.playbackRate,
 | ||||
|     //       position: audio.currentTime,
 | ||||
|     //     });
 | ||||
|     //   }
 | ||||
|     // }
 | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										6
									
								
								src/shims-vue.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/shims-vue.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +0,0 @@ | ||||
| declare module '*.vue' { | ||||
|   import Vue from 'vue' | ||||
|   export default Vue | ||||
| } | ||||
| 
 | ||||
| declare module "md5-es"; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user