Browse Source

feat: added way to use the MediaSession API

Kristian Vos 3 years ago
parent
commit
e99af52c32

BIN
frontend/dist/assets/15-seconds-of-silence.mp3


+ 3 - 0
frontend/src/main.js

@@ -5,6 +5,7 @@ import VueTippy, { Tippy } from "vue-tippy";
 import { createRouter, createWebHistory } from "vue-router";
 
 import ws from "@/ws";
+import ms from "@/ms";
 import store from "./store";
 
 import AppComponent from "./App.vue";
@@ -227,6 +228,8 @@ lofig.folder = "../config/default.json";
 	const websocketsDomain = await lofig.get("backend.websocketsDomain");
 	ws.init(websocketsDomain);
 
+	ms.init();
+
 	ws.socket.on("ready", res => {
 		const { loggedIn, role, username, userId, email } = res.data;
 

+ 104 - 0
frontend/src/ms.js

@@ -0,0 +1,104 @@
+/* global MediaMetadata */
+
+export default {
+	mediaSessionData: {},
+	listeners: {},
+	audio: null,
+	ytReady: false,
+	playSuccessful: false,
+	loopInterval: null,
+	setYTReady(ytReady) {
+		if (ytReady)
+			setTimeout(() => {
+				this.ytReady = true;
+			}, 1000);
+		else this.ytReady = false;
+	},
+	setListeners(priority, listeners) {
+		this.listeners[priority] = listeners;
+	},
+	removeListeners(priority) {
+		delete this.listeners[priority];
+	},
+	setMediaSessionData(priority, playing, title, artist, album, artwork) {
+		this.mediaSessionData[priority] = {
+			playing, // True = playing, false = paused
+			mediaMetadata: new MediaMetadata({
+				title,
+				artist,
+				album,
+				artwork: [{ src: artwork }]
+			})
+		};
+	},
+	removeMediaSessionData(priority) {
+		delete this.mediaSessionData[priority];
+	},
+	// Gets the highest priority media session data and updates the media session
+	updateMediaSession() {
+		const highestPriority = this.getHighestPriority();
+
+		if (typeof highestPriority === "number") {
+			const mediaSessionDataObject =
+				this.mediaSessionData[highestPriority];
+			navigator.mediaSession.metadata =
+				mediaSessionDataObject.mediaMetadata;
+
+			if (
+				mediaSessionDataObject.playing ||
+				!this.ytReady ||
+				!this.playSuccessful
+			) {
+				navigator.mediaSession.playbackState = "playing";
+				this.audio
+					.play()
+					.then(() => {
+						if (this.audio.currentTime > 1.0) {
+							this.audio.muted = true;
+						}
+						this.playSuccessful = true;
+					})
+					.catch(() => {
+						this.playSuccessful = false;
+					});
+			} else {
+				this.audio.pause();
+				navigator.mediaSession.playbackState = "paused";
+			}
+		} else {
+			this.audio.pause();
+			navigator.mediaSession.playbackState = "none";
+			navigator.mediaSession.metadata = null;
+		}
+	},
+	getHighestPriority() {
+		return Object.keys(this.mediaSessionData)
+			.map(priority => Number(priority))
+			.sort((a, b) => a > b)
+			.reverse()[0];
+	},
+	init() {
+		this.audio = new Audio(
+			"http://localhost/assets/15-seconds-of-silence.mp3"
+		);
+
+		this.audio.loop = true;
+		this.audio.volume = 0.1;
+
+		navigator.mediaSession.setActionHandler("play", () => {
+			this.listeners[this.getHighestPriority()].play();
+		});
+
+		navigator.mediaSession.setActionHandler("pause", () => {
+			this.listeners[this.getHighestPriority()].pause();
+		});
+
+		navigator.mediaSession.setActionHandler("nexttrack", () => {
+			this.listeners[this.getHighestPriority()].nexttrack();
+		});
+
+		this.loopInterval = setInterval(() => {
+			this.updateMediaSession();
+		}, 100);
+	}
+};

+ 37 - 0
frontend/src/pages/Station/index.vue

@@ -820,6 +820,7 @@ import Toast from "toasters";
 import { ContentLoader } from "vue-content-loader";
 
 import aw from "@/aw";
+import ms from "@/ms";
 import ws from "@/ws";
 import keyboardShortcuts from "@/keyboardShortcuts";
 
@@ -1053,6 +1054,21 @@ export default {
 			}
 		);
 
+		ms.setListeners(0, {
+			play: () => {
+				if (this.isOwnerOrAdmin()) this.resumeStation();
+				else this.resumeLocalStation();
+			},
+			pause: () => {
+				if (this.isOwnerOrAdmin()) this.pauseStation();
+				else this.pauseLocalStation();
+			},
+			nexttrack: () => {
+				if (this.isOwnerOrAdmin()) this.skipStation();
+				else this.voteSkipStation();
+			}
+		});
+
 		this.socket.on("event:station.nextSong", res => {
 			const { currentSong, startedAt, paused, timePaused } = res.data;
 
@@ -1249,6 +1265,9 @@ export default {
 	beforeUnmount() {
 		document.body.style.cssText = "";
 
+		ms.removeListeners(0);
+		ms.removeMediaSessionData(0);
+
 		/** Reset Songslist */
 		this.updateSongsList([]);
 
@@ -1289,6 +1308,18 @@ export default {
 		isOwnerOrAdmin() {
 			return this.isOwnerOnly() || this.isAdminOnly();
 		},
+		updateMediaSessionData(currentSong) {
+			if (currentSong) {
+				ms.setMediaSessionData(
+					0,
+					!this.localPaused && !this.stationPaused, // This should be improved later
+					this.currentSong.title,
+					this.currentSong.artists.join(", "),
+					null,
+					this.currentSong.thumbnail
+				);
+			} else ms.removeMediaSessionData(0);
+		},
 		removeFromQueue(youtubeId) {
 			window.socket.dispatch(
 				"stations.removeFromQueue",
@@ -1363,6 +1394,8 @@ export default {
 
 			clearTimeout(window.stationNextSongTimeout);
 
+			this.updateMediaSessionData(currentSong);
+
 			this.startedAt = startedAt;
 			this.updateStationPaused(paused);
 			this.timePaused = timePaused;
@@ -1475,6 +1508,7 @@ export default {
 		},
 		youtubeReady() {
 			if (!this.player) {
+				ms.setYTReady(false);
 				this.player = new window.YT.Player("stationPlayer", {
 					height: 270,
 					width: 480,
@@ -1494,6 +1528,7 @@ export default {
 					events: {
 						onReady: () => {
 							this.playerReady = true;
+							ms.setYTReady(true);
 
 							let volume = parseInt(
 								localStorage.getItem("volume")
@@ -1784,6 +1819,7 @@ export default {
 			this.pauseLocalPlayer();
 		},
 		resumeLocalPlayer() {
+			this.updateMediaSessionData(this.currentSong);
 			if (!this.noSong) {
 				if (this.playerReady) {
 					this.player.seekTo(
@@ -1795,6 +1831,7 @@ export default {
 			}
 		},
 		pauseLocalPlayer() {
+			this.updateMediaSessionData(this.currentSong);
 			if (!this.noSong) {
 				this.timeBeforePause = this.getTimeElapsed();
 				if (this.playerReady) this.player.pauseVideo();