mpvAudioPlayer.js 8.3 KB

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