wavesurfer.js 7.12.6 → 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.d.ts +1 -0
- package/dist/__tests__/hover.test.js +64 -0
- 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.d.ts +1 -0
- package/dist/__tests__/webaudio.test.js +223 -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 +69 -6
- 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);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import HoverPlugin from '../plugins/hover.js';
|
|
2
|
+
import { signal } from '../reactive/store.js';
|
|
3
|
+
const createEmitter = () => {
|
|
4
|
+
const listeners = new Map();
|
|
5
|
+
return {
|
|
6
|
+
on: jest.fn((event, listener) => {
|
|
7
|
+
if (!listeners.has(event)) {
|
|
8
|
+
listeners.set(event, new Set());
|
|
9
|
+
}
|
|
10
|
+
listeners.get(event).add(listener);
|
|
11
|
+
return () => { var _a; return (_a = listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(listener); };
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
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
|
+
};
|
|
27
|
+
describe('HoverPlugin', () => {
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
document.body.innerHTML = '';
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
test('passes the current wavesurfer duration to formatTimeCallback', () => {
|
|
33
|
+
var _a;
|
|
34
|
+
const container = document.createElement('div');
|
|
35
|
+
const formatTimeCallback = jest.fn((seconds) => `${seconds}`);
|
|
36
|
+
const { wavesurfer } = createWaveSurfer(container, 0);
|
|
37
|
+
const plugin = HoverPlugin.create({ formatTimeCallback });
|
|
38
|
+
plugin._init(wavesurfer);
|
|
39
|
+
wavesurfer.getDuration.mockReturnValue(12);
|
|
40
|
+
container.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: 50 }));
|
|
41
|
+
expect(formatTimeCallback).toHaveBeenCalledWith(6);
|
|
42
|
+
expect((_a = container.querySelector('[part="hover-label"]')) === null || _a === void 0 ? void 0 : _a.textContent).toBe('6');
|
|
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
|
+
});
|
|
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();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import WebAudioPlayer from '../webaudio.js';
|
|
2
|
+
// Mock Web Audio API
|
|
3
|
+
function createMockAudioContext() {
|
|
4
|
+
let _currentTime = 0;
|
|
5
|
+
const gainNode = {
|
|
6
|
+
gain: { value: 1 },
|
|
7
|
+
connect: jest.fn(),
|
|
8
|
+
disconnect: jest.fn(),
|
|
9
|
+
};
|
|
10
|
+
let bufferSourceOnended = null;
|
|
11
|
+
const bufferSource = {
|
|
12
|
+
buffer: null,
|
|
13
|
+
playbackRate: { value: 1 },
|
|
14
|
+
connect: jest.fn(),
|
|
15
|
+
disconnect: jest.fn(),
|
|
16
|
+
start: jest.fn(),
|
|
17
|
+
stop: jest.fn(),
|
|
18
|
+
addEventListener: jest.fn(),
|
|
19
|
+
get onended() {
|
|
20
|
+
return bufferSourceOnended;
|
|
21
|
+
},
|
|
22
|
+
set onended(fn) {
|
|
23
|
+
bufferSourceOnended = fn;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const audioContext = {
|
|
27
|
+
get currentTime() {
|
|
28
|
+
return _currentTime;
|
|
29
|
+
},
|
|
30
|
+
set currentTime(v) {
|
|
31
|
+
_currentTime = v;
|
|
32
|
+
},
|
|
33
|
+
sampleRate: 44100,
|
|
34
|
+
destination: {},
|
|
35
|
+
createGain: jest.fn(() => gainNode),
|
|
36
|
+
createBufferSource: jest.fn(() => bufferSource),
|
|
37
|
+
decodeAudioData: jest.fn(),
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
audioContext,
|
|
41
|
+
gainNode,
|
|
42
|
+
bufferSource,
|
|
43
|
+
triggerOnended: () => {
|
|
44
|
+
if (bufferSourceOnended)
|
|
45
|
+
bufferSourceOnended();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createMockBuffer(duration) {
|
|
50
|
+
return { duration, numberOfChannels: 1, getChannelData: jest.fn(() => new Float32Array(0)) };
|
|
51
|
+
}
|
|
52
|
+
describe('WebAudioPlayer', () => {
|
|
53
|
+
describe('audio session playback mode', () => {
|
|
54
|
+
const originalAudioSession = Object.getOwnPropertyDescriptor(navigator, 'audioSession');
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
if (originalAudioSession) {
|
|
57
|
+
Object.defineProperty(navigator, 'audioSession', originalAudioSession);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
delete navigator.audioSession;
|
|
61
|
+
}
|
|
62
|
+
jest.restoreAllMocks();
|
|
63
|
+
});
|
|
64
|
+
test('sets navigator.audioSession.type to playback when available', () => {
|
|
65
|
+
const { audioContext } = createMockAudioContext();
|
|
66
|
+
const audioSession = { type: 'ambient' };
|
|
67
|
+
Object.defineProperty(navigator, 'audioSession', { configurable: true, value: audioSession });
|
|
68
|
+
new WebAudioPlayer(audioContext);
|
|
69
|
+
expect(audioSession.type).toBe('playback');
|
|
70
|
+
});
|
|
71
|
+
test('warns when setting navigator.audioSession.type fails', () => {
|
|
72
|
+
const { audioContext } = createMockAudioContext();
|
|
73
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
74
|
+
const audioSession = {
|
|
75
|
+
get type() {
|
|
76
|
+
return 'ambient';
|
|
77
|
+
},
|
|
78
|
+
set type(_value) {
|
|
79
|
+
throw new Error('nope');
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
Object.defineProperty(navigator, 'audioSession', { configurable: true, value: audioSession });
|
|
83
|
+
new WebAudioPlayer(audioContext);
|
|
84
|
+
expect(warnSpy).toHaveBeenCalledWith('Setting navigator.audioSession.type failed:', expect.objectContaining({ message: 'nope' }));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('onended and finish event', () => {
|
|
88
|
+
test('emits ended when buffer finishes naturally at duration', () => {
|
|
89
|
+
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
90
|
+
const player = new WebAudioPlayer(audioContext);
|
|
91
|
+
const endedSpy = jest.fn();
|
|
92
|
+
player.on('ended', endedSpy);
|
|
93
|
+
player.buffer = createMockBuffer(10);
|
|
94
|
+
// Play
|
|
95
|
+
audioContext.currentTime = 100;
|
|
96
|
+
player.play();
|
|
97
|
+
// Simulate natural end: audioContext.currentTime has advanced by exactly the duration
|
|
98
|
+
audioContext.currentTime = 110; // 100 + 10 seconds of audio
|
|
99
|
+
triggerOnended();
|
|
100
|
+
expect(endedSpy).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
test('emits ended when buffer finishes naturally and currentTime is slightly less than duration due to float precision', () => {
|
|
103
|
+
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
104
|
+
const player = new WebAudioPlayer(audioContext);
|
|
105
|
+
const endedSpy = jest.fn();
|
|
106
|
+
player.on('ended', endedSpy);
|
|
107
|
+
player.buffer = createMockBuffer(10);
|
|
108
|
+
// Play
|
|
109
|
+
audioContext.currentTime = 100;
|
|
110
|
+
player.play();
|
|
111
|
+
// Simulate natural end with slight floating-point imprecision
|
|
112
|
+
// currentTime would be 0 + (109.999 - 100) * 1 = 9.999, which is < 10 (duration)
|
|
113
|
+
audioContext.currentTime = 109.999;
|
|
114
|
+
triggerOnended();
|
|
115
|
+
expect(endedSpy).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
test('does not emit ended when paused programmatically (far from end)', () => {
|
|
118
|
+
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
119
|
+
const player = new WebAudioPlayer(audioContext);
|
|
120
|
+
const endedSpy = jest.fn();
|
|
121
|
+
player.on('ended', endedSpy);
|
|
122
|
+
player.buffer = createMockBuffer(10);
|
|
123
|
+
// Play
|
|
124
|
+
audioContext.currentTime = 100;
|
|
125
|
+
player.play();
|
|
126
|
+
// Advance to mid-playback
|
|
127
|
+
audioContext.currentTime = 105;
|
|
128
|
+
// User pauses (sets this.paused = true synchronously)
|
|
129
|
+
player.pause();
|
|
130
|
+
// onended fires asynchronously after stop()
|
|
131
|
+
triggerOnended();
|
|
132
|
+
expect(endedSpy).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
test('does not emit ended when stopAt stops before end of audio', () => {
|
|
135
|
+
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
136
|
+
const player = new WebAudioPlayer(audioContext);
|
|
137
|
+
const endedSpy = jest.fn();
|
|
138
|
+
player.on('ended', endedSpy);
|
|
139
|
+
player.buffer = createMockBuffer(10);
|
|
140
|
+
// Play
|
|
141
|
+
audioContext.currentTime = 100;
|
|
142
|
+
player.play();
|
|
143
|
+
// Stop at 5 seconds
|
|
144
|
+
audioContext.currentTime = 100;
|
|
145
|
+
player.stopAt(5);
|
|
146
|
+
// Simulate buffer stopping at 5 seconds
|
|
147
|
+
audioContext.currentTime = 105;
|
|
148
|
+
triggerOnended();
|
|
149
|
+
// Should NOT emit ended because currentTime (5) is far from duration (10)
|
|
150
|
+
expect(endedSpy).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
test('emits ended when currentTime slightly exceeds duration', () => {
|
|
153
|
+
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
154
|
+
const player = new WebAudioPlayer(audioContext);
|
|
155
|
+
const endedSpy = jest.fn();
|
|
156
|
+
player.on('ended', endedSpy);
|
|
157
|
+
player.buffer = createMockBuffer(10);
|
|
158
|
+
// Play
|
|
159
|
+
audioContext.currentTime = 100;
|
|
160
|
+
player.play();
|
|
161
|
+
// Simulate natural end: audioContext has advanced slightly past the duration
|
|
162
|
+
audioContext.currentTime = 110.005;
|
|
163
|
+
triggerOnended();
|
|
164
|
+
expect(endedSpy).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
test('emits ended with non-default playback rate', () => {
|
|
167
|
+
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
168
|
+
const player = new WebAudioPlayer(audioContext);
|
|
169
|
+
const endedSpy = jest.fn();
|
|
170
|
+
player.on('ended', endedSpy);
|
|
171
|
+
player.buffer = createMockBuffer(10);
|
|
172
|
+
player._playbackRate = 2;
|
|
173
|
+
// Play
|
|
174
|
+
audioContext.currentTime = 100;
|
|
175
|
+
player.play();
|
|
176
|
+
// At 2x, 10s audio takes 5s real time
|
|
177
|
+
// currentTime = 0 + (104.999 - 100) * 2 = 9.998, which is < 10 but within tolerance
|
|
178
|
+
audioContext.currentTime = 104.999;
|
|
179
|
+
triggerOnended();
|
|
180
|
+
expect(endedSpy).toHaveBeenCalledTimes(1);
|
|
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
|
+
});
|
|
207
|
+
test('does not emit ended when currentTime is beyond tolerance threshold from duration', () => {
|
|
208
|
+
const { audioContext, triggerOnended } = createMockAudioContext();
|
|
209
|
+
const player = new WebAudioPlayer(audioContext);
|
|
210
|
+
const endedSpy = jest.fn();
|
|
211
|
+
player.on('ended', endedSpy);
|
|
212
|
+
player.buffer = createMockBuffer(10);
|
|
213
|
+
// Play
|
|
214
|
+
audioContext.currentTime = 100;
|
|
215
|
+
player.play();
|
|
216
|
+
// Simulate stopAt scenario where currentTime is 0.02s before duration (beyond 0.01s tolerance)
|
|
217
|
+
// currentTime = 0 + (109.98 - 100) * 1 = 9.98, duration - currentTime = 0.02 >= 0.01
|
|
218
|
+
audioContext.currentTime = 109.98;
|
|
219
|
+
triggerOnended();
|
|
220
|
+
expect(endedSpy).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
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;
|