All checks were successful
Build and Deploy to Web Server / deploy (push) Successful in 18m1s
368 lines
11 KiB
Plaintext
368 lines
11 KiB
Plaintext
---
|
|
export interface Track {
|
|
src: string;
|
|
metadata: Record<string, any>;
|
|
duration: number;
|
|
}
|
|
|
|
import { Icon } from "astro-icon/components";
|
|
|
|
interface Props {
|
|
height?: string;
|
|
}
|
|
|
|
const { height = "h-28" } = Astro.props;
|
|
|
|
import { getAudioDurationInSeconds } from "get-audio-duration";
|
|
import { join } from "path";
|
|
|
|
import { getCollection } from "astro:content";
|
|
const autoQueuedTracks = (
|
|
await getCollection("tracks", ({ data }) => data.autoQueue)
|
|
).sort(
|
|
(a, b) => (a.data.autoQueue?.order || -1) - (b.data.autoQueue?.order || 1)
|
|
);
|
|
|
|
const initialQueue = await Promise.all(
|
|
autoQueuedTracks.map(async ({ data }) => {
|
|
const fullFilePath = join(process.cwd(), "public", data.src);
|
|
const duration = await getAudioDurationInSeconds(fullFilePath);
|
|
return {
|
|
src: data.src,
|
|
metadata: data.metadata,
|
|
duration: duration
|
|
};
|
|
})
|
|
);
|
|
---
|
|
|
|
<div
|
|
id="player"
|
|
class:list={["fixed right-0 bottom-0 left-0 z-50 bg-black shadow-lg", height]}
|
|
transition:persist=""
|
|
transition:name="player"
|
|
transition:animate="none"
|
|
>
|
|
<div
|
|
id="player:time:progress"
|
|
class="relative top-0 right-0 left-0 m-0 h-2 w-full bg-gray-900 p-0"
|
|
>
|
|
<div
|
|
id="player:time:progress:buffered"
|
|
class="absolute top-0 left-0 m-0 h-full bg-gray-700 p-0"
|
|
>
|
|
</div>
|
|
<div
|
|
id="player:time:progress:played"
|
|
class="bg-primary absolute top-0 left-0 m-0 h-full p-0"
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="p-2 text-white">
|
|
<div class="grid grid-cols-2 text-xs">
|
|
<span id="player:times:current" class="col-span-1 text-left">0:00</span>
|
|
<span id="player:times:duration" class="col-span-1 text-right">0:00</span>
|
|
</div>
|
|
<div class="flex h-full flex-row items-center justify-center align-middle">
|
|
<div class="m-0 flex grow-0 flex-row space-x-2 p-0 text-3xl">
|
|
<span>
|
|
<Icon name="fa7-solid:backward" id="player:controls:previous" />
|
|
</span>
|
|
<span>
|
|
<Icon name="fa7-solid:play" id="player:controls:play" />
|
|
<Icon
|
|
name="fa7-solid:pause"
|
|
id="player:controls:pause"
|
|
class="hidden"
|
|
/>
|
|
</span>
|
|
<span>
|
|
<Icon name="fa7-solid:forward" id="player:controls:next" />
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-grow flex-col text-sm">
|
|
<span id="player:metadata:title" class="w-full text-right font-semibold"
|
|
></span>
|
|
<span id="player:metadata:subtitle" class="w-full text-right"></span>
|
|
<span id="player:metadata:extra" class="w-full text-right"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class:list={[height]}></div>
|
|
|
|
<script is:inline src="/js/howler.min.js"></script>
|
|
<script is:inline define:vars={{ initialQueue }}>
|
|
/**
|
|
* Player class containing the state of our playlist and where we are in it.
|
|
* Includes all methods for playing, skipping, updating the display, etc.
|
|
* @param {Array} playlist Array of objects with playlist song details ({title, file, howl}).
|
|
*/
|
|
class Player {
|
|
constructor(playlist) {
|
|
this.playlist = window.player?.playlist ?? playlist;
|
|
this.index = window.player?.index ?? 0;
|
|
|
|
this.UI = {
|
|
controls: {
|
|
play: document.getElementById("player:controls:play"),
|
|
pause: document.getElementById("player:controls:pause"),
|
|
previous: document.getElementById("player:controls:previous"),
|
|
next: document.getElementById("player:controls:next")
|
|
},
|
|
times: {
|
|
current: document.getElementById("player:times:current"),
|
|
duration: document.getElementById("player:times:duration")
|
|
},
|
|
metadata: {
|
|
title: document.getElementById("player:metadata:title"),
|
|
subtitle: document.getElementById("player:metadata:subtitle"),
|
|
extra: document.getElementById("player:metadata:extra")
|
|
},
|
|
progress: {
|
|
bar: document.getElementById("player:time:progress"),
|
|
buffered: document.getElementById("player:time:progress:buffered"),
|
|
played: document.getElementById("player:time:progress:played")
|
|
}
|
|
};
|
|
|
|
this.UI.metadata.title.textContent = playlist[this.index].metadata.title;
|
|
this.UI.metadata.subtitle.textContent =
|
|
playlist[this.index].metadata.subtitle;
|
|
this.UI.metadata.extra.textContent = playlist[this.index].metadata.extra;
|
|
this.UI.times.duration.textContent = this.formatTime(
|
|
Math.round(playlist[this.index].duration)
|
|
);
|
|
|
|
this.UI.controls.play.addEventListener("click", function () {
|
|
window.player.play();
|
|
});
|
|
this.UI.controls.pause.addEventListener("click", function () {
|
|
window.player.pause();
|
|
});
|
|
this.UI.controls.previous.addEventListener("click", function () {
|
|
window.player.skip("prev");
|
|
});
|
|
this.UI.controls.next.addEventListener("click", function () {
|
|
window.player.skip("next");
|
|
});
|
|
|
|
this.UI.progress.bar.addEventListener("click", function (event) {
|
|
window.player.seek(event.clientX / window.innerWidth);
|
|
});
|
|
|
|
window.playerIsInitialised = true;
|
|
}
|
|
|
|
/**
|
|
* Play a song in the playlist.
|
|
* @param {Number} index Index of the song in the playlist (leave empty to play the first or current).
|
|
*/
|
|
play(index) {
|
|
var sound;
|
|
|
|
index = typeof index === "number" ? index : this.index;
|
|
var data = this.playlist[index];
|
|
|
|
// If we already loaded this track, use the current one.
|
|
if (data.howl) {
|
|
sound = data.howl;
|
|
} else {
|
|
sound = data.howl = new Howl({
|
|
src: [data.src],
|
|
autoplay: false,
|
|
html5: true, // Force to HTML5 so that the audio can stream in (best for large files).
|
|
onplay: () => {
|
|
// Display the duration.
|
|
this.UI.times.duration.textContent = this.formatTime(
|
|
Math.round(sound.duration())
|
|
);
|
|
|
|
// Start updating the progress of the track.
|
|
requestAnimationFrame(this.step.bind(this));
|
|
|
|
this.UI.controls.pause.classList.remove("hidden");
|
|
this.UI.controls.play.classList.add("hidden");
|
|
},
|
|
// onload: () => {
|
|
// loading.style.display = "none";
|
|
// },
|
|
onend: () => {
|
|
this.skip("next");
|
|
},
|
|
onpause: () => {
|
|
this.UI.controls.pause.classList.add("hidden");
|
|
this.UI.controls.play.classList.remove("hidden");
|
|
},
|
|
// onstop: () => {
|
|
// },
|
|
onseek: () => {
|
|
// Start updating the progress of the track.
|
|
requestAnimationFrame(this.step.bind(this));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Begin playing the sound.
|
|
sound.play();
|
|
|
|
// Update the track display.
|
|
this.UI.metadata.title.textContent = data.metadata.title;
|
|
this.UI.metadata.subtitle.textContent = data.metadata.subtitle;
|
|
this.UI.metadata.extra.textContent = data.metadata.extra;
|
|
this.UI.times.duration.textContent = this.formatTime(
|
|
Math.round(data.duration)
|
|
);
|
|
this.UI.progress.buffered.style.width = "0%";
|
|
|
|
// Keep track of the index we are currently playing.
|
|
this.index = index;
|
|
}
|
|
|
|
/**
|
|
* Pause the currently playing track.
|
|
*/
|
|
pause() {
|
|
// Get the Howl we want to manipulate.
|
|
var sound = this.playlist[this.index].howl;
|
|
|
|
sound.pause();
|
|
|
|
this.UI.controls.pause.classList.add("hidden");
|
|
this.UI.controls.play.classList.remove("hidden");
|
|
}
|
|
|
|
/**
|
|
* Skip to the next or previous track.
|
|
* @param {String} direction 'next' or 'prev'.
|
|
*/
|
|
skip(direction) {
|
|
var current = this.playlist[this.index];
|
|
var sound = current.howl;
|
|
var currentlyPlaying = sound && sound.playing();
|
|
|
|
if (direction === "prev" && currentlyPlaying && sound.seek() > 3) {
|
|
sound.seek(0);
|
|
return;
|
|
}
|
|
|
|
let index;
|
|
if (direction === "prev") {
|
|
index = (this.index - 1 + this.playlist.length) % this.playlist.length;
|
|
} else {
|
|
index = (this.index + 1) % this.playlist.length;
|
|
}
|
|
|
|
this.skipTo(index);
|
|
}
|
|
|
|
/**
|
|
* Skip to a specific track based on its playlist index.
|
|
* @param {Number} index Index in the playlist.
|
|
*/
|
|
skipTo(index) {
|
|
// Stop the current track.
|
|
if (this.playlist[this.index].howl) {
|
|
this.playlist[this.index].howl.stop();
|
|
}
|
|
|
|
// Reset progress.
|
|
this.UI.progress.played.style.width = "0%";
|
|
this.UI.times.current.textContent = this.formatTime(0);
|
|
|
|
// Play the new track.
|
|
this.play(index);
|
|
}
|
|
|
|
/**
|
|
* Seek to a new position in the currently playing track.
|
|
* @param {Number} per Percentage through the song to skip.
|
|
*/
|
|
seek(per) {
|
|
// Get the Howl we want to manipulate.
|
|
var sound = this.playlist[this.index].howl;
|
|
|
|
if (sound.playing()) {
|
|
// Convert the percent into a seek position.
|
|
sound.seek(sound.duration() * per);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The step called within requestAnimationFrame to update the playback position.
|
|
*/
|
|
step() {
|
|
// Get the Howl we want to manipulate.
|
|
var sound = this.playlist[this.index].howl;
|
|
|
|
var seek = sound.seek() || 0;
|
|
this.UI.times.current.textContent = this.formatTime(Math.round(seek));
|
|
this.UI.progress.played.style.width =
|
|
((seek / sound.duration()) * 100 || 0) + "%";
|
|
|
|
this.UI.progress.buffered.style.width = `${this.getBufferedAmount(sound).bufferedPercent}%`;
|
|
|
|
// If the sound is still playing, continue stepping.
|
|
if (sound.playing()) {
|
|
requestAnimationFrame(this.step.bind(this));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format the time from seconds to M:SS.
|
|
* @param {Number} secs Seconds to format.
|
|
* @return {String} Formatted time.
|
|
*/
|
|
formatTime(secs) {
|
|
var minutes = Math.floor(secs / 60) || 0;
|
|
var seconds = secs - minutes * 60 || 0;
|
|
|
|
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
|
}
|
|
|
|
queue(track, immediatePlay = false) {
|
|
this.playlist.splice(this.index + 1, 0, track);
|
|
|
|
if (immediatePlay) {
|
|
this.skipTo(this.index + 1);
|
|
}
|
|
}
|
|
|
|
getBufferedAmount(sound) {
|
|
if (sound._sounds.length > 0) {
|
|
const audioNode = sound._sounds[0]._node; // The underlying HTML5 Audio element
|
|
if (audioNode.buffered.length > 0) {
|
|
const bufferedEnd = audioNode.buffered.end(0); // Time (seconds) where buffering ends
|
|
const duration = audioNode.duration;
|
|
const bufferedSeconds = bufferedEnd;
|
|
const bufferedPercent = duration ? (bufferedEnd / duration) * 100 : 0;
|
|
return { bufferedSeconds, bufferedPercent };
|
|
}
|
|
}
|
|
return { bufferedSeconds: 0, bufferedPercent: 0 };
|
|
}
|
|
|
|
addFromTrackCard(track) {
|
|
var current = this.playlist[this.index];
|
|
|
|
if (
|
|
track.metadata.title === current.metadata.title &&
|
|
track.metadata.subtitle === current.metadata.subtitle &&
|
|
track.metadata.extra === current.metadata.extra
|
|
) {
|
|
if (current.howl && current.howl.playing()) {
|
|
this.pause();
|
|
} else {
|
|
this.play();
|
|
}
|
|
} else {
|
|
this.queue(track, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!window.playerIsInitialised) {
|
|
window.player = new Player(initialQueue);
|
|
}
|
|
</script>
|