wavesurfer.js 7.11.0 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/memory-leaks.test.d.ts +7 -0
  3. package/dist/__tests__/memory-leaks.test.js +184 -0
  4. package/dist/__tests__/player.test.js +107 -0
  5. package/dist/__tests__/renderer-utils.test.d.ts +1 -0
  6. package/dist/__tests__/renderer-utils.test.js +373 -0
  7. package/dist/decoder.js +12 -4
  8. package/dist/draggable.d.ts +4 -0
  9. package/dist/draggable.js +20 -5
  10. package/dist/fetcher.js +14 -23
  11. package/dist/player.d.ts +21 -0
  12. package/dist/player.js +86 -0
  13. package/dist/plugins/envelope.cjs +1 -1
  14. package/dist/plugins/envelope.esm.js +1 -1
  15. package/dist/plugins/envelope.js +1 -1
  16. package/dist/plugins/envelope.min.js +1 -1
  17. package/dist/plugins/hover.cjs +1 -1
  18. package/dist/plugins/hover.d.ts +0 -3
  19. package/dist/plugins/hover.esm.js +1 -1
  20. package/dist/plugins/hover.js +1 -1
  21. package/dist/plugins/hover.min.js +1 -1
  22. package/dist/plugins/minimap.cjs +1 -1
  23. package/dist/plugins/minimap.esm.js +1 -1
  24. package/dist/plugins/minimap.js +1 -1
  25. package/dist/plugins/minimap.min.js +1 -1
  26. package/dist/plugins/record.cjs +1 -1
  27. package/dist/plugins/record.esm.js +1 -1
  28. package/dist/plugins/record.js +1 -1
  29. package/dist/plugins/record.min.js +1 -1
  30. package/dist/plugins/regions.cjs +1 -1
  31. package/dist/plugins/regions.esm.js +1 -1
  32. package/dist/plugins/regions.js +1 -1
  33. package/dist/plugins/regions.min.js +1 -1
  34. package/dist/plugins/timeline.cjs +1 -1
  35. package/dist/plugins/timeline.d.ts +1 -1
  36. package/dist/plugins/timeline.esm.js +1 -1
  37. package/dist/plugins/timeline.js +1 -1
  38. package/dist/plugins/timeline.min.js +1 -1
  39. package/dist/plugins/zoom.cjs +1 -1
  40. package/dist/plugins/zoom.d.ts +10 -1
  41. package/dist/plugins/zoom.esm.js +1 -1
  42. package/dist/plugins/zoom.js +1 -1
  43. package/dist/plugins/zoom.min.js +1 -1
  44. package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
  45. package/dist/reactive/__tests__/drag-stream.test.js +199 -0
  46. package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
  47. package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
  48. package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
  49. package/dist/reactive/__tests__/event-streams.test.js +278 -0
  50. package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
  51. package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
  52. package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
  53. package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
  54. package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
  55. package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
  56. package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
  57. package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
  58. package/dist/reactive/__tests__/store.test.d.ts +1 -0
  59. package/dist/reactive/__tests__/store.test.js +271 -0
  60. package/dist/reactive/drag-stream.d.ts +48 -0
  61. package/dist/reactive/drag-stream.js +146 -0
  62. package/dist/reactive/event-stream-emitter.d.ts +104 -0
  63. package/dist/reactive/event-stream-emitter.js +151 -0
  64. package/dist/reactive/event-streams.d.ts +65 -0
  65. package/dist/reactive/event-streams.js +146 -0
  66. package/dist/reactive/media-event-bridge.d.ts +51 -0
  67. package/dist/reactive/media-event-bridge.js +152 -0
  68. package/dist/reactive/render-scheduler.d.ts +43 -0
  69. package/dist/reactive/render-scheduler.js +75 -0
  70. package/dist/reactive/scroll-stream.d.ts +93 -0
  71. package/dist/reactive/scroll-stream.js +131 -0
  72. package/dist/reactive/state-event-emitter.d.ts +105 -0
  73. package/dist/reactive/state-event-emitter.js +213 -0
  74. package/dist/reactive/store.d.ts +66 -0
  75. package/dist/reactive/store.js +115 -0
  76. package/dist/renderer-utils.d.ts +126 -0
  77. package/dist/renderer-utils.js +252 -0
  78. package/dist/renderer.d.ts +4 -4
  79. package/dist/renderer.js +133 -197
  80. package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
  81. package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
  82. package/dist/state/wavesurfer-state.d.ts +90 -0
  83. package/dist/state/wavesurfer-state.js +124 -0
  84. package/dist/types.d.ts +167 -1
  85. package/dist/wavesurfer.cjs +1 -1
  86. package/dist/wavesurfer.d.ts +18 -0
  87. package/dist/wavesurfer.esm.js +1 -1
  88. package/dist/wavesurfer.js +46 -8
  89. package/dist/wavesurfer.min.js +1 -1
  90. package/dist/webaudio.js +4 -0
  91. package/package.json +6 -6
@@ -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
+ }