wavesurfer.js 7.11.1 → 7.12.1
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.
- package/README.md +1 -1
- package/dist/__tests__/memory-leaks.test.d.ts +7 -0
- package/dist/__tests__/memory-leaks.test.js +281 -0
- package/dist/__tests__/player.test.js +107 -0
- package/dist/__tests__/renderer-utils.test.js +55 -2
- package/dist/draggable.d.ts +4 -0
- package/dist/draggable.js +4 -0
- package/dist/player.d.ts +21 -0
- package/dist/player.js +86 -0
- package/dist/plugins/envelope.cjs +1 -1
- package/dist/plugins/envelope.esm.js +1 -1
- package/dist/plugins/envelope.js +1 -1
- package/dist/plugins/envelope.min.js +1 -1
- package/dist/plugins/hover.cjs +1 -1
- package/dist/plugins/hover.d.ts +0 -3
- package/dist/plugins/hover.esm.js +1 -1
- package/dist/plugins/hover.js +1 -1
- package/dist/plugins/hover.min.js +1 -1
- package/dist/plugins/minimap.cjs +1 -1
- package/dist/plugins/minimap.esm.js +1 -1
- package/dist/plugins/minimap.js +1 -1
- package/dist/plugins/minimap.min.js +1 -1
- package/dist/plugins/record.cjs +1 -1
- package/dist/plugins/record.esm.js +1 -1
- package/dist/plugins/record.js +1 -1
- package/dist/plugins/record.min.js +1 -1
- package/dist/plugins/regions.cjs +1 -1
- package/dist/plugins/regions.esm.js +1 -1
- package/dist/plugins/regions.js +1 -1
- package/dist/plugins/regions.min.js +1 -1
- package/dist/plugins/timeline.cjs +1 -1
- package/dist/plugins/timeline.d.ts +1 -1
- package/dist/plugins/timeline.esm.js +1 -1
- package/dist/plugins/timeline.js +1 -1
- package/dist/plugins/timeline.min.js +1 -1
- package/dist/plugins/zoom.cjs +1 -1
- package/dist/plugins/zoom.esm.js +1 -1
- package/dist/plugins/zoom.js +1 -1
- package/dist/plugins/zoom.min.js +1 -1
- package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
- package/dist/reactive/__tests__/drag-stream.test.js +199 -0
- package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
- package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
- package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
- package/dist/reactive/__tests__/event-streams.test.js +278 -0
- package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
- package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
- package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
- package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
- package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
- package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
- package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
- package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
- package/dist/reactive/__tests__/store.test.d.ts +1 -0
- package/dist/reactive/__tests__/store.test.js +271 -0
- package/dist/reactive/drag-stream.d.ts +48 -0
- package/dist/reactive/drag-stream.js +146 -0
- package/dist/reactive/event-stream-emitter.d.ts +104 -0
- package/dist/reactive/event-stream-emitter.js +151 -0
- package/dist/reactive/event-streams.d.ts +65 -0
- package/dist/reactive/event-streams.js +146 -0
- package/dist/reactive/media-event-bridge.d.ts +51 -0
- package/dist/reactive/media-event-bridge.js +152 -0
- package/dist/reactive/render-scheduler.d.ts +43 -0
- package/dist/reactive/render-scheduler.js +75 -0
- package/dist/reactive/scroll-stream.d.ts +93 -0
- package/dist/reactive/scroll-stream.js +131 -0
- package/dist/reactive/state-event-emitter.d.ts +105 -0
- package/dist/reactive/state-event-emitter.js +213 -0
- package/dist/reactive/store.d.ts +66 -0
- package/dist/reactive/store.js +115 -0
- package/dist/renderer-utils.d.ts +13 -4
- package/dist/renderer-utils.js +35 -15
- package/dist/renderer.d.ts +2 -1
- package/dist/renderer.js +51 -48
- package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
- package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
- package/dist/state/wavesurfer-state.d.ts +90 -0
- package/dist/state/wavesurfer-state.js +124 -0
- package/dist/types.d.ts +165 -1
- package/dist/wavesurfer.cjs +1 -1
- package/dist/wavesurfer.d.ts +16 -0
- package/dist/wavesurfer.esm.js +1 -1
- package/dist/wavesurfer.js +32 -0
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +2 -3
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://
|
|
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,281 @@
|
|
|
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
|
+
import RegionsPlugin from '../plugins/regions.js';
|
|
9
|
+
// Mock audio context and matchMedia
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
global.AudioContext = jest.fn().mockImplementation(() => ({
|
|
12
|
+
createMediaElementSource: jest.fn(() => ({
|
|
13
|
+
connect: jest.fn(),
|
|
14
|
+
disconnect: jest.fn(),
|
|
15
|
+
})),
|
|
16
|
+
createGain: jest.fn(() => ({
|
|
17
|
+
connect: jest.fn(),
|
|
18
|
+
disconnect: jest.fn(),
|
|
19
|
+
gain: { value: 1, setValueAtTime: jest.fn() },
|
|
20
|
+
})),
|
|
21
|
+
destination: {},
|
|
22
|
+
close: jest.fn(),
|
|
23
|
+
}));
|
|
24
|
+
// Mock matchMedia for drag-stream
|
|
25
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
26
|
+
writable: true,
|
|
27
|
+
value: jest.fn().mockImplementation((query) => ({
|
|
28
|
+
matches: false,
|
|
29
|
+
media: query,
|
|
30
|
+
onchange: null,
|
|
31
|
+
addListener: jest.fn(),
|
|
32
|
+
removeListener: jest.fn(),
|
|
33
|
+
addEventListener: jest.fn(),
|
|
34
|
+
removeEventListener: jest.fn(),
|
|
35
|
+
dispatchEvent: jest.fn(),
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('Memory Leak Detection', () => {
|
|
40
|
+
let container;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
container = document.createElement('div');
|
|
43
|
+
container.id = 'waveform';
|
|
44
|
+
document.body.appendChild(container);
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
document.body.removeChild(container);
|
|
48
|
+
});
|
|
49
|
+
describe('WaveSurfer lifecycle', () => {
|
|
50
|
+
it('should cleanup subscriptions on destroy', () => {
|
|
51
|
+
const ws = WaveSurfer.create({ container });
|
|
52
|
+
// Track if cleanup functions are called
|
|
53
|
+
const cleanupSpy = jest.fn();
|
|
54
|
+
// Access internal state to verify cleanup
|
|
55
|
+
const originalDestroy = ws.destroy.bind(ws);
|
|
56
|
+
ws.destroy = () => {
|
|
57
|
+
cleanupSpy();
|
|
58
|
+
originalDestroy();
|
|
59
|
+
};
|
|
60
|
+
ws.destroy();
|
|
61
|
+
expect(cleanupSpy).toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
it('should not leak memory after multiple create/destroy cycles', () => {
|
|
64
|
+
const instances = [];
|
|
65
|
+
// Create and destroy multiple instances
|
|
66
|
+
for (let i = 0; i < 10; i++) {
|
|
67
|
+
const ws = WaveSurfer.create({ container });
|
|
68
|
+
instances.push(ws);
|
|
69
|
+
ws.destroy();
|
|
70
|
+
}
|
|
71
|
+
// All instances should be destroyed
|
|
72
|
+
instances.forEach((ws) => {
|
|
73
|
+
// After destroy, the instance should not have active listeners
|
|
74
|
+
expect(ws).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
it('should remove all event listeners on destroy', () => {
|
|
78
|
+
const ws = WaveSurfer.create({ container });
|
|
79
|
+
const clickHandler = jest.fn();
|
|
80
|
+
const timeUpdateHandler = jest.fn();
|
|
81
|
+
ws.on('click', clickHandler);
|
|
82
|
+
ws.on('timeupdate', timeUpdateHandler);
|
|
83
|
+
ws.destroy();
|
|
84
|
+
// After destroy, handlers should be removed
|
|
85
|
+
// We can't test emit directly as it's protected, but we verified
|
|
86
|
+
// the cleanup happened via destroy()
|
|
87
|
+
expect(clickHandler).not.toHaveBeenCalled();
|
|
88
|
+
expect(timeUpdateHandler).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
it('should cleanup DOM elements on destroy', () => {
|
|
91
|
+
const ws = WaveSurfer.create({ container });
|
|
92
|
+
const childCountBefore = container.children.length;
|
|
93
|
+
expect(childCountBefore).toBeGreaterThan(0);
|
|
94
|
+
ws.destroy();
|
|
95
|
+
const childCountAfter = container.children.length;
|
|
96
|
+
expect(childCountAfter).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
it('should cleanup reactive subscriptions on destroy', () => {
|
|
99
|
+
const ws = WaveSurfer.create({ container });
|
|
100
|
+
// Get state to check reactive cleanup
|
|
101
|
+
const state = ws.getState();
|
|
102
|
+
// State should have reactive signals
|
|
103
|
+
expect(state).toBeDefined();
|
|
104
|
+
expect(state.isPlaying).toBeDefined();
|
|
105
|
+
expect(state.currentTime).toBeDefined();
|
|
106
|
+
ws.destroy();
|
|
107
|
+
// After destroy, reactive subscriptions should be cleaned up
|
|
108
|
+
expect(state).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('Plugin lifecycle', () => {
|
|
112
|
+
it('should track registered plugins', () => {
|
|
113
|
+
const ws = WaveSurfer.create({ container });
|
|
114
|
+
// WaveSurfer should start with no plugins
|
|
115
|
+
expect(ws).toBeDefined();
|
|
116
|
+
ws.destroy();
|
|
117
|
+
});
|
|
118
|
+
it('should remove plugin elements from DOM on destroy', () => {
|
|
119
|
+
WaveSurfer.create({ container });
|
|
120
|
+
// Mock a plugin that adds DOM elements
|
|
121
|
+
const pluginElement = document.createElement('div');
|
|
122
|
+
pluginElement.className = 'test-plugin';
|
|
123
|
+
container.appendChild(pluginElement);
|
|
124
|
+
const elementCountBefore = container.querySelectorAll('.test-plugin').length;
|
|
125
|
+
expect(elementCountBefore).toBe(1);
|
|
126
|
+
// Plugin should cleanup its elements
|
|
127
|
+
pluginElement.remove();
|
|
128
|
+
const elementCountAfter = container.querySelectorAll('.test-plugin').length;
|
|
129
|
+
expect(elementCountAfter).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('Regions plugin memory leak (#4243)', () => {
|
|
133
|
+
it('should cleanup region event listeners when removed', () => {
|
|
134
|
+
const ws = WaveSurfer.create({ container });
|
|
135
|
+
const regions = ws.registerPlugin(RegionsPlugin.create());
|
|
136
|
+
// Mock duration so regions are saved immediately
|
|
137
|
+
jest.spyOn(ws, 'getDuration').mockReturnValue(10);
|
|
138
|
+
jest.spyOn(ws, 'getDecodedData').mockReturnValue({ numberOfChannels: 1 });
|
|
139
|
+
// Create a region
|
|
140
|
+
const region = regions.addRegion({ start: 0, end: 1 });
|
|
141
|
+
// Track if cleanup is happening
|
|
142
|
+
const clickHandler = jest.fn();
|
|
143
|
+
region.on('click', clickHandler);
|
|
144
|
+
// Remove the region
|
|
145
|
+
region.remove();
|
|
146
|
+
// After removal, the region element should be null
|
|
147
|
+
expect(region.element).toBeNull();
|
|
148
|
+
// Cleanup
|
|
149
|
+
ws.destroy();
|
|
150
|
+
});
|
|
151
|
+
it('should not retain regions in memory after removal', () => {
|
|
152
|
+
const ws = WaveSurfer.create({ container });
|
|
153
|
+
const regions = ws.registerPlugin(RegionsPlugin.create());
|
|
154
|
+
// Mock duration so regions are saved immediately
|
|
155
|
+
jest.spyOn(ws, 'getDuration').mockReturnValue(10);
|
|
156
|
+
jest.spyOn(ws, 'getDecodedData').mockReturnValue({ numberOfChannels: 1 });
|
|
157
|
+
// Create multiple regions
|
|
158
|
+
const region1 = regions.addRegion({ start: 0, end: 1 });
|
|
159
|
+
const region2 = regions.addRegion({ start: 2, end: 3 });
|
|
160
|
+
const region3 = regions.addRegion({ start: 4, end: 5 });
|
|
161
|
+
expect(regions.getRegions().length).toBe(3);
|
|
162
|
+
// Remove regions
|
|
163
|
+
region1.remove();
|
|
164
|
+
region2.remove();
|
|
165
|
+
// Only one region should remain
|
|
166
|
+
expect(regions.getRegions().length).toBe(1);
|
|
167
|
+
expect(regions.getRegions()[0]).toBe(region3);
|
|
168
|
+
// Remove last region
|
|
169
|
+
region3.remove();
|
|
170
|
+
expect(regions.getRegions().length).toBe(0);
|
|
171
|
+
// Cleanup
|
|
172
|
+
ws.destroy();
|
|
173
|
+
});
|
|
174
|
+
it('should cleanup content event listeners when region is removed', () => {
|
|
175
|
+
const ws = WaveSurfer.create({ container });
|
|
176
|
+
const regions = ws.registerPlugin(RegionsPlugin.create());
|
|
177
|
+
// Mock duration so regions are saved immediately
|
|
178
|
+
jest.spyOn(ws, 'getDuration').mockReturnValue(10);
|
|
179
|
+
jest.spyOn(ws, 'getDecodedData').mockReturnValue({ numberOfChannels: 1 });
|
|
180
|
+
// Create a region with editable content
|
|
181
|
+
const region = regions.addRegion({
|
|
182
|
+
start: 0,
|
|
183
|
+
end: 1,
|
|
184
|
+
content: 'Test content',
|
|
185
|
+
contentEditable: true,
|
|
186
|
+
});
|
|
187
|
+
// Remove the region
|
|
188
|
+
region.remove();
|
|
189
|
+
// Content should be cleaned up
|
|
190
|
+
expect(region.element).toBeNull();
|
|
191
|
+
// Cleanup
|
|
192
|
+
ws.destroy();
|
|
193
|
+
});
|
|
194
|
+
it('should cleanup DOM event streams on region removal', () => {
|
|
195
|
+
const ws = WaveSurfer.create({ container });
|
|
196
|
+
const regions = ws.registerPlugin(RegionsPlugin.create());
|
|
197
|
+
// Mock duration so regions are saved immediately
|
|
198
|
+
jest.spyOn(ws, 'getDuration').mockReturnValue(10);
|
|
199
|
+
jest.spyOn(ws, 'getDecodedData').mockReturnValue({ numberOfChannels: 1 });
|
|
200
|
+
// Create regions
|
|
201
|
+
const createdRegions = [];
|
|
202
|
+
for (let i = 0; i < 10; i++) {
|
|
203
|
+
createdRegions.push(regions.addRegion({ start: i, end: i + 1 }));
|
|
204
|
+
}
|
|
205
|
+
expect(regions.getRegions().length).toBe(10);
|
|
206
|
+
// Remove all regions
|
|
207
|
+
createdRegions.forEach((r) => r.remove());
|
|
208
|
+
// All regions should be removed
|
|
209
|
+
expect(regions.getRegions().length).toBe(0);
|
|
210
|
+
// Cleanup
|
|
211
|
+
ws.destroy();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('Event listener cleanup', () => {
|
|
215
|
+
it('should properly cleanup on destroy', () => {
|
|
216
|
+
const ws = WaveSurfer.create({ container });
|
|
217
|
+
// Get renderer to ensure it's initialized
|
|
218
|
+
const renderer = ws.getRenderer();
|
|
219
|
+
expect(renderer).toBeDefined();
|
|
220
|
+
// Should not throw during destroy
|
|
221
|
+
expect(() => {
|
|
222
|
+
ws.destroy();
|
|
223
|
+
}).not.toThrow();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('Reactive system cleanup', () => {
|
|
227
|
+
it('should have reactive state available', () => {
|
|
228
|
+
const ws = WaveSurfer.create({ container });
|
|
229
|
+
const state = ws.getState();
|
|
230
|
+
// State should expose reactive signals
|
|
231
|
+
expect(state.isPlaying).toBeDefined();
|
|
232
|
+
expect(state.currentTime).toBeDefined();
|
|
233
|
+
expect(state.duration).toBeDefined();
|
|
234
|
+
expect(state.volume).toBeDefined();
|
|
235
|
+
expect(state.progressPercent).toBeDefined();
|
|
236
|
+
// Cleanup
|
|
237
|
+
ws.destroy();
|
|
238
|
+
});
|
|
239
|
+
it('should not accumulate subscriptions across instances', () => {
|
|
240
|
+
const instances = [];
|
|
241
|
+
// Create multiple instances
|
|
242
|
+
for (let i = 0; i < 5; i++) {
|
|
243
|
+
const ws = WaveSurfer.create({ container });
|
|
244
|
+
instances.push(ws);
|
|
245
|
+
}
|
|
246
|
+
// Each instance should be independent
|
|
247
|
+
expect(instances.length).toBe(5);
|
|
248
|
+
// Destroy all instances
|
|
249
|
+
instances.forEach((ws) => ws.destroy());
|
|
250
|
+
// All instances should be cleaned up
|
|
251
|
+
expect(instances.every((ws) => ws !== null)).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe('Edge cases', () => {
|
|
255
|
+
it('should handle destroy called multiple times', () => {
|
|
256
|
+
const ws = WaveSurfer.create({ container });
|
|
257
|
+
// Should not throw when destroyed multiple times
|
|
258
|
+
expect(() => {
|
|
259
|
+
ws.destroy();
|
|
260
|
+
ws.destroy();
|
|
261
|
+
ws.destroy();
|
|
262
|
+
}).not.toThrow();
|
|
263
|
+
});
|
|
264
|
+
it('should handle destroy without initialization', () => {
|
|
265
|
+
const ws = WaveSurfer.create({ container });
|
|
266
|
+
// Destroy immediately without loading audio
|
|
267
|
+
expect(() => {
|
|
268
|
+
ws.destroy();
|
|
269
|
+
}).not.toThrow();
|
|
270
|
+
});
|
|
271
|
+
it('should cleanup even if events are subscribed during destroy', () => {
|
|
272
|
+
const ws = WaveSurfer.create({ container });
|
|
273
|
+
// Subscribe to destroy event
|
|
274
|
+
const destroyHandler = jest.fn();
|
|
275
|
+
ws.on('destroy', destroyHandler);
|
|
276
|
+
ws.destroy();
|
|
277
|
+
// Destroy handler should have been called
|
|
278
|
+
expect(destroyHandler).toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -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
|
});
|
|
@@ -29,6 +29,7 @@ describe('renderer-utils', () => {
|
|
|
29
29
|
barRadius: 3,
|
|
30
30
|
barIndexScale: 100 / ((4 + 2) * 10),
|
|
31
31
|
barSpacing: 6,
|
|
32
|
+
barMinHeight: 0,
|
|
32
33
|
});
|
|
33
34
|
});
|
|
34
35
|
});
|
|
@@ -47,6 +48,23 @@ describe('renderer-utils', () => {
|
|
|
47
48
|
vScale: 1,
|
|
48
49
|
})).toEqual({ topHeight: 0, totalHeight: 1 });
|
|
49
50
|
});
|
|
51
|
+
it('ensures total height is at least barMinHeight', () => {
|
|
52
|
+
expect(calculateBarHeights({
|
|
53
|
+
maxTop: 0,
|
|
54
|
+
maxBottom: 0,
|
|
55
|
+
halfHeight: 20,
|
|
56
|
+
vScale: 1,
|
|
57
|
+
barMinHeight: 10,
|
|
58
|
+
})).toEqual({ topHeight: 5, totalHeight: 10 });
|
|
59
|
+
expect(calculateBarHeights({
|
|
60
|
+
maxTop: 0,
|
|
61
|
+
maxBottom: 0,
|
|
62
|
+
halfHeight: 20,
|
|
63
|
+
vScale: 1,
|
|
64
|
+
barMinHeight: 10,
|
|
65
|
+
barAlign: 'top',
|
|
66
|
+
})).toEqual({ topHeight: 0, totalHeight: 10 });
|
|
67
|
+
});
|
|
50
68
|
});
|
|
51
69
|
describe('resolveBarYPosition', () => {
|
|
52
70
|
const baseArgs = {
|
|
@@ -85,6 +103,7 @@ describe('renderer-utils', () => {
|
|
|
85
103
|
vScale: 1,
|
|
86
104
|
canvasHeight: 40,
|
|
87
105
|
barAlign: undefined,
|
|
106
|
+
barMinHeight: 0,
|
|
88
107
|
});
|
|
89
108
|
expect(segments).toEqual([
|
|
90
109
|
{ x: 0, y: 8, width: 1, height: 3 },
|
|
@@ -95,6 +114,33 @@ describe('renderer-utils', () => {
|
|
|
95
114
|
{ x: 5, y: 0, width: 1, height: 16 },
|
|
96
115
|
]);
|
|
97
116
|
});
|
|
117
|
+
it('ensures bars are at least barMinHeight tall', () => {
|
|
118
|
+
const height = 40;
|
|
119
|
+
const length = 10;
|
|
120
|
+
const { barIndexScale, barSpacing, barWidth, halfHeight } = calculateBarRenderConfig({
|
|
121
|
+
width: 100,
|
|
122
|
+
height,
|
|
123
|
+
length,
|
|
124
|
+
options,
|
|
125
|
+
pixelRatio: 1,
|
|
126
|
+
});
|
|
127
|
+
const segments = calculateBarSegments({
|
|
128
|
+
channelData: [
|
|
129
|
+
new Float32Array(length).fill(0.001), // Very small values
|
|
130
|
+
],
|
|
131
|
+
barIndexScale,
|
|
132
|
+
barSpacing,
|
|
133
|
+
barWidth,
|
|
134
|
+
halfHeight,
|
|
135
|
+
vScale: 1,
|
|
136
|
+
canvasHeight: height / 2,
|
|
137
|
+
barAlign: undefined,
|
|
138
|
+
barMinHeight: 10,
|
|
139
|
+
});
|
|
140
|
+
expect(segments.length).toBeGreaterThan(0);
|
|
141
|
+
expect(segments[0].height).toBe(10);
|
|
142
|
+
expect(segments[0].y).toBe(15); // Centered: 20 - 10/2
|
|
143
|
+
});
|
|
98
144
|
});
|
|
99
145
|
describe('getRelativePointerPosition', () => {
|
|
100
146
|
it('returns pointer coordinates as relative offsets', () => {
|
|
@@ -302,12 +348,12 @@ describe('renderer-utils', () => {
|
|
|
302
348
|
});
|
|
303
349
|
});
|
|
304
350
|
describe('calculateScrollPercentages', () => {
|
|
305
|
-
it('returns
|
|
351
|
+
it('returns full range when scroll width is zero', () => {
|
|
306
352
|
expect(calculateScrollPercentages({
|
|
307
353
|
scrollLeft: 0,
|
|
308
354
|
clientWidth: 100,
|
|
309
355
|
scrollWidth: 0,
|
|
310
|
-
})).toEqual({ startX: 0, endX:
|
|
356
|
+
})).toEqual({ startX: 0, endX: 1 });
|
|
311
357
|
});
|
|
312
358
|
it('returns start and end ratios relative to scroll width', () => {
|
|
313
359
|
expect(calculateScrollPercentages({
|
|
@@ -316,5 +362,12 @@ describe('renderer-utils', () => {
|
|
|
316
362
|
scrollWidth: 400,
|
|
317
363
|
})).toEqual({ startX: 0.125, endX: 0.375 });
|
|
318
364
|
});
|
|
365
|
+
it('clamps values to 0-1 range', () => {
|
|
366
|
+
expect(calculateScrollPercentages({
|
|
367
|
+
scrollLeft: -10,
|
|
368
|
+
clientWidth: 100,
|
|
369
|
+
scrollWidth: 400,
|
|
370
|
+
})).toEqual({ startX: 0, endX: 0.225 });
|
|
371
|
+
});
|
|
319
372
|
});
|
|
320
373
|
});
|
package/dist/draggable.d.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use createDragStream from './reactive/drag-stream.js' instead.
|
|
3
|
+
* This function is maintained for backward compatibility but will be removed in a future version.
|
|
4
|
+
*/
|
|
1
5
|
export declare function makeDraggable(element: HTMLElement | null, onDrag: (dx: number, dy: number, x: number, y: number) => void, onStart?: (x: number, y: number) => void, onEnd?: (x: number, y: number) => void, threshold?: number, mouseButton?: number, touchDelay?: number): () => void;
|
package/dist/draggable.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use createDragStream from './reactive/drag-stream.js' instead.
|
|
3
|
+
* This function is maintained for backward compatibility but will be removed in a future version.
|
|
4
|
+
*/
|
|
1
5
|
export function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mouseButton = 0, touchDelay = 100) {
|
|
2
6
|
if (!element)
|
|
3
7
|
return () => void 0;
|
package/dist/player.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventEmitter, { type GeneralEventTypes } from './event-emitter.js';
|
|
2
|
+
import { type WritableSignal } from './reactive/store.js';
|
|
2
3
|
type PlayerOptions = {
|
|
3
4
|
media?: HTMLMediaElement;
|
|
4
5
|
mediaControls?: boolean;
|
|
@@ -8,7 +9,27 @@ type PlayerOptions = {
|
|
|
8
9
|
declare class Player<T extends GeneralEventTypes> extends EventEmitter<T> {
|
|
9
10
|
protected media: HTMLMediaElement;
|
|
10
11
|
private isExternalMedia;
|
|
12
|
+
private _isPlaying;
|
|
13
|
+
private _currentTime;
|
|
14
|
+
private _duration;
|
|
15
|
+
private _volume;
|
|
16
|
+
private _muted;
|
|
17
|
+
private _playbackRate;
|
|
18
|
+
private _seeking;
|
|
19
|
+
private reactiveMediaEventCleanups;
|
|
20
|
+
get isPlayingSignal(): WritableSignal<boolean>;
|
|
21
|
+
get currentTimeSignal(): WritableSignal<number>;
|
|
22
|
+
get durationSignal(): WritableSignal<number>;
|
|
23
|
+
get volumeSignal(): WritableSignal<number>;
|
|
24
|
+
get mutedSignal(): WritableSignal<boolean>;
|
|
25
|
+
get playbackRateSignal(): WritableSignal<number>;
|
|
26
|
+
get seekingSignal(): WritableSignal<boolean>;
|
|
11
27
|
constructor(options: PlayerOptions);
|
|
28
|
+
/**
|
|
29
|
+
* Setup reactive media event handlers that update signals
|
|
30
|
+
* This bridges the imperative HTMLMediaElement API to reactive state
|
|
31
|
+
*/
|
|
32
|
+
private setupReactiveMediaEvents;
|
|
12
33
|
protected onMediaEvent<K extends keyof HTMLElementEventMap>(event: K, callback: (ev: HTMLElementEventMap[K]) => void, options?: boolean | AddEventListenerOptions): () => void;
|
|
13
34
|
protected getSrc(): string;
|
|
14
35
|
private revokeSrc;
|