mpvAudioPlayer.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. let fadeTimeout;
  2. function fade(instance, elem, startingVolume) {
  3. instance._isFadingOut = true;
  4. // Need to record the starting volume on each pass rather than querying elem.volume
  5. // This is due to iOS safari not allowing volume changes and always returning the system volume value
  6. const newVolume = Math.max(0, startingVolume - 15);
  7. console.debug('fading volume to ' + newVolume);
  8. api.player.setVolume(newVolume);
  9. if (newVolume <= 0) {
  10. instance._isFadingOut = false;
  11. return Promise.resolve();
  12. }
  13. return new Promise(function (resolve, reject) {
  14. cancelFadeTimeout();
  15. fadeTimeout = setTimeout(function () {
  16. fade(instance, null, newVolume).then(resolve, reject);
  17. }, 100);
  18. });
  19. }
  20. function cancelFadeTimeout() {
  21. const timeout = fadeTimeout;
  22. if (timeout) {
  23. clearTimeout(timeout);
  24. fadeTimeout = null;
  25. }
  26. }
  27. class mpvAudioPlayer {
  28. constructor({ events, appHost, appSettings }) {
  29. const self = this;
  30. self.events = events;
  31. self.appHost = appHost;
  32. self.appSettings = appSettings;
  33. self.name = 'MPV Audio Player';
  34. self.type = 'mediaplayer';
  35. self.id = 'mpvaudioplayer';
  36. self.syncPlayWrapAs = 'htmlaudioplayer';
  37. self.useServerPlaybackInfoForAudio = true;
  38. self._duration = undefined;
  39. self._currentTime = undefined;
  40. self._paused = false;
  41. self._volume = self.getSavedVolume() * 100;
  42. self._playRate = 1;
  43. self._hasConnection = false;
  44. self.play = (options) => {
  45. self._started = false;
  46. self._timeUpdated = false;
  47. self._currentTime = null;
  48. self._duration = undefined;
  49. const player = window.api.player;
  50. if (!self._hasConnection) {
  51. self._hasConnection = true;
  52. player.playing.connect(onPlaying);
  53. player.positionUpdate.connect(onTimeUpdate);
  54. player.finished.connect(onEnded);
  55. player.updateDuration.connect(onDuration);
  56. player.error.connect(onError);
  57. player.paused.connect(onPause);
  58. }
  59. return setCurrentSrc(options);
  60. };
  61. function setCurrentSrc(options) {
  62. return new Promise((resolve) => {
  63. const val = options.url;
  64. self._currentSrc = val;
  65. console.debug('playing url: ' + val);
  66. // Convert to seconds
  67. const ms = (options.playerStartPositionTicks || 0) / 10000;
  68. self._currentPlayOptions = options;
  69. window.api.player.load(val,
  70. { startMilliseconds: ms, autoplay: true },
  71. {type: 'music', headers: {'User-Agent': 'JellyfinMediaPlayer'}, metadata: options.item, media: {}},
  72. '#1',
  73. '',
  74. resolve);
  75. });
  76. }
  77. self.onEndedInternal = () => {
  78. const stopInfo = {
  79. src: self._currentSrc
  80. };
  81. self.events.trigger(self, 'stopped', [stopInfo]);
  82. self._currentTime = null;
  83. self._currentSrc = null;
  84. self._currentPlayOptions = null;
  85. };
  86. self.stop = (destroyPlayer) => {
  87. cancelFadeTimeout();
  88. const src = self._currentSrc;
  89. if (src) {
  90. const originalVolume = self._volume;
  91. return fade(self, null, self._volume).then(function () {
  92. self.pause();
  93. self.setVolume(originalVolume, false);
  94. self.onEndedInternal();
  95. if (destroyPlayer) {
  96. self.destroy();
  97. }
  98. });
  99. }
  100. return Promise.resolve();
  101. };
  102. self.destroy = () => {
  103. window.api.player.stop();
  104. const player = window.api.player;
  105. self._hasConnection = false;
  106. player.playing.disconnect(onPlaying);
  107. player.positionUpdate.disconnect(onTimeUpdate);
  108. player.finished.disconnect(onEnded);
  109. self._duration = undefined;
  110. player.updateDuration.disconnect(onDuration);
  111. player.error.disconnect(onError);
  112. player.paused.disconnect(onPause);
  113. };
  114. function onDuration(duration) {
  115. self._duration = duration;
  116. }
  117. function onEnded() {
  118. self.onEndedInternal();
  119. }
  120. function onTimeUpdate(time) {
  121. // Don't trigger events after user stop
  122. if (!self._isFadingOut) {
  123. self._currentTime = time;
  124. self.events.trigger(self, 'timeupdate');
  125. }
  126. }
  127. function onPlaying() {
  128. if (!self._started) {
  129. self._started = true;
  130. }
  131. const volume = self.getSavedVolume() * 100;
  132. self.setVolume(volume, volume != self._volume);
  133. self.setPlaybackRate(1);
  134. self.setMute(false, false);
  135. if (self._paused) {
  136. self._paused = false;
  137. self.events.trigger(self, 'unpause');
  138. }
  139. self.events.trigger(self, 'playing');
  140. }
  141. function onPause() {
  142. self._paused = true;
  143. self.events.trigger(self, 'pause');
  144. }
  145. function onError(error) {
  146. console.error(`media element error: ${error}`);
  147. self.events.trigger(self, 'error', [
  148. {
  149. type: 'mediadecodeerror'
  150. }
  151. ]);
  152. }
  153. }
  154. getSavedVolume() {
  155. return this.appSettings.get('volume') || 1;
  156. }
  157. currentSrc() {
  158. return this._currentSrc;
  159. }
  160. canPlayMediaType(mediaType) {
  161. return (mediaType || '').toLowerCase() === 'audio';
  162. }
  163. getDeviceProfile(item, options) {
  164. if (this.appHost.getDeviceProfile) {
  165. return this.appHost.getDeviceProfile(item, options);
  166. }
  167. return {};
  168. }
  169. currentTime(val) {
  170. if (val != null) {
  171. window.api.player.seekTo(val);
  172. return;
  173. }
  174. return this._currentTime;
  175. }
  176. currentTimeAsync() {
  177. return new Promise((resolve) => {
  178. window.api.player.getPosition(resolve);
  179. });
  180. }
  181. duration() {
  182. if (this._duration) {
  183. return this._duration;
  184. }
  185. return null;
  186. }
  187. seekable() {
  188. return Boolean(this._duration);
  189. }
  190. getBufferedRanges() {
  191. return [];
  192. }
  193. pause() {
  194. window.api.player.pause();
  195. }
  196. // This is a retry after error
  197. resume() {
  198. this._paused = false;
  199. window.api.player.play();
  200. }
  201. unpause() {
  202. window.api.player.play();
  203. }
  204. paused() {
  205. return this._paused;
  206. }
  207. setPlaybackRate(value) {
  208. this._playRate = value;
  209. window.api.player.setPlaybackRate(value * 1000);
  210. }
  211. getPlaybackRate() {
  212. return this._playRate;
  213. }
  214. getSupportedPlaybackRates() {
  215. return [{
  216. name: '0.5x',
  217. id: 0.5
  218. }, {
  219. name: '0.75x',
  220. id: 0.75
  221. }, {
  222. name: '1x',
  223. id: 1.0
  224. }, {
  225. name: '1.25x',
  226. id: 1.25
  227. }, {
  228. name: '1.5x',
  229. id: 1.5
  230. }, {
  231. name: '1.75x',
  232. id: 1.75
  233. }, {
  234. name: '2x',
  235. id: 2.0
  236. }];
  237. }
  238. saveVolume(value) {
  239. if (value) {
  240. this.appSettings.set('volume', value);
  241. }
  242. }
  243. setVolume(val, save = true) {
  244. this._volume = val;
  245. if (save) {
  246. this.saveVolume((val || 100) / 100);
  247. this.events.trigger(this, 'volumechange');
  248. }
  249. window.api.player.setVolume(val);
  250. }
  251. getVolume() {
  252. return this._volume;
  253. }
  254. volumeUp() {
  255. this.setVolume(Math.min(this.getVolume() + 2, 100));
  256. }
  257. volumeDown() {
  258. this.setVolume(Math.max(this.getVolume() - 2, 0));
  259. }
  260. setMute(mute, triggerEvent = true) {
  261. this._muted = mute;
  262. window.api.player.setMuted(mute);
  263. if (triggerEvent) {
  264. this.events.trigger(this, 'volumechange');
  265. }
  266. }
  267. isMuted() {
  268. return this._muted;
  269. }
  270. supports(feature) {
  271. if (!supportedFeatures) {
  272. supportedFeatures = getSupportedFeatures();
  273. }
  274. return supportedFeatures.indexOf(feature) !== -1;
  275. }
  276. }
  277. let supportedFeatures;
  278. function getSupportedFeatures() {
  279. return ['PlaybackRate'];
  280. }
  281. window._mpvAudioPlayer = mpvAudioPlayer;