wavesurfer.js 7.12.7 → 7.12.8
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 +12 -6
- package/dist/__tests__/dom.test.js +17 -1
- package/dist/__tests__/drag-stream.test.d.ts +1 -0
- package/dist/__tests__/drag-stream.test.js +108 -0
- package/dist/__tests__/draggable.test.js +34 -0
- package/dist/__tests__/hover.test.js +35 -7
- package/dist/__tests__/regions.test.js +39 -0
- package/dist/__tests__/renderer.test.js +4 -0
- package/dist/__tests__/wavesurfer.test.js +13 -0
- package/dist/__tests__/webaudio.test.js +25 -0
- package/dist/dom.d.ts +5 -0
- package/dist/dom.js +12 -0
- package/dist/draggable.js +14 -4
- package/dist/fetcher.js +31 -10
- package/dist/fft.js +5 -1
- package/dist/player.d.ts +1 -0
- package/dist/player.js +13 -4
- 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 +2 -0
- 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.d.ts +1 -0
- 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/spectrogram-windowed.cjs +1 -1
- package/dist/plugins/spectrogram-windowed.esm.js +1 -1
- package/dist/plugins/spectrogram-windowed.js +1 -1
- package/dist/plugins/spectrogram-windowed.min.js +1 -1
- package/dist/plugins/spectrogram.cjs +1 -1
- package/dist/plugins/spectrogram.esm.js +1 -1
- package/dist/plugins/spectrogram.js +1 -1
- package/dist/plugins/spectrogram.min.js +1 -1
- package/dist/plugins/timeline.cjs +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.d.ts +1 -0
- 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/drag-stream.js +14 -4
- package/dist/reactive/state-event-emitter.js +7 -0
- package/dist/renderer-utils.js +4 -2
- package/dist/renderer.d.ts +2 -0
- package/dist/renderer.js +23 -11
- package/dist/timer.js +1 -0
- package/dist/types.d.ts +18 -2
- package/dist/wavesurfer.cjs +1 -1
- package/dist/wavesurfer.d.ts +2 -0
- package/dist/wavesurfer.esm.js +1 -1
- package/dist/wavesurfer.js +35 -6
- package/dist/wavesurfer.min.js +1 -1
- package/dist/webaudio.d.ts +5 -0
- package/dist/webaudio.js +54 -3
- package/package.json +15 -14
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
<img width="626" alt="waveform screenshot" src="https://github.com/katspaugh/wavesurfer.js/assets/381895/05f03bed-800e-4fa1-b09a-82a39a1c62ce">
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
---
|
|
10
|
+
### Gold Sponsor 💖 [Closed Caption Creator](https://www.closedcaptioncreator.com) – Professional Subtitle Editor
|
|
11
|
+
---
|
|
10
12
|
|
|
11
13
|
# Table of contents
|
|
12
14
|
|
|
@@ -35,7 +37,7 @@ Alternatively, insert a UMD script tag which exports the library as a global `Wa
|
|
|
35
37
|
<script src="https://unpkg.com/wavesurfer.js@7"></script>
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
Create a wavesurfer instance and pass various [options](
|
|
40
|
+
Create a wavesurfer instance and pass various [options](https://wavesurfer.xyz/docs/api/options):
|
|
39
41
|
```js
|
|
40
42
|
const wavesurfer = WaveSurfer.create({
|
|
41
43
|
container: '#waveform',
|
|
@@ -59,13 +61,17 @@ TypeScript types are included in the package, so there's no need to install `@ty
|
|
|
59
61
|
|
|
60
62
|
See more [examples](https://wavesurfer.xyz/examples).
|
|
61
63
|
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
See the [wavesurfer.js guide](https://wavesurfer.xyz/docs/) on our website for beginner-friendly, practical documentation covering common use cases, plugins, framework integration, and troubleshooting.
|
|
67
|
+
|
|
62
68
|
## API reference
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
The full generated API reference lives at [wavesurfer.xyz/docs/api](https://wavesurfer.xyz/docs/api/):
|
|
65
71
|
|
|
66
|
-
* [methods](https://wavesurfer.xyz/docs/methods)
|
|
67
|
-
* [options](
|
|
68
|
-
* [events](
|
|
72
|
+
* [methods](https://wavesurfer.xyz/docs/api/methods)
|
|
73
|
+
* [options](https://wavesurfer.xyz/docs/api/options)
|
|
74
|
+
* [events](https://wavesurfer.xyz/docs/api/events)
|
|
69
75
|
|
|
70
76
|
## Plugins
|
|
71
77
|
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
import createElement from '../dom.js';
|
|
1
|
+
import createElement, { isHTMLElement } from '../dom.js';
|
|
2
|
+
describe('isHTMLElement', () => {
|
|
3
|
+
test('accepts elements from the current realm', () => {
|
|
4
|
+
expect(isHTMLElement(document.createElement('div'))).toBe(true);
|
|
5
|
+
});
|
|
6
|
+
test('accepts element-like objects from another realm (e.g. an iframe)', () => {
|
|
7
|
+
const foreign = { nodeType: 1, style: {} };
|
|
8
|
+
expect(isHTMLElement(foreign)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
test('rejects non-elements', () => {
|
|
11
|
+
expect(isHTMLElement(null)).toBe(false);
|
|
12
|
+
expect(isHTMLElement(undefined)).toBe(false);
|
|
13
|
+
expect(isHTMLElement('#container')).toBe(false);
|
|
14
|
+
expect(isHTMLElement({})).toBe(false);
|
|
15
|
+
expect(isHTMLElement(document.createTextNode('text'))).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
2
18
|
describe('createElement', () => {
|
|
3
19
|
test('creates DOM structure', () => {
|
|
4
20
|
var _a;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createDragStream } from '../reactive/drag-stream.js';
|
|
2
|
+
import { effect } from '../reactive/store.js';
|
|
3
|
+
describe('createDragStream', () => {
|
|
4
|
+
beforeAll(() => {
|
|
5
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
6
|
+
writable: true,
|
|
7
|
+
value: jest.fn().mockReturnValue({
|
|
8
|
+
matches: false,
|
|
9
|
+
addListener: jest.fn(),
|
|
10
|
+
removeListener: jest.fn(),
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
if (typeof window.PointerEvent === 'undefined') {
|
|
14
|
+
class FakePointerEvent extends MouseEvent {
|
|
15
|
+
constructor(type, props) {
|
|
16
|
+
super(type, props);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
window.PointerEvent = FakePointerEvent;
|
|
21
|
+
// @ts-expect-error
|
|
22
|
+
global.PointerEvent = FakePointerEvent;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const pointerEvent = (type, props) => {
|
|
26
|
+
var _a;
|
|
27
|
+
const e = new PointerEvent(type, props);
|
|
28
|
+
Object.defineProperty(e, 'pointerId', { value: (_a = props.pointerId) !== null && _a !== void 0 ? _a : 0, configurable: true });
|
|
29
|
+
return e;
|
|
30
|
+
};
|
|
31
|
+
const setup = () => {
|
|
32
|
+
const el = document.createElement('div');
|
|
33
|
+
document.body.appendChild(el);
|
|
34
|
+
jest.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
35
|
+
left: 0,
|
|
36
|
+
top: 0,
|
|
37
|
+
width: 100,
|
|
38
|
+
height: 100,
|
|
39
|
+
right: 100,
|
|
40
|
+
bottom: 100,
|
|
41
|
+
x: 0,
|
|
42
|
+
y: 0,
|
|
43
|
+
toJSON: () => { },
|
|
44
|
+
});
|
|
45
|
+
const stream = createDragStream(el, { threshold: 0 });
|
|
46
|
+
const events = [];
|
|
47
|
+
const unsubscribe = effect(() => {
|
|
48
|
+
const drag = stream.signal.value;
|
|
49
|
+
if (drag)
|
|
50
|
+
events.push(drag);
|
|
51
|
+
}, [stream.signal]);
|
|
52
|
+
return {
|
|
53
|
+
el,
|
|
54
|
+
events,
|
|
55
|
+
cleanup: () => {
|
|
56
|
+
unsubscribe();
|
|
57
|
+
stream.cleanup();
|
|
58
|
+
el.remove();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
test('emits start, move and end on a simple drag', () => {
|
|
63
|
+
const { el, events, cleanup } = setup();
|
|
64
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 1 }));
|
|
65
|
+
document.dispatchEvent(pointerEvent('pointermove', { clientX: 20, clientY: 20, pointerId: 1 }));
|
|
66
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 20, clientY: 20, pointerId: 1 }));
|
|
67
|
+
expect(events.map((e) => e.type)).toEqual(['start', 'move', 'end']);
|
|
68
|
+
cleanup();
|
|
69
|
+
});
|
|
70
|
+
test('drag still works after a two-finger touch', () => {
|
|
71
|
+
const { el, events, cleanup } = setup();
|
|
72
|
+
// Two-finger tap: the second finger lifts before the first
|
|
73
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 1 }));
|
|
74
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 50, clientY: 10, pointerId: 2 }));
|
|
75
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 50, clientY: 10, pointerId: 2 }));
|
|
76
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 10, clientY: 10, pointerId: 1 }));
|
|
77
|
+
// A subsequent single-finger drag should work
|
|
78
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 3 }));
|
|
79
|
+
document.dispatchEvent(pointerEvent('pointermove', { clientX: 30, clientY: 10, pointerId: 3 }));
|
|
80
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 30, clientY: 10, pointerId: 3 }));
|
|
81
|
+
expect(events.map((e) => e.type)).toEqual(['start', 'move', 'end']);
|
|
82
|
+
cleanup();
|
|
83
|
+
});
|
|
84
|
+
test('a second finger lifting does not end the first pointer drag', () => {
|
|
85
|
+
const { el, events, cleanup } = setup();
|
|
86
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 1 }));
|
|
87
|
+
document.dispatchEvent(pointerEvent('pointermove', { clientX: 20, clientY: 10, pointerId: 1 }));
|
|
88
|
+
// A second finger touches and lifts mid-drag
|
|
89
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 50, clientY: 10, pointerId: 2 }));
|
|
90
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 50, clientY: 10, pointerId: 2 }));
|
|
91
|
+
// The first finger continues dragging and lifts
|
|
92
|
+
document.dispatchEvent(pointerEvent('pointermove', { clientX: 30, clientY: 10, pointerId: 1 }));
|
|
93
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 30, clientY: 10, pointerId: 1 }));
|
|
94
|
+
expect(events.map((e) => e.type)).toEqual(['start', 'move', 'move', 'end']);
|
|
95
|
+
cleanup();
|
|
96
|
+
});
|
|
97
|
+
test('only the dragging pointer moves the drag', () => {
|
|
98
|
+
const { el, events, cleanup } = setup();
|
|
99
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 1 }));
|
|
100
|
+
document.dispatchEvent(pointerEvent('pointermove', { clientX: 20, clientY: 10, pointerId: 1 }));
|
|
101
|
+
// Moves from an untracked pointer are ignored
|
|
102
|
+
document.dispatchEvent(pointerEvent('pointermove', { clientX: 90, clientY: 90, pointerId: 7 }));
|
|
103
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 30, clientY: 10, pointerId: 1 }));
|
|
104
|
+
expect(events.map((e) => e.type)).toEqual(['start', 'move', 'end']);
|
|
105
|
+
expect(events[1].x).toBe(20);
|
|
106
|
+
cleanup();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -21,6 +21,40 @@ describe('makeDraggable', () => {
|
|
|
21
21
|
global.PointerEvent = FakePointerEvent;
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
|
+
test('drag still works after a two-finger touch', () => {
|
|
25
|
+
const el = document.createElement('div');
|
|
26
|
+
document.body.appendChild(el);
|
|
27
|
+
jest.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
28
|
+
left: 0,
|
|
29
|
+
top: 0,
|
|
30
|
+
width: 100,
|
|
31
|
+
height: 100,
|
|
32
|
+
right: 100,
|
|
33
|
+
bottom: 100,
|
|
34
|
+
x: 0,
|
|
35
|
+
y: 0,
|
|
36
|
+
toJSON: () => { },
|
|
37
|
+
});
|
|
38
|
+
const pointerEvent = (type, props) => {
|
|
39
|
+
const e = new PointerEvent(type, props);
|
|
40
|
+
Object.defineProperty(e, 'pointerId', { value: props.pointerId, configurable: true });
|
|
41
|
+
return e;
|
|
42
|
+
};
|
|
43
|
+
const onDrag = jest.fn();
|
|
44
|
+
const unsubscribe = makeDraggable(el, onDrag, undefined, undefined, 0);
|
|
45
|
+
// Two-finger tap: the second finger lifts before the first
|
|
46
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 1 }));
|
|
47
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 50, clientY: 10, pointerId: 2 }));
|
|
48
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 50, clientY: 10, pointerId: 2 }));
|
|
49
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 10, clientY: 10, pointerId: 1 }));
|
|
50
|
+
// A subsequent single-finger drag should work
|
|
51
|
+
el.dispatchEvent(pointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 3 }));
|
|
52
|
+
document.dispatchEvent(pointerEvent('pointermove', { clientX: 30, clientY: 10, pointerId: 3 }));
|
|
53
|
+
document.dispatchEvent(pointerEvent('pointerup', { clientX: 30, clientY: 10, pointerId: 3 }));
|
|
54
|
+
expect(onDrag).toHaveBeenCalled();
|
|
55
|
+
unsubscribe();
|
|
56
|
+
el.remove();
|
|
57
|
+
});
|
|
24
58
|
test('invokes callbacks on drag', () => {
|
|
25
59
|
const el = document.createElement('div');
|
|
26
60
|
document.body.appendChild(el);
|
|
@@ -12,6 +12,18 @@ const createEmitter = () => {
|
|
|
12
12
|
}),
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
|
+
const createWaveSurfer = (container, durationValue) => {
|
|
16
|
+
const duration = signal(durationValue);
|
|
17
|
+
Object.defineProperty(container, 'getBoundingClientRect', {
|
|
18
|
+
configurable: true,
|
|
19
|
+
value: () => ({ left: 0, width: 100 }),
|
|
20
|
+
});
|
|
21
|
+
document.body.appendChild(container);
|
|
22
|
+
return {
|
|
23
|
+
duration,
|
|
24
|
+
wavesurfer: Object.assign(Object.assign({}, createEmitter()), { options: { progressColor: '#555' }, getDuration: jest.fn(() => duration.value), getState: jest.fn(() => ({ duration })), getWrapper: jest.fn(() => container) }),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
15
27
|
describe('HoverPlugin', () => {
|
|
16
28
|
afterEach(() => {
|
|
17
29
|
document.body.innerHTML = '';
|
|
@@ -19,18 +31,34 @@ describe('HoverPlugin', () => {
|
|
|
19
31
|
});
|
|
20
32
|
test('passes the current wavesurfer duration to formatTimeCallback', () => {
|
|
21
33
|
var _a;
|
|
22
|
-
const duration = signal(0);
|
|
23
34
|
const container = document.createElement('div');
|
|
24
35
|
const formatTimeCallback = jest.fn((seconds) => `${seconds}`);
|
|
25
|
-
|
|
26
|
-
configurable: true,
|
|
27
|
-
value: () => ({ left: 0, width: 100 }),
|
|
28
|
-
});
|
|
29
|
-
document.body.appendChild(container);
|
|
36
|
+
const { wavesurfer } = createWaveSurfer(container, 0);
|
|
30
37
|
const plugin = HoverPlugin.create({ formatTimeCallback });
|
|
31
|
-
plugin._init(
|
|
38
|
+
plugin._init(wavesurfer);
|
|
39
|
+
wavesurfer.getDuration.mockReturnValue(12);
|
|
32
40
|
container.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: 50 }));
|
|
33
41
|
expect(formatTimeCallback).toHaveBeenCalledWith(6);
|
|
34
42
|
expect((_a = container.querySelector('[part="hover-label"]')) === null || _a === void 0 ? void 0 : _a.textContent).toBe('6');
|
|
35
43
|
});
|
|
44
|
+
test('keeps the hover line hidden after pointerleave when duration updates', () => {
|
|
45
|
+
const container = document.createElement('div');
|
|
46
|
+
const formatTimeCallback = jest.fn((seconds) => `${seconds}`);
|
|
47
|
+
const { duration, wavesurfer } = createWaveSurfer(container, 10);
|
|
48
|
+
const plugin = HoverPlugin.create({ formatTimeCallback });
|
|
49
|
+
plugin._init(wavesurfer);
|
|
50
|
+
container.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: 50 }));
|
|
51
|
+
const hover = container.querySelector('[part="hover"]');
|
|
52
|
+
expect(hover === null || hover === void 0 ? void 0 : hover.style.opacity).toBe('1');
|
|
53
|
+
expect(formatTimeCallback).toHaveBeenCalledTimes(1);
|
|
54
|
+
container.dispatchEvent(new MouseEvent('pointerleave', { bubbles: true }));
|
|
55
|
+
expect(hover === null || hover === void 0 ? void 0 : hover.style.opacity).toBe('0');
|
|
56
|
+
// transform is cleared after the opacity transition ends, not immediately
|
|
57
|
+
hover === null || hover === void 0 ? void 0 : hover.dispatchEvent(new Event('transitionend'));
|
|
58
|
+
expect(hover === null || hover === void 0 ? void 0 : hover.style.transform).toBe('');
|
|
59
|
+
duration.set(12);
|
|
60
|
+
expect(hover === null || hover === void 0 ? void 0 : hover.style.opacity).toBe('0');
|
|
61
|
+
expect(hover === null || hover === void 0 ? void 0 : hover.style.transform).toBe('');
|
|
62
|
+
expect(formatTimeCallback).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
36
64
|
});
|
|
@@ -17,6 +17,12 @@ const createWaveSurfer = (duration = 10, width = 100, scroll = 0) => {
|
|
|
17
17
|
document.body.appendChild(wrapper);
|
|
18
18
|
return Object.assign(Object.assign({}, emitter), { getDecodedData: jest.fn(() => ({ numberOfChannels: 1 })), getDuration: jest.fn(() => duration), getScroll: jest.fn(() => scroll), getWidth: jest.fn(() => width), getWrapper: jest.fn(() => wrapper) });
|
|
19
19
|
};
|
|
20
|
+
const mockRect = (element, rect) => {
|
|
21
|
+
Object.defineProperty(element, 'getBoundingClientRect', {
|
|
22
|
+
configurable: true,
|
|
23
|
+
value: jest.fn(() => (Object.assign(Object.assign({}, rect), { right: rect.left + rect.width, bottom: rect.top + rect.height, x: rect.left, y: rect.top, toJSON: () => rect }))),
|
|
24
|
+
});
|
|
25
|
+
};
|
|
20
26
|
describe('RegionsPlugin', () => {
|
|
21
27
|
beforeEach(() => {
|
|
22
28
|
jest.useFakeTimers();
|
|
@@ -58,4 +64,37 @@ describe('RegionsPlugin', () => {
|
|
|
58
64
|
region.setOptions({ start: 8, end: 9 });
|
|
59
65
|
expect((_e = region.element) === null || _e === void 0 ? void 0 : _e.parentElement).toBeNull();
|
|
60
66
|
});
|
|
67
|
+
test('places a region in the first free row instead of summing all prior overlaps', () => {
|
|
68
|
+
var _a, _b, _c;
|
|
69
|
+
const wavesurfer = createWaveSurfer();
|
|
70
|
+
const plugin = RegionsPlugin.create();
|
|
71
|
+
plugin._init(wavesurfer);
|
|
72
|
+
const firstRegion = plugin.addRegion({ start: 0, end: 1, content: 'First' });
|
|
73
|
+
const secondRegion = plugin.addRegion({ start: 1, end: 2, content: 'Second' });
|
|
74
|
+
const thirdRegion = plugin.addRegion({ start: 2, end: 3, content: 'Third' });
|
|
75
|
+
mockRect(firstRegion.content, { left: 0, top: 0, width: 40, height: 10 });
|
|
76
|
+
mockRect(secondRegion.content, { left: 50, top: 0, width: 40, height: 10 });
|
|
77
|
+
mockRect(thirdRegion.content, { left: 30, top: 0, width: 40, height: 10 });
|
|
78
|
+
jest.runOnlyPendingTimers();
|
|
79
|
+
expect((_a = firstRegion.content) === null || _a === void 0 ? void 0 : _a.style.marginTop).toBe('0px');
|
|
80
|
+
expect((_b = secondRegion.content) === null || _b === void 0 ? void 0 : _b.style.marginTop).toBe('0px');
|
|
81
|
+
expect((_c = thirdRegion.content) === null || _c === void 0 ? void 0 : _c.style.marginTop).toBe('12px');
|
|
82
|
+
});
|
|
83
|
+
test('reflows shifted labels when another region moves away', () => {
|
|
84
|
+
var _a, _b, _c;
|
|
85
|
+
const wavesurfer = createWaveSurfer();
|
|
86
|
+
const plugin = RegionsPlugin.create();
|
|
87
|
+
plugin._init(wavesurfer);
|
|
88
|
+
const firstRegion = plugin.addRegion({ start: 0, end: 1, content: 'First' });
|
|
89
|
+
const secondRegion = plugin.addRegion({ start: 1, end: 2, content: 'Second' });
|
|
90
|
+
mockRect(firstRegion.content, { left: 0, top: 0, width: 40, height: 10 });
|
|
91
|
+
mockRect(secondRegion.content, { left: 20, top: 0, width: 40, height: 10 });
|
|
92
|
+
jest.runOnlyPendingTimers();
|
|
93
|
+
expect((_a = secondRegion.content) === null || _a === void 0 ? void 0 : _a.style.marginTop).toBe('12px');
|
|
94
|
+
mockRect(firstRegion.content, { left: 80, top: 0, width: 40, height: 10 });
|
|
95
|
+
firstRegion.onContentBlur();
|
|
96
|
+
jest.runOnlyPendingTimers();
|
|
97
|
+
expect((_b = firstRegion.content) === null || _b === void 0 ? void 0 : _b.style.marginTop).toBe('0px');
|
|
98
|
+
expect((_c = secondRegion.content) === null || _c === void 0 ? void 0 : _c.style.marginTop).toBe('0px');
|
|
99
|
+
});
|
|
61
100
|
});
|
|
@@ -60,6 +60,10 @@ describe('Renderer', () => {
|
|
|
60
60
|
container.remove();
|
|
61
61
|
jest.clearAllMocks();
|
|
62
62
|
});
|
|
63
|
+
test('parentFromOptionsContainer accepts an element from another realm (e.g. an iframe)', () => {
|
|
64
|
+
const foreign = { nodeType: 1, style: {} };
|
|
65
|
+
expect(renderer.parentFromOptionsContainer(foreign)).toBe(foreign);
|
|
66
|
+
});
|
|
63
67
|
test('parentFromOptionsContainer returns element and throws', () => {
|
|
64
68
|
expect(renderer.parentFromOptionsContainer(container)).toBe(container);
|
|
65
69
|
expect(renderer.parentFromOptionsContainer('#root')).toBe(container);
|
|
@@ -178,6 +178,19 @@ describe('WaveSurfer public methods', () => {
|
|
|
178
178
|
expect(spy).toHaveBeenCalledWith(2);
|
|
179
179
|
expect(ws.stopAtPosition).toBe(4);
|
|
180
180
|
}));
|
|
181
|
+
test('pauses and clamps time to the exact stop position when playback overshoots it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
+
const ws = createWs();
|
|
183
|
+
const media = ws.getMediaElement();
|
|
184
|
+
Object.defineProperty(media, 'paused', { configurable: true, value: false });
|
|
185
|
+
yield ws.play(1, 2);
|
|
186
|
+
// Simulate the clock overshooting the stop position between timer ticks
|
|
187
|
+
media.currentTime = 2.013;
|
|
188
|
+
const timer = getTimer();
|
|
189
|
+
const tick = timer.on.mock.calls.find(([event]) => event === 'tick')[1];
|
|
190
|
+
tick();
|
|
191
|
+
expect(media.pause).toHaveBeenCalled();
|
|
192
|
+
expect(ws.getCurrentTime()).toBe(2);
|
|
193
|
+
}));
|
|
181
194
|
test('playPause toggles play and pause', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
195
|
const ws = createWs();
|
|
183
196
|
const media = ws.getMediaElement();
|
|
@@ -179,6 +179,31 @@ describe('WebAudioPlayer', () => {
|
|
|
179
179
|
triggerOnended();
|
|
180
180
|
expect(endedSpy).toHaveBeenCalledTimes(1);
|
|
181
181
|
});
|
|
182
|
+
test('stopAt schedules the stop scaled by playback rate', () => {
|
|
183
|
+
const { audioContext, bufferSource } = createMockAudioContext();
|
|
184
|
+
const player = new WebAudioPlayer(audioContext);
|
|
185
|
+
player.buffer = createMockBuffer(10);
|
|
186
|
+
player._playbackRate = 2;
|
|
187
|
+
audioContext.currentTime = 100;
|
|
188
|
+
player.play();
|
|
189
|
+
// At 2x rate, stopping at media time 5 takes only 2.5s of real time
|
|
190
|
+
player.stopAt(5);
|
|
191
|
+
expect(bufferSource.stop).toHaveBeenCalledWith(102.5);
|
|
192
|
+
});
|
|
193
|
+
test('stopAt reports exactly the stop position after the buffer ends', () => {
|
|
194
|
+
var _a;
|
|
195
|
+
const { audioContext, bufferSource } = createMockAudioContext();
|
|
196
|
+
const player = new WebAudioPlayer(audioContext);
|
|
197
|
+
player.buffer = createMockBuffer(10);
|
|
198
|
+
audioContext.currentTime = 100;
|
|
199
|
+
player.play();
|
|
200
|
+
player.stopAt(5);
|
|
201
|
+
// The 'ended' event fires with some latency after the actual stop
|
|
202
|
+
audioContext.currentTime = 105.03;
|
|
203
|
+
const endedListener = (_a = bufferSource.addEventListener.mock.calls.find(([type]) => type === 'ended')) === null || _a === void 0 ? void 0 : _a[1];
|
|
204
|
+
endedListener === null || endedListener === void 0 ? void 0 : endedListener();
|
|
205
|
+
expect(player.currentTime).toBe(5);
|
|
206
|
+
});
|
|
182
207
|
test('does not emit ended when currentTime is beyond tolerance threshold from duration', () => {
|
|
183
208
|
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
184
209
|
const player = new WebAudioPlayer(audioContext);
|
package/dist/dom.d.ts
CHANGED
|
@@ -10,4 +10,9 @@ export declare function createElement(tagName: string, content: TreeNode & {
|
|
|
10
10
|
xmlns: string;
|
|
11
11
|
}, container?: Node): SVGElement;
|
|
12
12
|
export declare function createElement(tagName: string, content?: TreeNode, container?: Node): HTMLElement;
|
|
13
|
+
/**
|
|
14
|
+
* Check if a value is an HTML element, including elements from other realms
|
|
15
|
+
* (e.g. an iframe), for which `instanceof HTMLElement` returns false
|
|
16
|
+
*/
|
|
17
|
+
export declare function isHTMLElement(value: unknown): value is HTMLElement;
|
|
13
18
|
export default createElement;
|
package/dist/dom.js
CHANGED
|
@@ -33,4 +33,16 @@ export function createElement(tagName, content, container) {
|
|
|
33
33
|
container === null || container === void 0 ? void 0 : container.appendChild(el);
|
|
34
34
|
return el;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if a value is an HTML element, including elements from other realms
|
|
38
|
+
* (e.g. an iframe), for which `instanceof HTMLElement` returns false
|
|
39
|
+
*/
|
|
40
|
+
export function isHTMLElement(value) {
|
|
41
|
+
if (value instanceof HTMLElement)
|
|
42
|
+
return true;
|
|
43
|
+
return (typeof value === 'object' &&
|
|
44
|
+
value !== null &&
|
|
45
|
+
value.nodeType === Node.ELEMENT_NODE &&
|
|
46
|
+
typeof value.style === 'object');
|
|
47
|
+
}
|
|
36
48
|
export default createElement;
|
package/dist/draggable.js
CHANGED
|
@@ -11,15 +11,20 @@ export function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mo
|
|
|
11
11
|
const onPointerDown = (event) => {
|
|
12
12
|
if (event.button !== mouseButton)
|
|
13
13
|
return;
|
|
14
|
+
if (activePointers.has(event.pointerId))
|
|
15
|
+
return;
|
|
14
16
|
activePointers.set(event.pointerId, event);
|
|
15
17
|
if (activePointers.size > 1) {
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
20
|
+
const dragPointerId = event.pointerId;
|
|
18
21
|
let startX = event.clientX;
|
|
19
22
|
let startY = event.clientY;
|
|
20
23
|
let isDragging = false;
|
|
21
24
|
const touchStartTime = Date.now();
|
|
22
25
|
const onPointerMove = (event) => {
|
|
26
|
+
if (event.pointerId !== dragPointerId)
|
|
27
|
+
return;
|
|
23
28
|
if (event.defaultPrevented || activePointers.size > 1) {
|
|
24
29
|
return;
|
|
25
30
|
}
|
|
@@ -44,18 +49,23 @@ export function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mo
|
|
|
44
49
|
}
|
|
45
50
|
};
|
|
46
51
|
const onPointerUp = (event) => {
|
|
47
|
-
|
|
48
|
-
if (
|
|
52
|
+
// Only react to pointers that started on the element
|
|
53
|
+
if (!activePointers.delete(event.pointerId))
|
|
54
|
+
return;
|
|
55
|
+
// Only the pointer that started the drag can end it
|
|
56
|
+
if (event.pointerId === dragPointerId && isDragging) {
|
|
49
57
|
const x = event.clientX;
|
|
50
58
|
const y = event.clientY;
|
|
51
59
|
const rect = element.getBoundingClientRect();
|
|
52
60
|
const { left, top } = rect;
|
|
53
61
|
onEnd === null || onEnd === void 0 ? void 0 : onEnd(x - left, y - top);
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
// Keep listening until all pointers are released
|
|
64
|
+
if (activePointers.size === 0) {
|
|
65
|
+
unsubscribeDocument();
|
|
66
|
+
}
|
|
56
67
|
};
|
|
57
68
|
const onPointerLeave = (e) => {
|
|
58
|
-
activePointers.delete(e.pointerId);
|
|
59
69
|
if (!e.relatedTarget || e.relatedTarget === document.documentElement) {
|
|
60
70
|
onPointerUp(e);
|
|
61
71
|
}
|
package/dist/fetcher.js
CHANGED
|
@@ -7,20 +7,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
function watchProgress(response, progressCallback) {
|
|
10
|
+
function watchProgress(response, progressCallback, signal) {
|
|
11
11
|
return __awaiter(this, void 0, void 0, function* () {
|
|
12
|
+
var _a;
|
|
12
13
|
if (!response.body || !response.headers)
|
|
13
14
|
return;
|
|
14
15
|
const reader = response.body.getReader();
|
|
15
16
|
const contentLength = Number(response.headers.get('Content-Length')) || 0;
|
|
16
17
|
let receivedLength = 0;
|
|
17
|
-
//
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
receivedLength += (value === null || value === void 0 ? void 0 : value.length) || 0;
|
|
21
|
-
const percentage = Math.round((receivedLength / contentLength) * 100);
|
|
22
|
-
progressCallback(percentage);
|
|
18
|
+
// Abort the reader when the signal fires
|
|
19
|
+
const onAbort = () => {
|
|
20
|
+
reader.cancel();
|
|
23
21
|
};
|
|
22
|
+
if (signal) {
|
|
23
|
+
if (signal.aborted) {
|
|
24
|
+
reader.cancel();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
28
|
+
}
|
|
24
29
|
// Use iteration instead of recursion to avoid stack issues
|
|
25
30
|
try {
|
|
26
31
|
while (true) {
|
|
@@ -28,24 +33,40 @@ function watchProgress(response, progressCallback) {
|
|
|
28
33
|
if (data.done) {
|
|
29
34
|
break;
|
|
30
35
|
}
|
|
31
|
-
|
|
36
|
+
receivedLength += ((_a = data.value) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
37
|
+
// Only report progress if Content-Length is available and non-zero
|
|
38
|
+
if (contentLength > 0) {
|
|
39
|
+
const percentage = Math.round((receivedLength / contentLength) * 100);
|
|
40
|
+
progressCallback(percentage);
|
|
41
|
+
}
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
catch (err) {
|
|
35
|
-
// Ignore errors
|
|
45
|
+
// Ignore abort errors from reader cancellation
|
|
46
|
+
if (err instanceof DOMException && err.name === 'AbortError')
|
|
47
|
+
return;
|
|
48
|
+
// Ignore other errors because we can only handle the main response
|
|
36
49
|
console.warn('Progress tracking error:', err);
|
|
37
50
|
}
|
|
51
|
+
finally {
|
|
52
|
+
// Remove the abort listener to prevent leaks
|
|
53
|
+
if (signal) {
|
|
54
|
+
signal.removeEventListener('abort', onAbort);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
38
57
|
});
|
|
39
58
|
}
|
|
40
59
|
function fetchBlob(url, progressCallback, requestInit) {
|
|
41
60
|
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
var _a;
|
|
42
62
|
// Fetch the resource
|
|
43
63
|
const response = yield fetch(url, requestInit);
|
|
44
64
|
if (response.status >= 400) {
|
|
45
65
|
throw new Error(`Failed to fetch ${url}: ${response.status} (${response.statusText})`);
|
|
46
66
|
}
|
|
47
67
|
// Read the data to track progress
|
|
48
|
-
|
|
68
|
+
// Pass the abort signal so the progress reader can be cancelled
|
|
69
|
+
watchProgress(response.clone(), progressCallback, (_a = requestInit === null || requestInit === void 0 ? void 0 : requestInit.signal) !== null && _a !== void 0 ? _a : undefined);
|
|
49
70
|
return response.blob();
|
|
50
71
|
});
|
|
51
72
|
}
|
package/dist/fft.js
CHANGED
|
@@ -535,7 +535,11 @@ function FFT(bufferSize, sampleRate, windowFunc, alpha) {
|
|
|
535
535
|
limit = limit << 1;
|
|
536
536
|
bit = bit >> 1;
|
|
537
537
|
}
|
|
538
|
-
|
|
538
|
+
// Precompute sin/cos tables for FFT twiddle factors
|
|
539
|
+
// Index 0 is a special case: -PI/0 would produce NaN, use 0 instead
|
|
540
|
+
this.sinTable[0] = 0;
|
|
541
|
+
this.cosTable[0] = 1;
|
|
542
|
+
for (let i = 1; i < bufferSize; i++) {
|
|
539
543
|
this.sinTable[i] = Math.sin(-Math.PI / i);
|
|
540
544
|
this.cosTable[i] = Math.cos(-Math.PI / i);
|
|
541
545
|
}
|
package/dist/player.d.ts
CHANGED
package/dist/player.js
CHANGED
|
@@ -36,6 +36,7 @@ class Player extends EventEmitter {
|
|
|
36
36
|
constructor(options) {
|
|
37
37
|
super();
|
|
38
38
|
this.isExternalMedia = false;
|
|
39
|
+
this._ownBlobUrl = null;
|
|
39
40
|
this.reactiveMediaEventCleanups = [];
|
|
40
41
|
if (options.media) {
|
|
41
42
|
this.media = options.media;
|
|
@@ -121,9 +122,10 @@ class Player extends EventEmitter {
|
|
|
121
122
|
return this.media.currentSrc || this.media.src || '';
|
|
122
123
|
}
|
|
123
124
|
revokeSrc() {
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
URL.revokeObjectURL(
|
|
125
|
+
// Only revoke blob URLs that we created (not ones owned by external callers)
|
|
126
|
+
if (this._ownBlobUrl) {
|
|
127
|
+
URL.revokeObjectURL(this._ownBlobUrl);
|
|
128
|
+
this._ownBlobUrl = null;
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
canPlayType(type) {
|
|
@@ -135,6 +137,10 @@ class Player extends EventEmitter {
|
|
|
135
137
|
return; // no need to change the source
|
|
136
138
|
this.revokeSrc();
|
|
137
139
|
const newSrc = blob instanceof Blob && (this.canPlayType(blob.type) || !url) ? URL.createObjectURL(blob) : url;
|
|
140
|
+
// Track blob URLs we created so we can revoke them on destroy
|
|
141
|
+
if (newSrc !== url) {
|
|
142
|
+
this._ownBlobUrl = newSrc;
|
|
143
|
+
}
|
|
138
144
|
// Reset the media element, otherwise it keeps the previous source
|
|
139
145
|
if (prevSrc) {
|
|
140
146
|
this.media.removeAttribute('src');
|
|
@@ -152,10 +158,13 @@ class Player extends EventEmitter {
|
|
|
152
158
|
// Cleanup reactive media event listeners
|
|
153
159
|
this.reactiveMediaEventCleanups.forEach((cleanup) => cleanup());
|
|
154
160
|
this.reactiveMediaEventCleanups = [];
|
|
161
|
+
// Revoke blob URLs that we created
|
|
162
|
+
this.revokeSrc();
|
|
163
|
+
// Clear all event emitter listeners
|
|
164
|
+
this.unAll();
|
|
155
165
|
if (this.isExternalMedia)
|
|
156
166
|
return;
|
|
157
167
|
this.media.pause();
|
|
158
|
-
this.revokeSrc();
|
|
159
168
|
this.media.removeAttribute('src');
|
|
160
169
|
// Load resets the media element to its initial state
|
|
161
170
|
this.media.load();
|