minimap.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /**
  2. * @typedef {Object} MinimapPluginParams
  3. * @desc Extends the `WavesurferParams` wavesurfer was initialised with
  4. * @property {?string|HTMLElement} container CSS selector or HTML element where
  5. * the ELAN information should be renderer. By default it is simply appended
  6. * after the waveform.
  7. * @property {?boolean} deferInit Set to true to manually call
  8. * `initPlugin('minimap')`
  9. */
  10. /**
  11. * Renders a smaller version waveform as a minimap of the main waveform.
  12. *
  13. * @implements {PluginClass}
  14. * @extends {Observer}
  15. * @example
  16. * // es6
  17. * import MinimapPlugin from 'wavesurfer.minimap.js';
  18. *
  19. * // commonjs
  20. * var MinimapPlugin = require('wavesurfer.minimap.js');
  21. *
  22. * // if you are using <script> tags
  23. * var MinimapPlugin = window.WaveSurfer.minimap;
  24. *
  25. * // ... initialising wavesurfer with the plugin
  26. * var wavesurfer = WaveSurfer.create({
  27. * // wavesurfer options ...
  28. * plugins: [
  29. * MinimapPlugin.create({
  30. * // plugin options ...
  31. * })
  32. * ]
  33. * });
  34. */
  35. export default class MinimapPlugin {
  36. /**
  37. * Minimap plugin definition factory
  38. *
  39. * This function must be used to create a plugin definition which can be
  40. * used by wavesurfer to correctly instantiate the plugin.
  41. *
  42. * @param {MinimapPluginParams} params parameters use to initialise the plugin
  43. * @return {PluginDefinition} an object representing the plugin
  44. */
  45. static create(params) {
  46. return {
  47. name: 'minimap',
  48. deferInit: params && params.deferInit ? params.deferInit : false,
  49. params: params,
  50. staticProps: {
  51. initMinimap(customConfig) {
  52. console.warn('Deprecated initMinimap!');
  53. params = customConfig;
  54. this.initPlugins('minimap');
  55. }
  56. },
  57. instance: MinimapPlugin
  58. };
  59. }
  60. constructor(params, ws) {
  61. this.params = ws.util.extend(
  62. {}, ws.params, {
  63. showRegions: false,
  64. showOverview: false,
  65. overviewBorderColor: 'green',
  66. overviewBorderSize: 2,
  67. // the container should be different
  68. container: false,
  69. height: Math.max(Math.round(ws.params.height / 4), 20)
  70. }, params, {
  71. scrollParent: false,
  72. fillParent: true
  73. }
  74. );
  75. // if container is a selector, get the element
  76. if (typeof params.container === 'string') {
  77. const el = document.querySelector(params.container);
  78. if (!el) {
  79. console.warn(`Wavesurfer minimap container ${params.container} was not found! The minimap will be automatically appended below the waveform.`);
  80. }
  81. this.params.container = el;
  82. }
  83. // if no container is specified add a new element and insert it
  84. if (!params.container) {
  85. this.params.container = ws.util.style(document.createElement('minimap'), {
  86. display: 'block'
  87. });
  88. }
  89. this.drawer = new (ws.Drawer)(this.params.container, this.params);
  90. this.wavesurfer = ws;
  91. this.util = ws.util;
  92. /**
  93. * Minimap needs to register to ready and waveform-ready events to
  94. * work with MediaElement, the time when ready is called is different
  95. * (peaks can not be got)
  96. *
  97. * @type {string}
  98. * @see https://github.com/katspaugh/wavesurfer.js/issues/736
  99. */
  100. this.renderEvent = ws.params.backend === 'MediaElement' ? 'waveform-ready' : 'ready';
  101. this.overviewRegion = null;
  102. this.drawer.createWrapper();
  103. this.createElements();
  104. let isInitialised = false;
  105. // ws ready event listener
  106. this._onShouldRender = () => {
  107. // only bind the events in the first run
  108. if (!isInitialised) {
  109. this.bindWavesurferEvents();
  110. this.bindMinimapEvents();
  111. isInitialised = true;
  112. }
  113. // if there is no such element, append it to the container (below
  114. // the waveform)
  115. if (!document.body.contains(this.params.container)) {
  116. ws.container.insertBefore(this.params.container, null);
  117. }
  118. if (this.wavesurfer.regions && this.params.showRegions) {
  119. this.regions();
  120. }
  121. this.render();
  122. };
  123. this._onAudioprocess = currentTime => {
  124. this.drawer.progress(this.wavesurfer.backend.getPlayedPercents());
  125. };
  126. // ws seek event listener
  127. this._onSeek = () => this.drawer.progress(ws.backend.getPlayedPercents());
  128. // event listeners for the overview region
  129. this._onScroll = e => {
  130. if (!this.draggingOverview) {
  131. this.moveOverviewRegion(e.target.scrollLeft / this.ratio);
  132. }
  133. };
  134. this._onMouseover = e => {
  135. if (this.draggingOverview) {
  136. this.draggingOverview = false;
  137. }
  138. };
  139. let prevWidth = 0;
  140. this._onResize = ws.util.debounce(() => {
  141. if (prevWidth != this.drawer.wrapper.clientWidth) {
  142. prevWidth = this.drawer.wrapper.clientWidth;
  143. this.render();
  144. this.drawer.progress(this.wavesurfer.backend.getPlayedPercents());
  145. }
  146. });
  147. }
  148. init() {
  149. if (this.wavesurfer.isReady) {
  150. this._onShouldRender();
  151. }
  152. this.wavesurfer.on(this.renderEvent, this._onShouldRender);
  153. }
  154. destroy() {
  155. window.removeEventListener('resize', this._onResize, true);
  156. window.removeEventListener('orientationchange', this._onResize, true);
  157. this.wavesurfer.drawer.wrapper.removeEventListener('mouseover', this._onMouseover);
  158. this.wavesurfer.un(this.renderEvent, this._onShouldRender);
  159. this.wavesurfer.un('seek', this._onSeek);
  160. this.wavesurfer.un('scroll', this._onScroll);
  161. this.wavesurfer.un('audioprocess', this._onAudioprocess);
  162. this.drawer.destroy();
  163. this.overviewRegion = null;
  164. this.unAll();
  165. }
  166. regions() {
  167. this.regions = {};
  168. this.wavesurfer.on('region-created', region => {
  169. this.regions[region.id] = region;
  170. this.renderRegions();
  171. });
  172. this.wavesurfer.on('region-updated', region => {
  173. this.regions[region.id] = region;
  174. this.renderRegions();
  175. });
  176. this.wavesurfer.on('region-removed', region => {
  177. delete this.regions[region.id];
  178. this.renderRegions();
  179. });
  180. }
  181. renderRegions() {
  182. const regionElements = this.drawer.wrapper.querySelectorAll('region');
  183. let i;
  184. for (i = 0; i < regionElements.length; ++i) {
  185. this.drawer.wrapper.removeChild(regionElements[i]);
  186. }
  187. Object.keys(this.regions).forEach(id => {
  188. const region = this.regions[id];
  189. const width = (this.drawer.width * ((region.end - region.start) / this.wavesurfer.getDuration()));
  190. const left = (this.drawer.width * (region.start / this.wavesurfer.getDuration()));
  191. const regionElement = this.util.style(document.createElement('region'), {
  192. height: 'inherit',
  193. backgroundColor: region.color,
  194. width: width + 'px',
  195. left: left + 'px',
  196. display: 'block',
  197. position: 'absolute'
  198. });
  199. regionElement.classList.add(id);
  200. this.drawer.wrapper.appendChild(regionElement);
  201. });
  202. }
  203. createElements() {
  204. this.drawer.createElements();
  205. if (this.params.showOverview) {
  206. this.overviewRegion = this.util.style(document.createElement('overview'), {
  207. height: (this.drawer.wrapper.offsetHeight - (this.params.overviewBorderSize * 2)) + 'px',
  208. width: '0px',
  209. display: 'block',
  210. position: 'absolute',
  211. cursor: 'move',
  212. border: this.params.overviewBorderSize + 'px solid ' + this.params.overviewBorderColor,
  213. zIndex: 2,
  214. opacity: this.params.overviewOpacity
  215. });
  216. this.drawer.wrapper.appendChild(this.overviewRegion);
  217. }
  218. }
  219. bindWavesurferEvents() {
  220. window.addEventListener('resize', this._onResize, true);
  221. window.addEventListener('orientationchange', this._onResize, true);
  222. this.wavesurfer.on('audioprocess', this._onAudioprocess);
  223. this.wavesurfer.on('seek', this._onSeek);
  224. if (this.params.showOverview) {
  225. this.wavesurfer.on('scroll', this._onScroll);
  226. this.wavesurfer.drawer.wrapper.addEventListener('mouseover', this._onMouseover);
  227. }
  228. }
  229. bindMinimapEvents() {
  230. const positionMouseDown = {
  231. clientX: 0,
  232. clientY: 0
  233. };
  234. let relativePositionX = 0;
  235. let seek = true;
  236. // the following event listeners will be destroyed by using
  237. // this.unAll() and nullifying the DOM node references after
  238. // removing them
  239. this.on('click', (e, position) => {
  240. if (seek) {
  241. this.progress(position);
  242. this.wavesurfer.seekAndCenter(position);
  243. } else {
  244. seek = true;
  245. }
  246. });
  247. if (this.params.showOverview) {
  248. this.overviewRegion.addEventListener('mousedown', event => {
  249. this.draggingOverview = true;
  250. relativePositionX = event.layerX;
  251. positionMouseDown.clientX = event.clientX;
  252. positionMouseDown.clientY = event.clientY;
  253. });
  254. this.drawer.wrapper.addEventListener('mousemove', event => {
  255. if (this.draggingOverview) {
  256. this.moveOverviewRegion(event.clientX - this.drawer.container.getBoundingClientRect().left - relativePositionX);
  257. }
  258. });
  259. this.drawer.wrapper.addEventListener('mouseup', event => {
  260. if (positionMouseDown.clientX - event.clientX === 0 && positionMouseDown.clientX - event.clientX === 0) {
  261. seek = true;
  262. this.draggingOverview = false;
  263. } else if (this.draggingOverview) {
  264. seek = false;
  265. this.draggingOverview = false;
  266. }
  267. });
  268. }
  269. }
  270. render() {
  271. const len = this.drawer.getWidth();
  272. const peaks = this.wavesurfer.backend.getPeaks(len, 0, len);
  273. this.drawer.drawPeaks(peaks, len, 0, len);
  274. this.drawer.progress(this.wavesurfer.backend.getPlayedPercents());
  275. if (this.params.showOverview) {
  276. //get proportional width of overview region considering the respective
  277. //width of the drawers
  278. this.ratio = this.wavesurfer.drawer.width / this.drawer.width;
  279. this.waveShowedWidth = this.wavesurfer.drawer.width / this.ratio;
  280. this.waveWidth = this.wavesurfer.drawer.width;
  281. this.overviewWidth = (this.drawer.width / this.ratio);
  282. this.overviewPosition = 0;
  283. this.moveOverviewRegion(this.wavesurfer.drawer.wrapper.scrollLeft / this.ratio);
  284. this.overviewRegion.style.width = (this.overviewWidth - (this.params.overviewBorderSize * 2)) + 'px';
  285. }
  286. }
  287. moveOverviewRegion(pixels) {
  288. if (pixels < 0) {
  289. this.overviewPosition = 0;
  290. } else if (pixels + this.overviewWidth < this.drawer.width) {
  291. this.overviewPosition = pixels;
  292. } else {
  293. this.overviewPosition = (this.drawer.width - this.overviewWidth);
  294. }
  295. this.overviewRegion.style.left = this.overviewPosition + 'px';
  296. if (this.draggingOverview) {
  297. this.wavesurfer.drawer.wrapper.scrollLeft = this.overviewPosition * this.ratio;
  298. }
  299. }
  300. }