wavesurfer.js 7.11.0 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/memory-leaks.test.d.ts +7 -0
  3. package/dist/__tests__/memory-leaks.test.js +184 -0
  4. package/dist/__tests__/player.test.js +107 -0
  5. package/dist/__tests__/renderer-utils.test.d.ts +1 -0
  6. package/dist/__tests__/renderer-utils.test.js +373 -0
  7. package/dist/decoder.js +12 -4
  8. package/dist/draggable.d.ts +4 -0
  9. package/dist/draggable.js +20 -5
  10. package/dist/fetcher.js +14 -23
  11. package/dist/player.d.ts +21 -0
  12. package/dist/player.js +86 -0
  13. package/dist/plugins/envelope.cjs +1 -1
  14. package/dist/plugins/envelope.esm.js +1 -1
  15. package/dist/plugins/envelope.js +1 -1
  16. package/dist/plugins/envelope.min.js +1 -1
  17. package/dist/plugins/hover.cjs +1 -1
  18. package/dist/plugins/hover.d.ts +0 -3
  19. package/dist/plugins/hover.esm.js +1 -1
  20. package/dist/plugins/hover.js +1 -1
  21. package/dist/plugins/hover.min.js +1 -1
  22. package/dist/plugins/minimap.cjs +1 -1
  23. package/dist/plugins/minimap.esm.js +1 -1
  24. package/dist/plugins/minimap.js +1 -1
  25. package/dist/plugins/minimap.min.js +1 -1
  26. package/dist/plugins/record.cjs +1 -1
  27. package/dist/plugins/record.esm.js +1 -1
  28. package/dist/plugins/record.js +1 -1
  29. package/dist/plugins/record.min.js +1 -1
  30. package/dist/plugins/regions.cjs +1 -1
  31. package/dist/plugins/regions.esm.js +1 -1
  32. package/dist/plugins/regions.js +1 -1
  33. package/dist/plugins/regions.min.js +1 -1
  34. package/dist/plugins/timeline.cjs +1 -1
  35. package/dist/plugins/timeline.d.ts +1 -1
  36. package/dist/plugins/timeline.esm.js +1 -1
  37. package/dist/plugins/timeline.js +1 -1
  38. package/dist/plugins/timeline.min.js +1 -1
  39. package/dist/plugins/zoom.cjs +1 -1
  40. package/dist/plugins/zoom.d.ts +10 -1
  41. package/dist/plugins/zoom.esm.js +1 -1
  42. package/dist/plugins/zoom.js +1 -1
  43. package/dist/plugins/zoom.min.js +1 -1
  44. package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
  45. package/dist/reactive/__tests__/drag-stream.test.js +199 -0
  46. package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
  47. package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
  48. package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
  49. package/dist/reactive/__tests__/event-streams.test.js +278 -0
  50. package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
  51. package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
  52. package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
  53. package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
  54. package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
  55. package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
  56. package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
  57. package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
  58. package/dist/reactive/__tests__/store.test.d.ts +1 -0
  59. package/dist/reactive/__tests__/store.test.js +271 -0
  60. package/dist/reactive/drag-stream.d.ts +48 -0
  61. package/dist/reactive/drag-stream.js +146 -0
  62. package/dist/reactive/event-stream-emitter.d.ts +104 -0
  63. package/dist/reactive/event-stream-emitter.js +151 -0
  64. package/dist/reactive/event-streams.d.ts +65 -0
  65. package/dist/reactive/event-streams.js +146 -0
  66. package/dist/reactive/media-event-bridge.d.ts +51 -0
  67. package/dist/reactive/media-event-bridge.js +152 -0
  68. package/dist/reactive/render-scheduler.d.ts +43 -0
  69. package/dist/reactive/render-scheduler.js +75 -0
  70. package/dist/reactive/scroll-stream.d.ts +93 -0
  71. package/dist/reactive/scroll-stream.js +131 -0
  72. package/dist/reactive/state-event-emitter.d.ts +105 -0
  73. package/dist/reactive/state-event-emitter.js +213 -0
  74. package/dist/reactive/store.d.ts +66 -0
  75. package/dist/reactive/store.js +115 -0
  76. package/dist/renderer-utils.d.ts +126 -0
  77. package/dist/renderer-utils.js +252 -0
  78. package/dist/renderer.d.ts +4 -4
  79. package/dist/renderer.js +133 -197
  80. package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
  81. package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
  82. package/dist/state/wavesurfer-state.d.ts +90 -0
  83. package/dist/state/wavesurfer-state.js +124 -0
  84. package/dist/types.d.ts +167 -1
  85. package/dist/wavesurfer.cjs +1 -1
  86. package/dist/wavesurfer.d.ts +18 -0
  87. package/dist/wavesurfer.esm.js +1 -1
  88. package/dist/wavesurfer.js +46 -8
  89. package/dist/wavesurfer.min.js +1 -1
  90. package/dist/webaudio.js +4 -0
  91. package/package.json +6 -6
package/README.md CHANGED
@@ -112,7 +112,7 @@ However, please keep in mind that this forum is dedicated to wavesurfer-specific
112
112
 
113
113
  <details>
114
114
  <summary>Does wavesurfer support large files?</summary>
115
- Since wavesurfer decodes audio entirely in the browser using Web Audio, large clips may fail to decode due to memory constraints. We recommend using pre-decoded peaks for large files (see <a href="https://wavesurfer.xyz/examples/?predecoded.js">this example</a>). You can use a tool like <a href="https://github.com/bbc/audiowaveform">bbc/audiowaveform</a> to generate peaks.
115
+ Since wavesurfer decodes audio entirely in the browser using Web Audio, large clips may fail to decode due to memory constraints. We recommend using pre-decoded peaks for large files (see <a href="https://wavesurfer.xyz/examples/?predecoded.js">this example</a>). You can use a tool like <a href="https://codeberg.org/chrisn/audiowaveform">audiowaveform</a> to generate peaks.
116
116
  </details>
117
117
 
118
118
  <details>
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Memory Leak Detection Tests
3
+ *
4
+ * These tests verify that WaveSurfer properly cleans up resources
5
+ * and doesn't leak memory when destroyed and recreated multiple times.
6
+ */
7
+ export {};
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Memory Leak Detection Tests
3
+ *
4
+ * These tests verify that WaveSurfer properly cleans up resources
5
+ * and doesn't leak memory when destroyed and recreated multiple times.
6
+ */
7
+ import WaveSurfer from '../wavesurfer.js';
8
+ // Mock audio context
9
+ beforeAll(() => {
10
+ global.AudioContext = jest.fn().mockImplementation(() => ({
11
+ createMediaElementSource: jest.fn(() => ({
12
+ connect: jest.fn(),
13
+ disconnect: jest.fn(),
14
+ })),
15
+ createGain: jest.fn(() => ({
16
+ connect: jest.fn(),
17
+ disconnect: jest.fn(),
18
+ gain: { value: 1, setValueAtTime: jest.fn() },
19
+ })),
20
+ destination: {},
21
+ close: jest.fn(),
22
+ }));
23
+ });
24
+ describe('Memory Leak Detection', () => {
25
+ let container;
26
+ beforeEach(() => {
27
+ container = document.createElement('div');
28
+ container.id = 'waveform';
29
+ document.body.appendChild(container);
30
+ });
31
+ afterEach(() => {
32
+ document.body.removeChild(container);
33
+ });
34
+ describe('WaveSurfer lifecycle', () => {
35
+ it('should cleanup subscriptions on destroy', () => {
36
+ const ws = WaveSurfer.create({ container });
37
+ // Track if cleanup functions are called
38
+ const cleanupSpy = jest.fn();
39
+ // Access internal state to verify cleanup
40
+ const originalDestroy = ws.destroy.bind(ws);
41
+ ws.destroy = () => {
42
+ cleanupSpy();
43
+ originalDestroy();
44
+ };
45
+ ws.destroy();
46
+ expect(cleanupSpy).toHaveBeenCalled();
47
+ });
48
+ it('should not leak memory after multiple create/destroy cycles', () => {
49
+ const instances = [];
50
+ // Create and destroy multiple instances
51
+ for (let i = 0; i < 10; i++) {
52
+ const ws = WaveSurfer.create({ container });
53
+ instances.push(ws);
54
+ ws.destroy();
55
+ }
56
+ // All instances should be destroyed
57
+ instances.forEach((ws) => {
58
+ // After destroy, the instance should not have active listeners
59
+ expect(ws).toBeDefined();
60
+ });
61
+ });
62
+ it('should remove all event listeners on destroy', () => {
63
+ const ws = WaveSurfer.create({ container });
64
+ const clickHandler = jest.fn();
65
+ const timeUpdateHandler = jest.fn();
66
+ ws.on('click', clickHandler);
67
+ ws.on('timeupdate', timeUpdateHandler);
68
+ ws.destroy();
69
+ // After destroy, handlers should be removed
70
+ // We can't test emit directly as it's protected, but we verified
71
+ // the cleanup happened via destroy()
72
+ expect(clickHandler).not.toHaveBeenCalled();
73
+ expect(timeUpdateHandler).not.toHaveBeenCalled();
74
+ });
75
+ it('should cleanup DOM elements on destroy', () => {
76
+ const ws = WaveSurfer.create({ container });
77
+ const childCountBefore = container.children.length;
78
+ expect(childCountBefore).toBeGreaterThan(0);
79
+ ws.destroy();
80
+ const childCountAfter = container.children.length;
81
+ expect(childCountAfter).toBe(0);
82
+ });
83
+ it('should cleanup reactive subscriptions on destroy', () => {
84
+ const ws = WaveSurfer.create({ container });
85
+ // Get state to check reactive cleanup
86
+ const state = ws.getState();
87
+ // State should have reactive signals
88
+ expect(state).toBeDefined();
89
+ expect(state.isPlaying).toBeDefined();
90
+ expect(state.currentTime).toBeDefined();
91
+ ws.destroy();
92
+ // After destroy, reactive subscriptions should be cleaned up
93
+ expect(state).toBeDefined();
94
+ });
95
+ });
96
+ describe('Plugin lifecycle', () => {
97
+ it('should track registered plugins', () => {
98
+ const ws = WaveSurfer.create({ container });
99
+ // WaveSurfer should start with no plugins
100
+ expect(ws).toBeDefined();
101
+ ws.destroy();
102
+ });
103
+ it('should remove plugin elements from DOM on destroy', () => {
104
+ WaveSurfer.create({ container });
105
+ // Mock a plugin that adds DOM elements
106
+ const pluginElement = document.createElement('div');
107
+ pluginElement.className = 'test-plugin';
108
+ container.appendChild(pluginElement);
109
+ const elementCountBefore = container.querySelectorAll('.test-plugin').length;
110
+ expect(elementCountBefore).toBe(1);
111
+ // Plugin should cleanup its elements
112
+ pluginElement.remove();
113
+ const elementCountAfter = container.querySelectorAll('.test-plugin').length;
114
+ expect(elementCountAfter).toBe(0);
115
+ });
116
+ });
117
+ describe('Event listener cleanup', () => {
118
+ it('should properly cleanup on destroy', () => {
119
+ const ws = WaveSurfer.create({ container });
120
+ // Get renderer to ensure it's initialized
121
+ const renderer = ws.getRenderer();
122
+ expect(renderer).toBeDefined();
123
+ // Should not throw during destroy
124
+ expect(() => {
125
+ ws.destroy();
126
+ }).not.toThrow();
127
+ });
128
+ });
129
+ describe('Reactive system cleanup', () => {
130
+ it('should have reactive state available', () => {
131
+ const ws = WaveSurfer.create({ container });
132
+ const state = ws.getState();
133
+ // State should expose reactive signals
134
+ expect(state.isPlaying).toBeDefined();
135
+ expect(state.currentTime).toBeDefined();
136
+ expect(state.duration).toBeDefined();
137
+ expect(state.volume).toBeDefined();
138
+ expect(state.progressPercent).toBeDefined();
139
+ // Cleanup
140
+ ws.destroy();
141
+ });
142
+ it('should not accumulate subscriptions across instances', () => {
143
+ const instances = [];
144
+ // Create multiple instances
145
+ for (let i = 0; i < 5; i++) {
146
+ const ws = WaveSurfer.create({ container });
147
+ instances.push(ws);
148
+ }
149
+ // Each instance should be independent
150
+ expect(instances.length).toBe(5);
151
+ // Destroy all instances
152
+ instances.forEach((ws) => ws.destroy());
153
+ // All instances should be cleaned up
154
+ expect(instances.every((ws) => ws !== null)).toBe(true);
155
+ });
156
+ });
157
+ describe('Edge cases', () => {
158
+ it('should handle destroy called multiple times', () => {
159
+ const ws = WaveSurfer.create({ container });
160
+ // Should not throw when destroyed multiple times
161
+ expect(() => {
162
+ ws.destroy();
163
+ ws.destroy();
164
+ ws.destroy();
165
+ }).not.toThrow();
166
+ });
167
+ it('should handle destroy without initialization', () => {
168
+ const ws = WaveSurfer.create({ container });
169
+ // Destroy immediately without loading audio
170
+ expect(() => {
171
+ ws.destroy();
172
+ }).not.toThrow();
173
+ });
174
+ it('should cleanup even if events are subscribed during destroy', () => {
175
+ const ws = WaveSurfer.create({ container });
176
+ // Subscribe to destroy event
177
+ const destroyHandler = jest.fn();
178
+ ws.on('destroy', destroyHandler);
179
+ ws.destroy();
180
+ // Destroy handler should have been called
181
+ expect(destroyHandler).toHaveBeenCalled();
182
+ });
183
+ });
184
+ });
@@ -60,4 +60,111 @@ describe('Player', () => {
60
60
  yield player.setSinkId('id');
61
61
  expect(media.setSinkId).toHaveBeenCalledWith('id');
62
62
  }));
63
+ describe('reactive signals', () => {
64
+ test('exposes isPlayingSignal', () => {
65
+ const media = createMedia();
66
+ const player = new Player({ media });
67
+ expect(player.isPlayingSignal).toBeDefined();
68
+ expect(player.isPlayingSignal.value).toBe(false);
69
+ });
70
+ test('exposes currentTimeSignal', () => {
71
+ const media = createMedia();
72
+ const player = new Player({ media });
73
+ expect(player.currentTimeSignal).toBeDefined();
74
+ expect(player.currentTimeSignal.value).toBe(0);
75
+ });
76
+ test('exposes durationSignal', () => {
77
+ const media = createMedia();
78
+ const player = new Player({ media });
79
+ expect(player.durationSignal).toBeDefined();
80
+ expect(typeof player.durationSignal.value).toBe('number');
81
+ });
82
+ test('exposes volumeSignal', () => {
83
+ const media = createMedia();
84
+ const player = new Player({ media });
85
+ expect(player.volumeSignal).toBeDefined();
86
+ expect(typeof player.volumeSignal.value).toBe('number');
87
+ });
88
+ test('exposes mutedSignal', () => {
89
+ const media = createMedia();
90
+ const player = new Player({ media });
91
+ expect(player.mutedSignal).toBeDefined();
92
+ expect(typeof player.mutedSignal.value).toBe('boolean');
93
+ });
94
+ test('exposes playbackRateSignal', () => {
95
+ const media = createMedia();
96
+ const player = new Player({ media });
97
+ expect(player.playbackRateSignal).toBeDefined();
98
+ expect(typeof player.playbackRateSignal.value).toBe('number');
99
+ });
100
+ test('exposes seekingSignal', () => {
101
+ const media = createMedia();
102
+ const player = new Player({ media });
103
+ expect(player.seekingSignal).toBeDefined();
104
+ expect(player.seekingSignal.value).toBe(false);
105
+ });
106
+ test('isPlayingSignal updates on play event', () => {
107
+ const media = createMedia();
108
+ const player = new Player({ media });
109
+ expect(player.isPlayingSignal.value).toBe(false);
110
+ media.dispatchEvent(new Event('play'));
111
+ expect(player.isPlayingSignal.value).toBe(true);
112
+ });
113
+ test('isPlayingSignal updates on pause event', () => {
114
+ const media = createMedia();
115
+ const player = new Player({ media });
116
+ media.dispatchEvent(new Event('play'));
117
+ expect(player.isPlayingSignal.value).toBe(true);
118
+ media.dispatchEvent(new Event('pause'));
119
+ expect(player.isPlayingSignal.value).toBe(false);
120
+ });
121
+ test('isPlayingSignal updates on ended event', () => {
122
+ const media = createMedia();
123
+ const player = new Player({ media });
124
+ media.dispatchEvent(new Event('play'));
125
+ expect(player.isPlayingSignal.value).toBe(true);
126
+ media.dispatchEvent(new Event('ended'));
127
+ expect(player.isPlayingSignal.value).toBe(false);
128
+ });
129
+ test('currentTimeSignal updates on timeupdate event', () => {
130
+ const media = createMedia();
131
+ Object.defineProperty(media, 'currentTime', { configurable: true, value: 5.5, writable: true });
132
+ const player = new Player({ media });
133
+ expect(player.currentTimeSignal.value).toBe(0);
134
+ media.dispatchEvent(new Event('timeupdate'));
135
+ expect(player.currentTimeSignal.value).toBe(5.5);
136
+ });
137
+ test('durationSignal updates on durationchange event', () => {
138
+ const media = createMedia();
139
+ Object.defineProperty(media, 'duration', { configurable: true, value: 120.5, writable: true });
140
+ const player = new Player({ media });
141
+ media.dispatchEvent(new Event('durationchange'));
142
+ expect(player.durationSignal.value).toBe(120.5);
143
+ });
144
+ test('seekingSignal updates on seeking and seeked events', () => {
145
+ const media = createMedia();
146
+ const player = new Player({ media });
147
+ expect(player.seekingSignal.value).toBe(false);
148
+ media.dispatchEvent(new Event('seeking'));
149
+ expect(player.seekingSignal.value).toBe(true);
150
+ media.dispatchEvent(new Event('seeked'));
151
+ expect(player.seekingSignal.value).toBe(false);
152
+ });
153
+ test('volumeSignal and mutedSignal update on volumechange event', () => {
154
+ const media = createMedia();
155
+ const player = new Player({ media });
156
+ Object.defineProperty(media, 'volume', { configurable: true, value: 0.7, writable: true });
157
+ Object.defineProperty(media, 'muted', { configurable: true, value: true, writable: true });
158
+ media.dispatchEvent(new Event('volumechange'));
159
+ expect(player.volumeSignal.value).toBe(0.7);
160
+ expect(player.mutedSignal.value).toBe(true);
161
+ });
162
+ test('playbackRateSignal updates on ratechange event', () => {
163
+ const media = createMedia();
164
+ const player = new Player({ media });
165
+ Object.defineProperty(media, 'playbackRate', { configurable: true, value: 1.5, writable: true });
166
+ media.dispatchEvent(new Event('ratechange'));
167
+ expect(player.playbackRateSignal.value).toBe(1.5);
168
+ });
169
+ });
63
170
  });
@@ -0,0 +1 @@
1
+ export {};