WebOrbiton
v1.0.0.6

Publisium

224 lines · 7.0 KB
  1. (function () {
  2. var SPEEDS = [1, 1.25, 1.5, 1.75, 2, 0.75];
  3. ​
  4. function format(seconds) {
  5. if (!isFinite(seconds) || seconds < 0) {
  6. seconds = 0;
  7. }
  8. var whole = Math.floor(seconds);
  9. var minutes = Math.floor(whole / 60);
  10. var rest = whole % 60;
  11. ​
  12. return minutes + ':' + (rest < 10 ? '0' : '') + rest;
  13. }
  14. ​
  15. function init(root) {
  16. var tracks;
  17. try {
  18. tracks = JSON.parse(root.getAttribute('data-tracks') || '[]');
  19. } catch (error) {
  20. return;
  21. }
  22. if (!Array.isArray(tracks) || tracks.length === 0) {
  23. return;
  24. }
  25. ​
  26. var toggle = root.querySelector('.read-aloud-toggle');
  27. var seek = root.querySelector('.read-aloud-seek');
  28. var time = root.querySelector('.read-aloud-time');
  29. var speedButton = root.querySelector('.read-aloud-speed');
  30. var labelPlay = root.getAttribute('data-label-play') || 'Play';
  31. var labelPause = root.getAttribute('data-label-pause') || 'Pause';
  32. ​
  33. var audio = new Audio();
  34. audio.preload = 'metadata';
  35. ​
  36. var durations = tracks.map(function () { return 0; });
  37. var index = 0;
  38. var speedIndex = 0;
  39. var probed = false;
  40. var dragging = false;
  41. var loaded = false;
  42. ​
  43. seek.disabled = true;
  44. ​
  45. function total() {
  46. return durations.reduce(function (sum, value) { return sum + value; }, 0);
  47. }
  48. ​
  49. function known() {
  50. return durations.every(function (value) { return value > 0; });
  51. }
  52. ​
  53. function offset(trackIndex) {
  54. var sum = 0;
  55. for (var i = 0; i < trackIndex; i++) {
  56. sum += durations[i];
  57. }
  58. ​
  59. return sum;
  60. }
  61. ​
  62. function position() {
  63. return offset(index) + (audio.currentTime || 0);
  64. }
  65. ​
  66. function render() {
  67. var current = loaded ? position() : 0;
  68. ​
  69. if (known()) {
  70. var sum = total();
  71. if (!dragging) {
  72. seek.value = String(sum > 0 ? Math.round((current / sum) * 1000) : 0);
  73. }
  74. seek.disabled = false;
  75. time.textContent = format(dragging ? (seek.value / 1000) * sum : current) + ' / ' + format(sum);
  76. } else {
  77. time.textContent = format(audio.currentTime || 0);
  78. }
  79. ​
  80. seek.style.setProperty('--read-aloud-progress', (seek.value / 10) + '%');
  81. }
  82. ​
  83. function probe() {
  84. if (probed) {
  85. return;
  86. }
  87. probed = true;
  88. ​
  89. tracks.forEach(function (url, trackIndex) {
  90. var probeAudio = new Audio();
  91. probeAudio.preload = 'metadata';
  92. probeAudio.addEventListener('loadedmetadata', function () {
  93. if (isFinite(probeAudio.duration) && probeAudio.duration > 0) {
  94. durations[trackIndex] = probeAudio.duration;
  95. render();
  96. }
  97. });
  98. probeAudio.src = url;
  99. });
  100. }
  101. ​
  102. function load(trackIndex, autoplay, startAt) {
  103. index = trackIndex;
  104. loaded = true;
  105. audio.src = tracks[trackIndex];
  106. audio.defaultPlaybackRate = SPEEDS[speedIndex];
  107. audio.playbackRate = SPEEDS[speedIndex];
  108. ​
  109. if (startAt > 0) {
  110. audio.addEventListener('loadedmetadata', function seekAfterLoad() {
  111. audio.removeEventListener('loadedmetadata', seekAfterLoad);
  112. audio.currentTime = startAt;
  113. });
  114. }
  115. ​
  116. if (autoplay) {
  117. var attempt = audio.play();
  118. if (attempt && typeof attempt.catch === 'function') {
  119. attempt.catch(function () {});
  120. }
  121. }
  122. }
  123. ​
  124. function setPlaying(playing) {
  125. root.classList.toggle('is-playing', playing);
  126. var label = playing ? labelPause : labelPlay;
  127. toggle.setAttribute('aria-label', label);
  128. toggle.setAttribute('title', label);
  129. }
  130. ​
  131. function nextTrack() {
  132. if (index < tracks.length - 1) {
  133. load(index + 1, true, 0);
  134. return;
  135. }
  136. ​
  137. setPlaying(false);
  138. load(0, false, 0);
  139. loaded = false;
  140. seek.value = '0';
  141. render();
  142. }
  143. ​
  144. toggle.addEventListener('click', function () {
  145. probe();
  146. ​
  147. if (!loaded) {
  148. load(0, true, 0);
  149. return;
  150. }
  151. ​
  152. if (audio.paused) {
  153. var attempt = audio.play();
  154. if (attempt && typeof attempt.catch === 'function') {
  155. attempt.catch(function () {});
  156. }
  157. } else {
  158. audio.pause();
  159. }
  160. });
  161. ​
  162. speedButton.addEventListener('click', function () {
  163. speedIndex = (speedIndex + 1) % SPEEDS.length;
  164. audio.defaultPlaybackRate = SPEEDS[speedIndex];
  165. audio.playbackRate = SPEEDS[speedIndex];
  166. speedButton.textContent = SPEEDS[speedIndex] + '×';
  167. });
  168. ​
  169. seek.addEventListener('input', function () {
  170. dragging = true;
  171. render();
  172. });
  173. ​
  174. seek.addEventListener('change', function () {
  175. dragging = false;
  176. if (!known()) {
  177. return;
  178. }
  179. ​
  180. var target = (seek.value / 1000) * total();
  181. var trackIndex = 0;
  182. while (trackIndex < tracks.length - 1 && target >= offset(trackIndex) + durations[trackIndex]) {
  183. trackIndex++;
  184. }
  185. var within = Math.max(0, target - offset(trackIndex));
  186. var wasPlaying = loaded && !audio.paused;
  187. ​
  188. if (loaded && trackIndex === index) {
  189. audio.currentTime = within;
  190. } else {
  191. load(trackIndex, wasPlaying, within);
  192. }
  193. render();
  194. });
  195. ​
  196. audio.addEventListener('play', function () { setPlaying(true); });
  197. audio.addEventListener('pause', function () { setPlaying(false); });
  198. audio.addEventListener('timeupdate', render);
  199. audio.addEventListener('ended', nextTrack);
  200. audio.addEventListener('loadedmetadata', function () {
  201. if (isFinite(audio.duration) && audio.duration > 0) {
  202. durations[index] = audio.duration;
  203. render();
  204. }
  205. });
  206. audio.addEventListener('error', function () {
  207. if (index < tracks.length - 1) {
  208. load(index + 1, true, 0);
  209. } else {
  210. setPlaying(false);
  211. root.classList.add('is-error');
  212. }
  213. });
  214. ​
  215. root.addEventListener('pointerenter', probe, { once: true });
  216. root.addEventListener('focusin', probe, { once: true });
  217. ​
  218. setPlaying(false);
  219. render();
  220. }
  221. ​
  222. document.querySelectorAll('[data-read-aloud]').forEach(init);
  223. })();
  224. ​