let fadeTimeout; function fade(instance, elem, startingVolume) { instance._isFadingOut = true; // Need to record the starting volume on each pass rather than querying elem.volume // This is due to iOS safari not allowing volume changes and always returning the system volume value const newVolume = Math.max(0, startingVolume - 15); console.debug('fading volume to ' + newVolume); api.player.setVolume(newVolume); if (newVolume <= 0) { instance._isFadingOut = false; return Promise.resolve(); } return new Promise(function (resolve, reject) { cancelFadeTimeout(); fadeTimeout = setTimeout(function () { fade(instance, null, newVolume).then(resolve, reject); }, 100); }); } function cancelFadeTimeout() { const timeout = fadeTimeout; if (timeout) { clearTimeout(timeout); fadeTimeout = null; } } class mpvAudioPlayer { constructor({ events, appHost, appSettings }) { const self = this; self.events = events; self.appHost = appHost; self.appSettings = appSettings; self.name = 'MPV Audio Player'; self.type = 'mediaplayer'; self.id = 'mpvaudioplayer'; self.syncPlayWrapAs = 'htmlaudioplayer'; self.useServerPlaybackInfoForAudio = true; self._duration = undefined; self._currentTime = undefined; self._paused = false; self._volume = this.getSavedVolume() * 100; self._playRate = 1; self._hasConnection = false; self.play = (options) => { self._started = false; self._timeUpdated = false; self._currentTime = null; self._duration = undefined; const player = window.api.player; if (!self._hasConnection) { self._hasConnection = true; player.playing.connect(onPlaying); player.positionUpdate.connect(onTimeUpdate); player.finished.connect(onEnded); player.updateDuration.connect(onDuration); player.error.connect(onError); player.paused.connect(onPause); window.api.taskbar.pauseClicked.connect(onPauseClicked); } return setCurrentSrc(options); }; function setCurrentSrc(options) { return new Promise((resolve) => { const val = options.url; self._currentSrc = val; console.debug('playing url: ' + val); // Convert to seconds const ms = (options.playerStartPositionTicks || 0) / 10000; self._currentPlayOptions = options; window.api.player.load(val, { startMilliseconds: ms, autoplay: true }, {type: 'music', headers: {'User-Agent': 'JellyfinMediaPlayer'}, media: {}}, '#1', '', resolve); }); } self.onEndedInternal = () => { const stopInfo = { src: self._currentSrc }; self.events.trigger(self, 'stopped', [stopInfo]); window.api.taskbar.setControlsVisible(false); self._currentTime = null; self._currentSrc = null; self._currentPlayOptions = null; }; self.stop = (destroyPlayer) => { cancelFadeTimeout(); const src = self._currentSrc; if (src) { const originalVolume = self._volume; return fade(self, null, self._volume).then(function () { self.pause(); self.setVolume(originalVolume, false); self.onEndedInternal(); if (destroyPlayer) { self.destroy(); } }); } return Promise.resolve(); }; self.destroy = () => { window.api.player.stop(); const player = window.api.player; self._hasConnection = false; player.playing.disconnect(onPlaying); player.positionUpdate.disconnect(onTimeUpdate); player.finished.disconnect(onEnded); self._duration = undefined; player.updateDuration.disconnect(onDuration); player.error.disconnect(onError); player.paused.disconnect(onPause); window.api.taskbar.pauseClicked.disconnect(onPauseClicked); }; function onDuration(duration) { self._duration = duration; } function onEnded() { self.onEndedInternal(); } function onTimeUpdate(time) { // Don't trigger events after user stop if (!self._isFadingOut) { self._currentTime = time; self.events.trigger(self, 'timeupdate'); window.api.taskbar.setProgress(time * 100 / self._duration); } } function onPlaying() { if (!self._started) { self._started = true; window.api.taskbar.setControlsVisible(true); } self.setPlaybackRate(1); self.setMute(false); if (self._paused) { self._paused = false; self.events.trigger(self, 'unpause'); window.api.taskbar.setPaused(false); } self.events.trigger(self, 'playing'); } function onPause() { self._paused = true; self.events.trigger(self, 'pause'); window.api.taskbar.setPaused(true); } function onError(error) { console.error(`media element error: ${error}`); self.events.trigger(self, 'error', [ { type: 'mediadecodeerror' } ]); } function onPauseClicked() { self.paused() ? self.unpause() : self.pause(); } } getSavedVolume() { return this.appSettings.get('volume') || 1; } currentSrc() { return this._currentSrc; } canPlayMediaType(mediaType) { return (mediaType || '').toLowerCase() === 'audio'; } getDeviceProfile(item, options) { if (this.appHost.getDeviceProfile) { return this.appHost.getDeviceProfile(item, options); } return {}; } currentTime(val) { if (val != null) { window.api.player.seekTo(val); return; } return this._currentTime; } currentTimeAsync() { return new Promise((resolve) => { window.api.player.getPosition(resolve); }); } duration() { if (this._duration) { return this._duration; } return null; } seekable() { return Boolean(this._duration); } getBufferedRanges() { return []; } pause() { window.api.player.pause(); } // This is a retry after error resume() { this._paused = false; window.api.player.play(); } unpause() { window.api.player.play(); } paused() { return this._paused; } setPlaybackRate(value) { this._playRate = value; window.api.player.setPlaybackRate(value * 1000); } getPlaybackRate() { return this._playRate; } getSupportedPlaybackRates() { return [{ name: '0.5x', id: 0.5 }, { name: '0.75x', id: 0.75 }, { name: '1x', id: 1.0 }, { name: '1.25x', id: 1.25 }, { name: '1.5x', id: 1.5 }, { name: '1.75x', id: 1.75 }, { name: '2x', id: 2.0 }]; } saveVolume(value) { if (value) { this.appSettings.set('volume', value); } } setVolume(val, save = true) { this._volume = val; if (save) { this.saveVolume((val || 100) / 100); this.events.trigger(this, 'volumechange'); } window.api.player.setVolume(val); } getVolume() { return this._volume; } volumeUp() { this.setVolume(Math.min(this.getVolume() + 2, 100)); } volumeDown() { this.setVolume(Math.max(this.getVolume() - 2, 0)); } setMute(mute) { this._muted = mute; window.api.player.setMuted(mute); } isMuted() { return this._muted; } supports(feature) { if (!supportedFeatures) { supportedFeatures = getSupportedFeatures(); } return supportedFeatures.indexOf(feature) !== -1; } } let supportedFeatures; function getSupportedFeatures() { return ['PlaybackRate']; } window._mpvAudioPlayer = mpvAudioPlayer;