mpvAudioPlayer.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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, toast }) {
  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 error: ${error}`);
  147. toast(`media error: ${error}`);
  148. self.events.trigger(self, 'error', [
  149. {
  150. type: 'mediadecodeerror'
  151. }
  152. ]);
  153. }
  154. }
  155. getSavedVolume() {
  156. return this.appSettings.get('volume') || 1;
  157. }
  158. currentSrc() {
  159. return this._currentSrc;
  160. }
  161. canPlayMediaType(mediaType) {
  162. return (mediaType || '').toLowerCase() === 'audio';
  163. }
  164. getDeviceProfile(item, options) {
  165. if (this.appHost.getDeviceProfile) {
  166. return this.appHost.getDeviceProfile(item, options);
  167. }
  168. return {};
  169. }
  170. currentTime(val) {
  171. if (val != null) {
  172. window.api.player.seekTo(val);
  173. return;
  174. }
  175. return this._currentTime;
  176. }
  177. currentTimeAsync() {
  178. return new Promise((resolve) => {
  179. window.api.player.getPosition(resolve);
  180. });
  181. }
  182. duration() {
  183. if (this._duration) {
  184. return this._duration;
  185. }
  186. return null;
  187. }
  188. seekable() {
  189. return Boolean(this._duration);
  190. }
  191. getBufferedRanges() {
  192. return [];
  193. }
  194. pause() {
  195. window.api.player.pause();
  196. }
  197. // This is a retry after error
  198. resume() {
  199. this._paused = false;
  200. window.api.player.play();
  201. }
  202. unpause() {
  203. window.api.player.play();
  204. }
  205. paused() {
  206. return this._paused;
  207. }
  208. setPlaybackRate(value) {
  209. this._playRate = value;
  210. window.api.player.setPlaybackRate(value * 1000);
  211. }
  212. getPlaybackRate() {
  213. return this._playRate;
  214. }
  215. getSupportedPlaybackRates() {
  216. return [{
  217. name: '0.5x',
  218. id: 0.5
  219. }, {
  220. name: '0.75x',
  221. id: 0.75
  222. }, {
  223. name: '1x',
  224. id: 1.0
  225. }, {
  226. name: '1.25x',
  227. id: 1.25
  228. }, {
  229. name: '1.5x',
  230. id: 1.5
  231. }, {
  232. name: '1.75x',
  233. id: 1.75
  234. }, {
  235. name: '2x',
  236. id: 2.0
  237. }];
  238. }
  239. saveVolume(value) {
  240. if (value) {
  241. this.appSettings.set('volume', value);
  242. }
  243. }
  244. setVolume(val, save = true) {
  245. this._volume = val;
  246. if (save) {
  247. this.saveVolume((val || 100) / 100);
  248. this.events.trigger(this, 'volumechange');
  249. }
  250. window.api.player.setVolume(val);
  251. }
  252. getVolume() {
  253. return this._volume;
  254. }
  255. volumeUp() {
  256. this.setVolume(Math.min(this.getVolume() + 2, 100));
  257. }
  258. volumeDown() {
  259. this.setVolume(Math.max(this.getVolume() - 2, 0));
  260. }
  261. setMute(mute, triggerEvent = true) {
  262. this._muted = mute;
  263. window.api.player.setMuted(mute);
  264. if (triggerEvent) {
  265. this.events.trigger(this, 'volumechange');
  266. }
  267. }
  268. isMuted() {
  269. return this._muted;
  270. }
  271. supports(feature) {
  272. if (!supportedFeatures) {
  273. supportedFeatures = getSupportedFeatures();
  274. }
  275. return supportedFeatures.indexOf(feature) !== -1;
  276. }
  277. }
  278. let supportedFeatures;
  279. function getSupportedFeatures() {
  280. return ['PlaybackRate'];
  281. }
  282. window._mpvAudioPlayer = mpvAudioPlayer;