wavesurfer.js 7.11.0 → 7.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__tests__/memory-leaks.test.d.ts +7 -0
- package/dist/__tests__/memory-leaks.test.js +184 -0
- package/dist/__tests__/player.test.js +107 -0
- package/dist/__tests__/renderer-utils.test.d.ts +1 -0
- package/dist/__tests__/renderer-utils.test.js +373 -0
- package/dist/decoder.js +12 -4
- package/dist/draggable.d.ts +4 -0
- package/dist/draggable.js +20 -5
- package/dist/fetcher.js +14 -23
- 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.d.ts +10 -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 +126 -0
- package/dist/renderer-utils.js +252 -0
- package/dist/renderer.d.ts +4 -4
- package/dist/renderer.js +133 -197
- 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 +167 -1
- package/dist/wavesurfer.cjs +1 -1
- package/dist/wavesurfer.d.ts +18 -0
- package/dist/wavesurfer.esm.js +1 -1
- package/dist/wavesurfer.js +46 -8
- package/dist/wavesurfer.min.js +1 -1
- package/dist/webaudio.js +4 -0
- package/package.json +6 -6
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event stream emitter - bridges EventEmitter to reactive streams
|
|
3
|
+
*
|
|
4
|
+
* Provides reactive stream API on top of traditional EventEmitter.
|
|
5
|
+
* This allows users to choose between callback-based and stream-based APIs.
|
|
6
|
+
*/
|
|
7
|
+
import { signal } from './store.js';
|
|
8
|
+
/**
|
|
9
|
+
* Convert an EventEmitter event to a reactive signal/stream
|
|
10
|
+
*
|
|
11
|
+
* Creates a signal that updates whenever the event is emitted.
|
|
12
|
+
* Returns both the signal (for reading values) and cleanup function.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const { stream, cleanup } = toStream(wavesurfer, 'play')
|
|
17
|
+
*
|
|
18
|
+
* // Subscribe to play events
|
|
19
|
+
* stream.subscribe(() => console.log('Playing!'))
|
|
20
|
+
*
|
|
21
|
+
* // Cleanup when done
|
|
22
|
+
* cleanup()
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param emitter - EventEmitter instance
|
|
26
|
+
* @param eventName - Name of the event to stream
|
|
27
|
+
* @returns Object with stream signal and cleanup function
|
|
28
|
+
*/
|
|
29
|
+
export function toStream(emitter, eventName) {
|
|
30
|
+
const stream = signal(null);
|
|
31
|
+
// Listen to event and update signal
|
|
32
|
+
const handler = (...args) => {
|
|
33
|
+
stream.set(args);
|
|
34
|
+
};
|
|
35
|
+
// @ts-expect-error - EventEmitter on() signature
|
|
36
|
+
const unsubscribe = emitter.on(eventName, handler);
|
|
37
|
+
return {
|
|
38
|
+
stream,
|
|
39
|
+
cleanup: unsubscribe,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create multiple event streams from an emitter
|
|
44
|
+
*
|
|
45
|
+
* Helper to create streams for multiple events at once.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const streams = toStreams(wavesurfer, ['play', 'pause', 'timeupdate'])
|
|
50
|
+
*
|
|
51
|
+
* streams.play.subscribe(() => console.log('Play'))
|
|
52
|
+
* streams.pause.subscribe(() => console.log('Pause'))
|
|
53
|
+
* streams.timeupdate.subscribe(([time]) => console.log('Time:', time))
|
|
54
|
+
*
|
|
55
|
+
* // Cleanup all
|
|
56
|
+
* streams.cleanup()
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @param emitter - EventEmitter instance
|
|
60
|
+
* @param eventNames - Array of event names to stream
|
|
61
|
+
* @returns Object with streams for each event and cleanup function
|
|
62
|
+
*/
|
|
63
|
+
export function toStreams(emitter, eventNames) {
|
|
64
|
+
const cleanups = [];
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const eventName of eventNames) {
|
|
67
|
+
const { stream, cleanup } = toStream(emitter, eventName);
|
|
68
|
+
result[eventName] = stream;
|
|
69
|
+
cleanups.push(cleanup);
|
|
70
|
+
}
|
|
71
|
+
// Add cleanup that removes all event listeners
|
|
72
|
+
result.cleanup = () => {
|
|
73
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
74
|
+
};
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a stream that combines multiple events into one
|
|
79
|
+
*
|
|
80
|
+
* Useful when you want to react to any of several events.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const { stream, cleanup } = mergeStreams(wavesurfer, ['play', 'pause'])
|
|
85
|
+
*
|
|
86
|
+
* stream.subscribe(({ event, args }) => {
|
|
87
|
+
* console.log(`Event ${event} fired with`, args)
|
|
88
|
+
* })
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @param emitter - EventEmitter instance
|
|
92
|
+
* @param eventNames - Array of event names to merge
|
|
93
|
+
* @returns Object with merged stream and cleanup function
|
|
94
|
+
*/
|
|
95
|
+
export function mergeStreams(emitter, eventNames) {
|
|
96
|
+
const stream = signal(null);
|
|
97
|
+
const cleanups = [];
|
|
98
|
+
for (const eventName of eventNames) {
|
|
99
|
+
// @ts-expect-error - EventEmitter on() signature
|
|
100
|
+
const unsubscribe = emitter.on(eventName, (...args) => {
|
|
101
|
+
stream.set({ event: eventName, args });
|
|
102
|
+
});
|
|
103
|
+
cleanups.push(unsubscribe);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
stream,
|
|
107
|
+
cleanup: () => {
|
|
108
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Helper to map event stream values
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const { stream: timeStream } = toStream(wavesurfer, 'timeupdate')
|
|
118
|
+
* const seconds = mapStream(timeStream, ([time]) => Math.floor(time))
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function mapStream(source, mapper) {
|
|
122
|
+
const result = signal(mapper(source.value));
|
|
123
|
+
const unsubscribe = source.subscribe((value) => {
|
|
124
|
+
result.set(mapper(value));
|
|
125
|
+
});
|
|
126
|
+
result._cleanup = unsubscribe;
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Helper to filter event stream values
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const { stream: timeStream } = toStream(wavesurfer, 'timeupdate')
|
|
135
|
+
* const afterTenSeconds = filterStream(timeStream, ([time]) => time > 10)
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function filterStream(source, predicate) {
|
|
139
|
+
const initialValue = predicate(source.value) ? source.value : null;
|
|
140
|
+
const result = signal(initialValue);
|
|
141
|
+
const unsubscribe = source.subscribe((value) => {
|
|
142
|
+
if (predicate(value)) {
|
|
143
|
+
result.set(value);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
result.set(null);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
result._cleanup = unsubscribe;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event stream utilities for converting DOM events to reactive signals
|
|
3
|
+
*
|
|
4
|
+
* These utilities allow composing event handling using reactive primitives.
|
|
5
|
+
*/
|
|
6
|
+
import { type Signal, type WritableSignal } from './store.js';
|
|
7
|
+
/**
|
|
8
|
+
* Convert DOM events to a reactive signal
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const clicks = fromEvent(button, 'click')
|
|
13
|
+
* clicks.subscribe(event => console.log('Clicked!', event))
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function fromEvent<K extends keyof HTMLElementEventMap>(element: HTMLElement, eventName: K): WritableSignal<HTMLElementEventMap[K] | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Transform stream values using a mapping function
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const clicks = fromEvent(button, 'click')
|
|
23
|
+
* const positions = map(clicks, e => e ? e.clientX : 0)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function map<T, U>(source: Signal<T>, mapper: (value: T) => U): Signal<U>;
|
|
27
|
+
/**
|
|
28
|
+
* Filter stream values based on a predicate
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const numbers = signal(5)
|
|
33
|
+
* const evenOnly = filter(numbers, n => n % 2 === 0)
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function filter<T>(source: Signal<T>, predicate: (value: T) => boolean): Signal<T | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Debounce stream updates - wait for quiet period before emitting
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const input = fromEvent(textField, 'input')
|
|
43
|
+
* const debounced = debounce(input, 300) // Wait 300ms after last input
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function debounce<T>(source: Signal<T>, delay: number): Signal<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Throttle stream updates - limit update frequency
|
|
49
|
+
*
|
|
50
|
+
* Emits immediately, then waits before allowing next emission.
|
|
51
|
+
* Different from debounce which waits for quiet period.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const scroll = fromEvent(window, 'scroll')
|
|
56
|
+
* const throttled = throttle(scroll, 100) // Max once per 100ms
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function throttle<T>(source: Signal<T>, delay: number): Signal<T>;
|
|
60
|
+
/**
|
|
61
|
+
* Cleanup a stream created with event stream utilities
|
|
62
|
+
*
|
|
63
|
+
* This removes event listeners and unsubscribes from sources.
|
|
64
|
+
*/
|
|
65
|
+
export declare function cleanup(stream: Signal<any>): void;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event stream utilities for converting DOM events to reactive signals
|
|
3
|
+
*
|
|
4
|
+
* These utilities allow composing event handling using reactive primitives.
|
|
5
|
+
*/
|
|
6
|
+
import { signal } from './store.js';
|
|
7
|
+
/**
|
|
8
|
+
* Convert DOM events to a reactive signal
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const clicks = fromEvent(button, 'click')
|
|
13
|
+
* clicks.subscribe(event => console.log('Clicked!', event))
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function fromEvent(element, eventName) {
|
|
17
|
+
const stream = signal(null);
|
|
18
|
+
const handler = (event) => {
|
|
19
|
+
stream.set(event);
|
|
20
|
+
};
|
|
21
|
+
element.addEventListener(eventName, handler);
|
|
22
|
+
stream._cleanup = () => {
|
|
23
|
+
element.removeEventListener(eventName, handler);
|
|
24
|
+
};
|
|
25
|
+
return stream;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Transform stream values using a mapping function
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const clicks = fromEvent(button, 'click')
|
|
33
|
+
* const positions = map(clicks, e => e ? e.clientX : 0)
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function map(source, mapper) {
|
|
37
|
+
const result = signal(mapper(source.value));
|
|
38
|
+
const unsubscribe = source.subscribe((value) => {
|
|
39
|
+
;
|
|
40
|
+
result.set(mapper(value));
|
|
41
|
+
});
|
|
42
|
+
result._cleanup = unsubscribe;
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Filter stream values based on a predicate
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const numbers = signal(5)
|
|
51
|
+
* const evenOnly = filter(numbers, n => n % 2 === 0)
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function filter(source, predicate) {
|
|
55
|
+
const initialValue = predicate(source.value) ? source.value : null;
|
|
56
|
+
const result = signal(initialValue);
|
|
57
|
+
const unsubscribe = source.subscribe((value) => {
|
|
58
|
+
if (predicate(value)) {
|
|
59
|
+
;
|
|
60
|
+
result.set(value);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
;
|
|
64
|
+
result.set(null);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
result._cleanup = unsubscribe;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Debounce stream updates - wait for quiet period before emitting
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const input = fromEvent(textField, 'input')
|
|
76
|
+
* const debounced = debounce(input, 300) // Wait 300ms after last input
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function debounce(source, delay) {
|
|
80
|
+
const result = signal(source.value);
|
|
81
|
+
let timeout;
|
|
82
|
+
const unsubscribe = source.subscribe((value) => {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
timeout = setTimeout(() => {
|
|
85
|
+
;
|
|
86
|
+
result.set(value);
|
|
87
|
+
}, delay);
|
|
88
|
+
});
|
|
89
|
+
result._cleanup = () => {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
unsubscribe();
|
|
92
|
+
};
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Throttle stream updates - limit update frequency
|
|
97
|
+
*
|
|
98
|
+
* Emits immediately, then waits before allowing next emission.
|
|
99
|
+
* Different from debounce which waits for quiet period.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const scroll = fromEvent(window, 'scroll')
|
|
104
|
+
* const throttled = throttle(scroll, 100) // Max once per 100ms
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function throttle(source, delay) {
|
|
108
|
+
const result = signal(source.value);
|
|
109
|
+
let lastEmit = 0;
|
|
110
|
+
let timeout;
|
|
111
|
+
const unsubscribe = source.subscribe((value) => {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const timeSinceLastEmit = now - lastEmit;
|
|
114
|
+
if (timeSinceLastEmit >= delay) {
|
|
115
|
+
// Enough time has passed, emit immediately
|
|
116
|
+
;
|
|
117
|
+
result.set(value);
|
|
118
|
+
lastEmit = now;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Too soon, schedule for later
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
timeout = setTimeout(() => {
|
|
124
|
+
;
|
|
125
|
+
result.set(value);
|
|
126
|
+
lastEmit = Date.now();
|
|
127
|
+
}, delay - timeSinceLastEmit);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
result._cleanup = () => {
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
unsubscribe();
|
|
133
|
+
};
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Cleanup a stream created with event stream utilities
|
|
138
|
+
*
|
|
139
|
+
* This removes event listeners and unsubscribes from sources.
|
|
140
|
+
*/
|
|
141
|
+
export function cleanup(stream) {
|
|
142
|
+
const cleanupFn = stream._cleanup;
|
|
143
|
+
if (typeof cleanupFn === 'function') {
|
|
144
|
+
cleanupFn();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media event bridge utilities
|
|
3
|
+
*
|
|
4
|
+
* Bridges HTMLMediaElement events to reactive state updates.
|
|
5
|
+
* Provides a clean separation between imperative media API and reactive state.
|
|
6
|
+
*/
|
|
7
|
+
import type { WaveSurferActions } from '../state/wavesurfer-state.js';
|
|
8
|
+
/**
|
|
9
|
+
* Bridge HTMLMediaElement events to WaveSurfer state actions
|
|
10
|
+
*
|
|
11
|
+
* This function sets up event listeners on a media element that automatically
|
|
12
|
+
* update the reactive state through actions. It handles all standard media events
|
|
13
|
+
* (play, pause, timeupdate, etc.) and keeps state in sync with media.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const { state, actions } = createWaveSurferState()
|
|
18
|
+
* const media = document.createElement('audio')
|
|
19
|
+
*
|
|
20
|
+
* const cleanup = bridgeMediaEvents(media, actions)
|
|
21
|
+
*
|
|
22
|
+
* // Now media events automatically update state
|
|
23
|
+
* media.play() // → actions.setPlaying(true)
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @param media - HTMLMediaElement to listen to
|
|
27
|
+
* @param actions - State actions to call on events
|
|
28
|
+
* @returns Cleanup function that removes all listeners
|
|
29
|
+
*/
|
|
30
|
+
export declare function bridgeMediaEvents(media: HTMLMediaElement, actions: WaveSurferActions): () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Bridge HTMLMediaElement events with custom handler
|
|
33
|
+
*
|
|
34
|
+
* Similar to bridgeMediaEvents but allows custom state update logic.
|
|
35
|
+
* Useful when you need more control over how events map to state.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const cleanup = bridgeMediaEventsWithHandler(media, (event, data) => {
|
|
40
|
+
* if (event === 'play') {
|
|
41
|
+
* actions.setPlaying(true)
|
|
42
|
+
* console.log('Started playing')
|
|
43
|
+
* }
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @param media - HTMLMediaElement to listen to
|
|
48
|
+
* @param handler - Custom handler function
|
|
49
|
+
* @returns Cleanup function that removes all listeners
|
|
50
|
+
*/
|
|
51
|
+
export declare function bridgeMediaEventsWithHandler(media: HTMLMediaElement, handler: (event: string, data?: any) => void): () => void;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media event bridge utilities
|
|
3
|
+
*
|
|
4
|
+
* Bridges HTMLMediaElement events to reactive state updates.
|
|
5
|
+
* Provides a clean separation between imperative media API and reactive state.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Bridge HTMLMediaElement events to WaveSurfer state actions
|
|
9
|
+
*
|
|
10
|
+
* This function sets up event listeners on a media element that automatically
|
|
11
|
+
* update the reactive state through actions. It handles all standard media events
|
|
12
|
+
* (play, pause, timeupdate, etc.) and keeps state in sync with media.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const { state, actions } = createWaveSurferState()
|
|
17
|
+
* const media = document.createElement('audio')
|
|
18
|
+
*
|
|
19
|
+
* const cleanup = bridgeMediaEvents(media, actions)
|
|
20
|
+
*
|
|
21
|
+
* // Now media events automatically update state
|
|
22
|
+
* media.play() // → actions.setPlaying(true)
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param media - HTMLMediaElement to listen to
|
|
26
|
+
* @param actions - State actions to call on events
|
|
27
|
+
* @returns Cleanup function that removes all listeners
|
|
28
|
+
*/
|
|
29
|
+
export function bridgeMediaEvents(media, actions) {
|
|
30
|
+
const listeners = [];
|
|
31
|
+
// Helper to add event listener and track cleanup
|
|
32
|
+
const addListener = (event, handler, options) => {
|
|
33
|
+
media.addEventListener(event, handler, options);
|
|
34
|
+
listeners.push(() => media.removeEventListener(event, handler));
|
|
35
|
+
};
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Playback State Events
|
|
38
|
+
// ============================================================================
|
|
39
|
+
addListener('play', () => {
|
|
40
|
+
actions.setPlaying(true);
|
|
41
|
+
});
|
|
42
|
+
addListener('pause', () => {
|
|
43
|
+
actions.setPlaying(false);
|
|
44
|
+
});
|
|
45
|
+
addListener('ended', () => {
|
|
46
|
+
actions.setPlaying(false);
|
|
47
|
+
// Set current time to duration on end
|
|
48
|
+
if (media.duration) {
|
|
49
|
+
actions.setCurrentTime(media.duration);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Time and Duration Events
|
|
54
|
+
// ============================================================================
|
|
55
|
+
addListener('timeupdate', () => {
|
|
56
|
+
actions.setCurrentTime(media.currentTime);
|
|
57
|
+
});
|
|
58
|
+
addListener('durationchange', () => {
|
|
59
|
+
if (isFinite(media.duration)) {
|
|
60
|
+
actions.setDuration(media.duration);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
addListener('loadedmetadata', () => {
|
|
64
|
+
if (isFinite(media.duration)) {
|
|
65
|
+
actions.setDuration(media.duration);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Seeking Events
|
|
70
|
+
// ============================================================================
|
|
71
|
+
addListener('seeking', () => {
|
|
72
|
+
actions.setSeeking(true);
|
|
73
|
+
});
|
|
74
|
+
addListener('seeked', () => {
|
|
75
|
+
actions.setSeeking(false);
|
|
76
|
+
});
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Volume Events
|
|
79
|
+
// ============================================================================
|
|
80
|
+
addListener('volumechange', () => {
|
|
81
|
+
actions.setVolume(media.volume);
|
|
82
|
+
});
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Playback Rate Events
|
|
85
|
+
// ============================================================================
|
|
86
|
+
addListener('ratechange', () => {
|
|
87
|
+
actions.setPlaybackRate(media.playbackRate);
|
|
88
|
+
});
|
|
89
|
+
// Return cleanup function that removes all listeners
|
|
90
|
+
return () => {
|
|
91
|
+
listeners.forEach((cleanup) => cleanup());
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Bridge HTMLMediaElement events with custom handler
|
|
96
|
+
*
|
|
97
|
+
* Similar to bridgeMediaEvents but allows custom state update logic.
|
|
98
|
+
* Useful when you need more control over how events map to state.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const cleanup = bridgeMediaEventsWithHandler(media, (event, data) => {
|
|
103
|
+
* if (event === 'play') {
|
|
104
|
+
* actions.setPlaying(true)
|
|
105
|
+
* console.log('Started playing')
|
|
106
|
+
* }
|
|
107
|
+
* })
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @param media - HTMLMediaElement to listen to
|
|
111
|
+
* @param handler - Custom handler function
|
|
112
|
+
* @returns Cleanup function that removes all listeners
|
|
113
|
+
*/
|
|
114
|
+
export function bridgeMediaEventsWithHandler(media, handler) {
|
|
115
|
+
const listeners = [];
|
|
116
|
+
const addListener = (event) => {
|
|
117
|
+
const listener = (e) => {
|
|
118
|
+
handler(event, e);
|
|
119
|
+
};
|
|
120
|
+
media.addEventListener(event, listener);
|
|
121
|
+
listeners.push(() => media.removeEventListener(event, listener));
|
|
122
|
+
};
|
|
123
|
+
// Add all standard media events
|
|
124
|
+
const events = [
|
|
125
|
+
'play',
|
|
126
|
+
'pause',
|
|
127
|
+
'ended',
|
|
128
|
+
'timeupdate',
|
|
129
|
+
'durationchange',
|
|
130
|
+
'loadedmetadata',
|
|
131
|
+
'seeking',
|
|
132
|
+
'seeked',
|
|
133
|
+
'volumechange',
|
|
134
|
+
'ratechange',
|
|
135
|
+
'waiting',
|
|
136
|
+
'canplay',
|
|
137
|
+
'canplaythrough',
|
|
138
|
+
'loadstart',
|
|
139
|
+
'progress',
|
|
140
|
+
'suspend',
|
|
141
|
+
'abort',
|
|
142
|
+
'error',
|
|
143
|
+
'emptied',
|
|
144
|
+
'stalled',
|
|
145
|
+
'loadeddata',
|
|
146
|
+
'playing',
|
|
147
|
+
];
|
|
148
|
+
events.forEach((event) => addListener(event));
|
|
149
|
+
return () => {
|
|
150
|
+
listeners.forEach((cleanup) => cleanup());
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RenderScheduler batches multiple render requests into a single frame using requestAnimationFrame.
|
|
3
|
+
* This prevents multiple state changes from triggering redundant renders.
|
|
4
|
+
*/
|
|
5
|
+
export type RenderPriority = 'high' | 'normal' | 'low';
|
|
6
|
+
export declare class RenderScheduler {
|
|
7
|
+
private pendingRender;
|
|
8
|
+
private rafId;
|
|
9
|
+
/**
|
|
10
|
+
* Schedule a render to occur on the next animation frame.
|
|
11
|
+
* If a render is already scheduled, this is a no-op.
|
|
12
|
+
*
|
|
13
|
+
* @param renderFn - The function to call to perform the render
|
|
14
|
+
* @param priority - Render priority (high = immediate, normal/low = batched)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const scheduler = new RenderScheduler()
|
|
19
|
+
*
|
|
20
|
+
* // Multiple calls in same frame = single render
|
|
21
|
+
* scheduler.scheduleRender(() => draw())
|
|
22
|
+
* scheduler.scheduleRender(() => draw()) // no-op
|
|
23
|
+
* scheduler.scheduleRender(() => draw()) // no-op
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
scheduleRender(renderFn: () => void, priority?: RenderPriority): void;
|
|
27
|
+
/**
|
|
28
|
+
* Cancel any pending render request.
|
|
29
|
+
* Useful when unmounting or destroying components.
|
|
30
|
+
*/
|
|
31
|
+
cancelRender(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Force an immediate synchronous render, canceling any pending batched render.
|
|
34
|
+
* Use for high-priority updates like cursor during playback, or for testing.
|
|
35
|
+
*
|
|
36
|
+
* @param renderFn - The function to call to perform the render
|
|
37
|
+
*/
|
|
38
|
+
flushRender(renderFn: () => void): void;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a render is currently scheduled.
|
|
41
|
+
*/
|
|
42
|
+
isPending(): boolean;
|
|
43
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RenderScheduler batches multiple render requests into a single frame using requestAnimationFrame.
|
|
3
|
+
* This prevents multiple state changes from triggering redundant renders.
|
|
4
|
+
*/
|
|
5
|
+
export class RenderScheduler {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.pendingRender = false;
|
|
8
|
+
this.rafId = null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Schedule a render to occur on the next animation frame.
|
|
12
|
+
* If a render is already scheduled, this is a no-op.
|
|
13
|
+
*
|
|
14
|
+
* @param renderFn - The function to call to perform the render
|
|
15
|
+
* @param priority - Render priority (high = immediate, normal/low = batched)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const scheduler = new RenderScheduler()
|
|
20
|
+
*
|
|
21
|
+
* // Multiple calls in same frame = single render
|
|
22
|
+
* scheduler.scheduleRender(() => draw())
|
|
23
|
+
* scheduler.scheduleRender(() => draw()) // no-op
|
|
24
|
+
* scheduler.scheduleRender(() => draw()) // no-op
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
scheduleRender(renderFn, priority = 'normal') {
|
|
28
|
+
// High priority renders happen immediately
|
|
29
|
+
if (priority === 'high') {
|
|
30
|
+
this.flushRender(renderFn);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// If already scheduled, don't schedule again
|
|
34
|
+
if (this.pendingRender)
|
|
35
|
+
return;
|
|
36
|
+
this.pendingRender = true;
|
|
37
|
+
this.rafId = requestAnimationFrame(() => {
|
|
38
|
+
try {
|
|
39
|
+
renderFn();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
// Always clean up, even if render throws
|
|
43
|
+
this.pendingRender = false;
|
|
44
|
+
this.rafId = null;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Cancel any pending render request.
|
|
50
|
+
* Useful when unmounting or destroying components.
|
|
51
|
+
*/
|
|
52
|
+
cancelRender() {
|
|
53
|
+
if (this.rafId !== null) {
|
|
54
|
+
cancelAnimationFrame(this.rafId);
|
|
55
|
+
this.rafId = null;
|
|
56
|
+
this.pendingRender = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Force an immediate synchronous render, canceling any pending batched render.
|
|
61
|
+
* Use for high-priority updates like cursor during playback, or for testing.
|
|
62
|
+
*
|
|
63
|
+
* @param renderFn - The function to call to perform the render
|
|
64
|
+
*/
|
|
65
|
+
flushRender(renderFn) {
|
|
66
|
+
this.cancelRender();
|
|
67
|
+
renderFn();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if a render is currently scheduled.
|
|
71
|
+
*/
|
|
72
|
+
isPending() {
|
|
73
|
+
return this.pendingRender;
|
|
74
|
+
}
|
|
75
|
+
}
|