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: {
|
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,
|
||||||
})
|
})
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
@ -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
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