379 lines
9.5 KiB
JavaScript
379 lines
9.5 KiB
JavaScript
|
|
import { Events } from 'jellyfin-apiclient';
|
||
|
|
import * as htmlMediaHelper from '../../components/htmlMediaHelper';
|
||
|
|
|
||
|
|
function loadScript(src) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const s = document.createElement('script');
|
||
|
|
s.src = src;
|
||
|
|
s.onload = resolve;
|
||
|
|
s.onerror = reject;
|
||
|
|
document.head.appendChild(s);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function createApi() {
|
||
|
|
await loadScript('qrc:///qtwebchannel/qwebchannel.js');
|
||
|
|
const channel = await new Promise((resolve) => {
|
||
|
|
/*global QWebChannel */
|
||
|
|
new QWebChannel(window.qt.webChannelTransport, resolve);
|
||
|
|
});
|
||
|
|
return channel.objects;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function getApi() {
|
||
|
|
if (window.apiPromise) {
|
||
|
|
return await window.apiPromise;
|
||
|
|
}
|
||
|
|
|
||
|
|
window.apiPromise = createApi();
|
||
|
|
return await window.apiPromise;
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
instance.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 HtmlAudioPlayer {
|
||
|
|
constructor() {
|
||
|
|
const self = this;
|
||
|
|
|
||
|
|
self.name = 'Html Audio Player';
|
||
|
|
self.type = 'mediaplayer';
|
||
|
|
self.id = 'htmlaudioplayer';
|
||
|
|
self.useServerPlaybackInfoForAudio = true;
|
||
|
|
self.mustDestroy = true;
|
||
|
|
|
||
|
|
self._duration = undefined;
|
||
|
|
self._currentTime = undefined;
|
||
|
|
self._paused = false;
|
||
|
|
self._volume = htmlMediaHelper.getSavedVolume() * 100;
|
||
|
|
self._playRate = 1;
|
||
|
|
|
||
|
|
self.api = undefined;
|
||
|
|
|
||
|
|
self.ensureApi = async () => {
|
||
|
|
if (!self.api) {
|
||
|
|
self.api = await getApi();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
self.play = async (options) => {
|
||
|
|
self._started = false;
|
||
|
|
self._timeUpdated = false;
|
||
|
|
self._currentTime = null;
|
||
|
|
self._duration = undefined;
|
||
|
|
|
||
|
|
await self.ensureApi();
|
||
|
|
const player = self.api.player;
|
||
|
|
player.playing.connect(onPlaying);
|
||
|
|
player.positionUpdate.connect(onTimeUpdate);
|
||
|
|
player.finished.connect(onEnded);
|
||
|
|
player.updateDuration.connect(onDuration);
|
||
|
|
player.error.connect(onError);
|
||
|
|
player.paused.connect(onPause);
|
||
|
|
|
||
|
|
return await 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;
|
||
|
|
|
||
|
|
self.api.player.load(val,
|
||
|
|
{ startMilliseconds: ms, autoplay: true },
|
||
|
|
{type: 'music', headers: {'User-Agent': 'JellyfinMediaPlayer'}, frameRate: 0, media: {}},
|
||
|
|
'#1',
|
||
|
|
'',
|
||
|
|
resolve);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
self.onEndedInternal = () => {
|
||
|
|
const stopInfo = {
|
||
|
|
src: self._currentSrc
|
||
|
|
};
|
||
|
|
|
||
|
|
Events.trigger(self, 'stopped', [stopInfo]);
|
||
|
|
|
||
|
|
self._currentTime = null;
|
||
|
|
self._currentSrc = null;
|
||
|
|
self._currentPlayOptions = null;
|
||
|
|
};
|
||
|
|
|
||
|
|
self.stop = async (destroyPlayer) => {
|
||
|
|
cancelFadeTimeout();
|
||
|
|
|
||
|
|
const src = self._currentSrc;
|
||
|
|
|
||
|
|
if (src) {
|
||
|
|
const originalVolume = self._volume;
|
||
|
|
|
||
|
|
await self.ensureApi();
|
||
|
|
return await fade(self, null, self._volume).then(function () {
|
||
|
|
self.pause();
|
||
|
|
self.setVolume(originalVolume, false);
|
||
|
|
|
||
|
|
self.onEndedInternal();
|
||
|
|
|
||
|
|
if (destroyPlayer) {
|
||
|
|
self.destroy();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
};
|
||
|
|
|
||
|
|
self.destroy = async () => {
|
||
|
|
await self.ensureApi();
|
||
|
|
self.api.player.stop();
|
||
|
|
|
||
|
|
const player = self.api.player;
|
||
|
|
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);
|
||
|
|
};
|
||
|
|
|
||
|
|
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;
|
||
|
|
Events.trigger(self, 'timeupdate');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function onPlaying() {
|
||
|
|
if (!self._started) {
|
||
|
|
self._started = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
self.setPlaybackRate(1);
|
||
|
|
self.setMute(false);
|
||
|
|
|
||
|
|
if (self._paused) {
|
||
|
|
self._paused = false;
|
||
|
|
Events.trigger(self, 'unpause');
|
||
|
|
}
|
||
|
|
|
||
|
|
Events.trigger(self, 'playing');
|
||
|
|
}
|
||
|
|
|
||
|
|
function onPause() {
|
||
|
|
self._paused = true;
|
||
|
|
Events.trigger(self, 'pause');
|
||
|
|
}
|
||
|
|
|
||
|
|
function onError(error) {
|
||
|
|
console.error(`media element error: ${error}`);
|
||
|
|
|
||
|
|
htmlMediaHelper.onErrorInternal(self, 'mediadecodeerror');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
currentSrc() {
|
||
|
|
return this._currentSrc;
|
||
|
|
}
|
||
|
|
|
||
|
|
canPlayMediaType(mediaType) {
|
||
|
|
return (mediaType || '').toLowerCase() === 'audio';
|
||
|
|
}
|
||
|
|
|
||
|
|
getDeviceProfile() {
|
||
|
|
return Promise.resolve({
|
||
|
|
'Name': 'Jellyfin Media Player',
|
||
|
|
'MusicStreamingTranscodingBitrate': 1280000,
|
||
|
|
'TimelineOffsetSeconds': 5,
|
||
|
|
'TranscodingProfiles': [
|
||
|
|
{'Type': 'Audio'}
|
||
|
|
],
|
||
|
|
'DirectPlayProfiles': [{'Type': 'Audio'}],
|
||
|
|
'ResponseProfiles': [],
|
||
|
|
'ContainerProfiles': [],
|
||
|
|
'CodecProfiles': [],
|
||
|
|
'SubtitleProfiles': []
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
currentTime(val) {
|
||
|
|
if (val != null) {
|
||
|
|
this.ensureApi().then(() => {
|
||
|
|
this.api.player.seekTo(val);
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
return this._currentTime;
|
||
|
|
}
|
||
|
|
|
||
|
|
async currentTimeAsync() {
|
||
|
|
await this.ensureApi();
|
||
|
|
return await new Promise((resolve) => {
|
||
|
|
this.api.player.getPosition(resolve);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
duration() {
|
||
|
|
if (this._duration) {
|
||
|
|
return this._duration;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
seekable() {
|
||
|
|
return Boolean(this._duration);
|
||
|
|
}
|
||
|
|
|
||
|
|
getBufferedRanges() {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
async pause() {
|
||
|
|
await this.ensureApi();
|
||
|
|
this.api.player.pause();
|
||
|
|
}
|
||
|
|
|
||
|
|
// This is a retry after error
|
||
|
|
async resume() {
|
||
|
|
await this.ensureApi();
|
||
|
|
this._paused = false;
|
||
|
|
this.api.player.play();
|
||
|
|
}
|
||
|
|
|
||
|
|
async unpause() {
|
||
|
|
await this.ensureApi();
|
||
|
|
this.api.player.play();
|
||
|
|
}
|
||
|
|
|
||
|
|
paused() {
|
||
|
|
return this._paused;
|
||
|
|
}
|
||
|
|
|
||
|
|
async setPlaybackRate(value) {
|
||
|
|
this._playRate = value;
|
||
|
|
await this.ensureApi();
|
||
|
|
this.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
|
||
|
|
}];
|
||
|
|
}
|
||
|
|
|
||
|
|
async setVolume(val, save = true) {
|
||
|
|
this._volume = val;
|
||
|
|
if (save) {
|
||
|
|
htmlMediaHelper.saveVolume((val || 100) / 100);
|
||
|
|
Events.trigger(this, 'volumechange');
|
||
|
|
}
|
||
|
|
await this.ensureApi();
|
||
|
|
this.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));
|
||
|
|
}
|
||
|
|
|
||
|
|
async setMute(mute) {
|
||
|
|
this._muted = mute;
|
||
|
|
await this.ensureApi();
|
||
|
|
this.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'];
|
||
|
|
}
|
||
|
|
|
||
|
|
export default HtmlAudioPlayer;
|