add media session integration

This commit is contained in:
Thomas Amland 2020-08-04 18:52:54 +02:00
parent c0dfb5f853
commit c7a3e98e91
7 changed files with 178 additions and 64 deletions

55
src/global.d.ts vendored Normal file
View 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);
}

View File

@ -97,15 +97,14 @@ export default Vue.extend({
}), }),
}, },
methods: { methods: {
...mapMutations({ ...mapActions({
playPause: "player/playPause", playPause: "player/playPause",
}), }),
play(index: number) { play(index: number) {
if ((this.tracks as any)[index].id === this.playingTrackId) { if ((this.tracks as any)[index].id === this.playingTrackId) {
this.$store.commit("player/playPause") return this.$store.dispatch("player/playPause")
return;
} }
this.$store.dispatch('player/play', { return this.$store.dispatch('player/playQueue', {
index, index,
queue: this.tracks, queue: this.tracks,
}) })

View File

@ -10,7 +10,7 @@ import {setupRouter} from '@/shared/router'
import {setupStore} from '@/shared/store' import {setupStore} from '@/shared/store'
import { API } from '@/shared/api'; import { API } from '@/shared/api';
import { AuthService } from '@/auth/service'; import { AuthService } from '@/auth/service';
import { connectAudioToStore } from './player/store' import { setupAudio } from './player/store'
declare module 'vue/types/vue' { declare module 'vue/types/vue' {
interface Vue { interface Vue {
@ -28,7 +28,7 @@ const authService = new AuthService();
const api = new API(authService); const api = new API(authService);
const router = setupRouter(authService); const router = setupRouter(authService);
const store = setupStore(authService, api); const store = setupStore(authService, api);
connectAudioToStore(store); setupAudio(store);
Vue.prototype.$auth = authService; Vue.prototype.$auth = authService;
Vue.prototype.$api = api; Vue.prototype.$api = api;

View File

@ -75,20 +75,16 @@ export default Vue.extend({
]), ]),
}, },
methods: { methods: {
...mapMutations("player", [
"playPause",
]),
...mapActions("player", [ ...mapActions("player", [
"playPause",
"playNext", "playNext",
"playPrevious", "playPrevious",
]), ]),
seek(event: any) { seek(event: any) {
if (event.target) { if (event.target) {
const width = event.target.clientWidth; const width = event.target.clientWidth;
const value = event.offsetX / width const value = event.offsetX / width;
this.$store.commit("player/seek", value) return this.$store.dispatch("player/seek", value);
// this.internalValue = e.offsetX / width * 100
} }
} }
} }

View File

@ -11,7 +11,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from "vue"; import Vue from "vue";
import { mapState, mapMutations } from 'vuex'; import { mapState, mapMutations, mapActions } from 'vuex';
import TrackList from "@/library/TrackList.vue"; import TrackList from "@/library/TrackList.vue";
export default Vue.extend({ export default Vue.extend({
@ -25,7 +25,6 @@ export default Vue.extend({
}, },
methods: { methods: {
...mapMutations("player", { ...mapMutations("player", {
play: "playQueueIndex",
remove: "removeFromQueue", remove: "removeFromQueue",
}), }),
} }

View File

@ -1,5 +1,12 @@
import { Store, Module } from 'vuex' 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 { interface State {
queue: any[]; queue: any[];
@ -9,13 +16,6 @@ interface State {
currentTime: number; // position of current track in seconds 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> = { export const playerModule: Module<State, any> = {
namespaced: true, namespaced: true,
state: { state: {
@ -27,40 +27,44 @@ export const playerModule: Module<State, any> = {
}, },
mutations: { mutations: {
playPause(state: State) { setPlaying(state) {
if (state.isPlaying) { state.isPlaying = true;
if (mediaSession) {
mediaSession.playbackState = "playing";
}
},
setPaused(state) {
state.isPlaying = false; state.isPlaying = false;
audio.pause(); if (mediaSession) {
} else { mediaSession.playbackState = "paused";
state.isPlaying = true;
audio.play();
} }
}, },
seek(state, value: number) { setPosition(state, time: number) {
console.log("seek: " + value); audio.currentTime = time;
if (state.queueIndex != -1) {
const seconds = state.duration * value;
audio.currentTime = seconds;
}
}, },
setQueueAndPlay(state, { queue, index }) { setQueue(state, queue) {
state.queue = queue; state.queue = queue;
state.queueIndex = index; state.queueIndex = -1;
localStorage.setItem("queue", JSON.stringify(queue)); 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) { if (state.queue.length === 0) {
return; return;
} }
index = Math.max(0, index);
index = index < state.queue.length ? index : 0; index = index < state.queue.length ? index : 0;
state.isPlaying = true;
state.queueIndex = index; state.queueIndex = index;
audio.src = state.queue[index].url; localStorage.setItem("queueIndex", index);
audio.play(); 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) { removeFromQueue(state, index) {
state.queue.splice(index, 1); state.queue.splice(index, 1);
@ -68,23 +72,47 @@ export const playerModule: Module<State, any> = {
state.queueIndex--; state.queueIndex--;
} }
}, },
setProgress(state: State, value: any) { setProgress(state, value: any) {
state.currentTime = value; state.currentTime = value;
}, },
setDuration(state: State, value: any) { setDuration(state, value: any) {
state.duration = value; state.duration = value;
}, },
}, },
actions: { actions: {
play({ commit }, { queue, index }) { async playQueue({ commit }, { queue, index }) {
commit('setQueueAndPlay', { index, queue }); commit("setQueue", queue);
commit("setQueueIndex", index);
commit("setPlaying");
await audio.play();
}, },
playNext({ commit, state }) { async play({ commit }) {
commit("playQueueIndex", state.queueIndex + 1); commit("setPlaying");
await audio.play();
}, },
playPrevious({ commit, state }) { pause({ commit }) {
commit("playQueueIndex", state.queueIndex - 1); 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) => { audio.ontimeupdate = (event) => {
store.commit("player/setProgress", audio.currentTime) store.commit("player/setProgress", audio.currentTime)
}; };
@ -125,6 +153,49 @@ export function connectAudioToStore(store: Store<any>) {
store.dispatch("player/playNext"); store.dispatch("player/playNext");
} }
audio.onerror = (event) => { audio.onerror = (event) => {
store.commit("player/setPaused");
store.commit("setError", audio.error); 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
View File

@ -1,6 +0,0 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
declare module "md5-es";