Files
lms/docroot/js/mediaplayer.js
T

490 lines
14 KiB
JavaScript

// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
// Keep in sync with MediaPlayer::TranscodingMode cpp
LMSTranscodingMode = {
Never: 0,
Always: 1,
IfFormatNotSupported: 2,
}
Object.freeze(LMSTranscodingMode);
class LMSMediaPlayer {
// How much to increase / decrease volume when adjusting it with keyboard shortcuts
static #volumeStepAmount = 0.05;
// How much to seek back / forward (in seconds) with keyboard shortcuts
static #seekAmount = 5;
static #Mode = {
Transcoding: 1,
File: 2,
}
#root;
#elems;
#offset;
#trackId;
#duration;
#audioNativeSrc;
#audioTranscodingSrc;
#settings;
#playedDuration;
#lastStartPlaying;
#audioIsInit;
#pendingTrackParameters;
#gainNode;
#audioCtx;
constructor(root, defaultSettings) {
this.#root = root;
this.#elems = {};
this.#offset = 0;
this.#trackId = null;
this.#duration = 0;
this.#audioNativeSrc;
this.#audioTranscodingSrc;
this.#settings = {};
this.#playedDuration = 0;
this.#lastStartPlaying = null;
this.#audioIsInit = false;
this.#pendingTrackParameters = null;
this.#gainNode = null;
this.#audioCtx = null;
this.#elems.audio = document.getElementById("lms-mp-audio");
this.#elems.playpause = document.getElementById("lms-mp-playpause");
this.#elems.previous = document.getElementById("lms-mp-previous");
this.#elems.next = document.getElementById("lms-mp-next");
this.#elems.progress = document.getElementById("lms-mp-progress");
this.#elems.seek = document.getElementById("lms-mp-seek");
this.#elems.curtime = document.getElementById("lms-mp-curtime");
this.#elems.duration = document.getElementById("lms-mp-duration");
this.#elems.volume = document.getElementById("lms-mp-volume");
this.#elems.volumeslider = document.getElementById("lms-mp-volume-slider");
this.#elems.transcodingActive = document.getElementById("lms-transcoding-active");
this.#elems.playpause.addEventListener("click", () => {
this.#playPause();
});
this.#elems.previous.addEventListener("click", () => {
this.#playPrevious();
});
this.#elems.next.addEventListener("click", () => {
this.#playNext();
});
this.#elems.seek.addEventListener("change", () => {
this.#seekTo(parseInt(this.#elems.seek.value, 10));
});
this.#elems.audio.addEventListener("play", this.#updateControls.bind(this));
this.#elems.audio.addEventListener("playing", this.#updateControls.bind(this));
this.#elems.audio.addEventListener("pause", this.#updateControls.bind(this));
this.#elems.audio.addEventListener("play", this.#updateMediaSessionState.bind(this));
this.#elems.audio.addEventListener("playing", this.#updateMediaSessionState.bind(this));
this.#elems.audio.addEventListener("pause", this.#updateMediaSessionState.bind(this));
this.#elems.audio.addEventListener("pause", this.#pauseTimer.bind(this));
this.#elems.audio.addEventListener("playing", this.#startTimer.bind(this));
this.#elems.audio.addEventListener("waiting", this.#pauseTimer.bind(this));
this.#elems.audio.addEventListener("timeupdate", () => {
this.#elems.progress.style.width = "" + ((this.#offset + this.#elems.audio.currentTime) / this.#duration) * 100 + "%";
this.#elems.curtime.innerHTML = this.#durationToString(this.#offset + this.#elems.audio.currentTime);
});
this.#elems.audio.addEventListener("ended", () => {
this.#resetTimer();
Wt.emit(this.#root, "playbackEnded");
});
this.#elems.audio.addEventListener("canplay", () => {
if (this.#getAudioMode() == LMSMediaPlayer.#Mode.Transcoding) {
this.#elems.transcodingActive.style.display = "inline";
}
else {
this.#elems.transcodingActive.style.display = "none";
}
});
this.#initVolume();
this.#initDefaultSettings(defaultSettings);
this.#elems.volumeslider.addEventListener("input", () => {
this.#setVolume(this.#elems.volumeslider.value);
});
this.#elems.volume.addEventListener("click", () => {
if (this.#elems.audio.volume != 0) {
this.#setVolume(0);
}
else {
this.#setVolume(this.#elems.lastvolume);
}
});
document.addEventListener("keydown", (event) => {
let handled = false;
if (event.target instanceof HTMLInputElement)
return;
if (event.keyCode == 32) {
this.#playPause();
handled = true;
}
else if (event.ctrlKey && !event.shiftKey && event.keyCode == 37) {
this.#playPrevious();
handled = true;
}
else if (event.ctrlKey && !event.shiftKey && event.keyCode == 39) {
this.#playNext();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 40) {
this.#stepVolumeDown();
handled = true;
}
else if (event.ctrlKey && event.keyCode == 38) {
this.#stepVolumeUp();
handled = true;
}
else if (event.ctrlKey && event.shiftKey && event.keyCode == 37) {
this.#seekBack();
handled = true;
}
else if (event.ctrlKey && event.shiftKey && event.keyCode == 39) {
this.#seekForward();
handled = true;
}
if (handled)
event.preventDefault();
});
document.addEventListener("touchstart", this.#unlock.bind(this));
document.addEventListener("touchend", this.#unlock.bind(this));
document.addEventListener("click", this.#unlock.bind(this));
}
#unlock() {
document.removeEventListener("touchstart", this.#unlock.bind(this));
document.removeEventListener("touchend", this.#unlock.bind(this));
document.removeEventListener("click", this.#unlock.bind(this));
this.#initAudioCtx();
};
#initAudioCtx() {
if (this.#audioIsInit)
return;
this.#audioIsInit = true;
this.#audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.#gainNode = this.#audioCtx.createGain();
let source = this.#audioCtx.createMediaElementSource(this.#elems.audio);
source.connect(this.#gainNode);
this.#gainNode.connect(this.#audioCtx.destination);
this.#audioCtx.resume(); // not sure of this
if ("mediaSession" in navigator) {
navigator.mediaSession.setActionHandler("play", () => {
this.#playPause();
});
navigator.mediaSession.setActionHandler("pause", () => {
this.#playPause();
});
navigator.mediaSession.setActionHandler("previoustrack", () => {
this.#playPrevious();
});
navigator.mediaSession.setActionHandler("nexttrack", () => {
this.#playNext();
});
navigator.mediaSession.setActionHandler("seekto", (e) => {
this.#seekTo(e.seekTime);
});
}
if (this.#pendingTrackParameters != null) {
this.#applyAudioTrackParameters(this.#pendingTrackParameters);
this.#pendingTrackParameters = null;
}
}
#updateControls() {
const pauseClass = "fa-pause";
const playClass = "fa-play";
if (this.#elems.audio.paused) {
this.#elems.playpause.firstElementChild.classList.remove(pauseClass);
this.#elems.playpause.firstElementChild.classList.add(playClass);
}
else {
this.#elems.playpause.firstElementChild.classList.remove(playClass);
this.#elems.playpause.firstElementChild.classList.add(pauseClass);
}
}
#startTimer() {
if (this.#lastStartPlaying == null)
Wt.emit(this.#root, "scrobbleListenNow", this.#trackId);
this.#lastStartPlaying = Date.now();
}
#pauseTimer() {
if (this.#lastStartPlaying != null) {
this.#playedDuration += Date.now() - this.#lastStartPlaying;
this.#lastStartPlaying = null;
}
}
#resetTimer() {
if (this.#lastStartPlaying != null)
this.#pauseTimer();
if (this.#playedDuration > 0) {
Wt.emit(this.#root, "scrobbleListenFinished", this.#trackId, this.#playedDuration);
this.#playedDuration = 0;
}
}
#durationToString(duration) {
const seconds = parseInt(duration, 10);
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.round(seconds % 60);
return [
h,
m > 9 ? m : (h ? '0' + m : m || '0'),
s > 9 ? s : '0' + s
].filter(Boolean).join(':');
}
#playTrack() {
this.#elems.audio.play()
.then(_ => { })
.catch(error => { console.log("Cannot play audio: " + error); });
}
#playPause() {
this.#initAudioCtx();
if (this.#elems.audio.paused && this.#elems.audio.children.length > 0) {
this.#playTrack();
}
else
this.#elems.audio.pause();
}
#playPrevious() {
this.#initAudioCtx();
Wt.emit(this.#root, "playPrevious");
}
#playNext() {
this.#initAudioCtx();
Wt.emit(this.#root, "playNext");
}
#initVolume() {
if (typeof (Storage) !== "undefined" && localStorage.volume) {
this.#elems.volumeslider.value = Number(localStorage.volume);
}
this.#setVolume(this.#elems.volumeslider.value);
}
#initDefaultSettings = function (defaultSettings) {
if (typeof (Storage) !== "undefined" && localStorage.settings) {
this.#settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
}
else {
this.#settings = defaultSettings;
}
Wt.emit(this.#root, "settingsLoaded", JSON.stringify(this.#settings));
}
#setVolume(volume) {
this.#elems.lastvolume = this.#elems.audio.volume;
this.#elems.audio.volume = volume;
this.#elems.volumeslider.value = volume;
if (volume > 0.5) {
this.#elems.volume.classList.remove("fa-volume-off");
this.#elems.volume.classList.remove("fa-volume-down");
this.#elems.volume.classList.add("fa-volume-up");
}
else if (volume > 0) {
this.#elems.volume.classList.remove("fa-volume-off");
this.#elems.volume.classList.remove("fa-volume-up");
this.#elems.volume.classList.add("fa-volume-down");
}
else {
this.#elems.volume.classList.remove("fa-volume-up");
this.#elems.volume.classList.remove("fa-volume-down");
this.#elems.volume.classList.add("fa-volume-off");
}
if (typeof (Storage) !== "undefined") {
localStorage.volume = volume;
}
}
#stepVolumeDown() {
let currentVolume = this.#elems.audio.volume;
let remainder = (currentVolume * 10) % (LMSMediaPlayer.#volumeStepAmount * 10);
let newVolume = remainder === 0 ? currentVolume - LMSMediaPlayer.#volumeStepAmount : currentVolume - (remainder / 10);
this.#setVolume(Math.max(newVolume, 0));
}
#stepVolumeUp() {
let currentVolume = this.#elems.audio.volume;
let remainder = (currentVolume * 10) % (LMSMediaPlayer.#volumeStepAmount * 10);
let newVolume = remainder === 0 ? currentVolume + LMSMediaPlayer.#volumeStepAmount : currentVolume + (LMSMediaPlayer.#volumeStepAmount - (remainder / 10));
this.#setVolume(Math.min(newVolume, 1));
}
#setReplayGain(replayGain) {
this.#gainNode.gain.value = Math.pow(10, (this.#settings.replayGain.preAmpGain + replayGain) / 20);
}
#seekTo(seekTime) {
this.#initAudioCtx();
let mode = this.#getAudioMode();
if (!mode)
return;
switch (mode) {
case LMSMediaPlayer.#Mode.Transcoding:
this.#offset = seekTime;
this.#removeAudioSources();
this.#addAudioSource(this.#audioTranscodingSrc + "&offset=" + this.#offset);
this.#elems.audio.load();
this.#elems.audio.currentTime = 0;
this.#playTrack();
break;
case LMSMediaPlayer.#Mode.File:
this.#elems.audio.currentTime = seekTime;
this.#playTrack();
break;
}
this.#updateMediaSessionState();
}
#seekBack() {
let currentPosition = this.#offset + this.#elems.audio.currentTime;
let newPosition = currentPosition - LMSMediaPlayer.#seekAmount;
this.#seekTo(Math.max(newPosition, 0));
}
#seekForward() {
let currentPosition = this.#offset + this.#elems.audio.currentTime;
let newPosition = currentPosition + LMSMediaPlayer.#seekAmount;
this.#seekTo(Math.min(newPosition, this.#duration));
}
#updateMediaSessionState() {
if ("mediaSession" in navigator) {
navigator.mediaSession.setPositionState({
duration: this.#duration,
playbackRate: 1,
position: Math.min(this.#offset + this.#elems.audio.currentTime, this.#duration),
});
if (this.#elems.audio.paused)
navigator.mediaSession.playbackState = "paused";
else
navigator.mediaSession.playbackState = "playing";
}
}
#removeAudioSources() {
while (this.#elems.audio.lastElementChild) {
this.#elems.audio.removeChild(this.#elems.audio.lastElementChild);
}
}
#addAudioSource(audioSrc) {
let source = document.createElement('source');
source.src = audioSrc;
this.#elems.audio.appendChild(source);
}
#getAudioMode() {
if (this.#elems.audio.currentSrc) {
if (this.#elems.audio.currentSrc.includes("format"))
return LMSMediaPlayer.#Mode.Transcoding;
else
return LMSMediaPlayer.#Mode.File;
}
else
return undefined;
}
#applyAudioTrackParameters(params) {
this.#setReplayGain(params.replayGain);
if ("mediaSession" in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: params.title,
artist: params.artist,
album: params.release,
artwork: params.artwork,
});
}
}
loadTrack(params, autoplay) {
this.#resetTimer();
this.#trackId = params.trackId;
this.#offset = 0;
this.#duration = params.duration;
this.#audioNativeSrc = params.nativeResource;
this.#audioTranscodingSrc = params.transcodingResource + "&bitrate=" + this.#settings.transcoding.bitrate + "&format=" + this.#settings.transcoding.format;
this.#elems.seek.max = this.#duration;
this.#removeAudioSources();
// ! order is important
if (this.#settings.transcoding.mode == LMSTranscodingMode.Never || this.#settings.transcoding.mode == LMSTranscodingMode.IfFormatNotSupported) {
this.#addAudioSource(this.#audioNativeSrc);
}
if (this.#settings.transcoding.mode == LMSTranscodingMode.Always || this.#settings.transcoding.mode == LMSTranscodingMode.IfFormatNotSupported) {
this.#addAudioSource(this.#audioTranscodingSrc);
}
this.#elems.audio.load();
this.#elems.curtime.innerHTML = this.#durationToString(this.#offset);
this.#elems.duration.innerHTML = this.#durationToString(this.#duration);
if (!this.#audioIsInit) {
this.#pendingTrackParameters = params;
return;
}
this.#applyAudioTrackParameters(params);
if (autoplay && this.#audioCtx.state == "running")
this.#playTrack();
}
stop() {
this.#elems.audio.pause();
}
setSettings(settings) {
this.#settings = settings;
if (typeof (Storage) !== "undefined") {
localStorage.settings = JSON.stringify(this.#settings);
}
}
}
// @license-end