@wavv/ui 2.4.15-alpha.2 → 2.4.15-alpha.4
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/build/components/Audio.d.ts +2 -0
- package/build/components/Audio.js +101 -12
- package/package.json +1 -1
|
@@ -46,6 +46,8 @@ type AudioProps = {
|
|
|
46
46
|
playedColor?: string;
|
|
47
47
|
/** Color for unplayed portion of waveform */
|
|
48
48
|
unplayedColor?: string;
|
|
49
|
+
/** Force prefetch into blob for remote URLs to fix seeking (use when server range support is unreliable) */
|
|
50
|
+
forceBlobForSeeking?: boolean;
|
|
49
51
|
};
|
|
50
52
|
ref?: React.Ref<AudioRef>;
|
|
51
53
|
onPlay?: (event: ChangeEvent<HTMLAudioElement>) => void;
|
|
@@ -12,11 +12,18 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
12
12
|
const [totalDuration, setTotalDuration] = useState(0);
|
|
13
13
|
const [displayCurrent, setDisplayCurrent] = useState('');
|
|
14
14
|
const [displayDuration, setDisplayDuration] = useState('0:00');
|
|
15
|
+
const [playbackUrl, setPlaybackUrl] = useState(src);
|
|
15
16
|
const audioRef = useRef(null);
|
|
17
|
+
const objectUrlRef = useRef(null);
|
|
16
18
|
const isSeekingRef = useRef(false);
|
|
17
19
|
const seekedTimeoutRef = useRef(null);
|
|
20
|
+
const lastRequestedSeekSecondsRef = useRef(null);
|
|
21
|
+
const seekVerifyRetryCountRef = useRef(0);
|
|
22
|
+
const seekVerifyTimeoutRef = useRef(null);
|
|
18
23
|
const END_EPSILON_SECONDS = 0.75;
|
|
19
|
-
const
|
|
24
|
+
const SEEK_TOLERANCE_SECONDS = 0.5;
|
|
25
|
+
const MAX_SEEK_VERIFY_RETRIES = 3;
|
|
26
|
+
const { height = 30, showHoverTime = false, playedColor, unplayedColor, forceBlobForSeeking = false } = 'object' == typeof waveform ? waveform : {};
|
|
20
27
|
const play = ()=>{
|
|
21
28
|
const { current: audio } = audioRef;
|
|
22
29
|
if (audio) audio.play().catch(()=>{
|
|
@@ -40,6 +47,7 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
40
47
|
if (isPlaying) pause();
|
|
41
48
|
else {
|
|
42
49
|
if (isNearEnd(progress)) {
|
|
50
|
+
lastRequestedSeekSecondsRef.current = null;
|
|
43
51
|
setProgress(0);
|
|
44
52
|
setDisplayCurrent('0:00');
|
|
45
53
|
const { current: audio } = audioRef;
|
|
@@ -68,6 +76,7 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
68
76
|
if (onPause) onPause(event);
|
|
69
77
|
};
|
|
70
78
|
const handleAudioStop = (event)=>{
|
|
79
|
+
lastRequestedSeekSecondsRef.current = null;
|
|
71
80
|
setIsPlaying(false);
|
|
72
81
|
setIsCleared(true);
|
|
73
82
|
setProgress(0);
|
|
@@ -93,9 +102,54 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
93
102
|
}, [
|
|
94
103
|
isPlaying
|
|
95
104
|
]);
|
|
105
|
+
useEffect(()=>{
|
|
106
|
+
setPlaybackUrl(src);
|
|
107
|
+
if (objectUrlRef.current) {
|
|
108
|
+
try {
|
|
109
|
+
URL.revokeObjectURL(objectUrlRef.current);
|
|
110
|
+
} catch (_e) {}
|
|
111
|
+
objectUrlRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
if (!waveform || !src || !src.startsWith('http')) return;
|
|
114
|
+
let cancelled = false;
|
|
115
|
+
const buildBlobUrl = async ()=>{
|
|
116
|
+
try {
|
|
117
|
+
const resp = await fetch(src, {
|
|
118
|
+
cache: 'force-cache'
|
|
119
|
+
});
|
|
120
|
+
const buf = await resp.arrayBuffer();
|
|
121
|
+
if (cancelled) return;
|
|
122
|
+
const useBlob = forceBlobForSeeking || 'bytes' !== resp.headers.get('accept-ranges');
|
|
123
|
+
if (useBlob) {
|
|
124
|
+
try {
|
|
125
|
+
if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
|
|
126
|
+
} catch (_e) {}
|
|
127
|
+
const blob = new Blob([
|
|
128
|
+
buf
|
|
129
|
+
], {
|
|
130
|
+
type: resp.headers.get('content-type') || 'audio/mpeg'
|
|
131
|
+
});
|
|
132
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
133
|
+
objectUrlRef.current = objectUrl;
|
|
134
|
+
if (!cancelled) setPlaybackUrl(objectUrl);
|
|
135
|
+
}
|
|
136
|
+
} catch (_e) {}
|
|
137
|
+
};
|
|
138
|
+
buildBlobUrl();
|
|
139
|
+
return ()=>{
|
|
140
|
+
cancelled = true;
|
|
141
|
+
try {
|
|
142
|
+
if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
|
|
143
|
+
} catch (_e) {}
|
|
144
|
+
};
|
|
145
|
+
}, [
|
|
146
|
+
src,
|
|
147
|
+
waveform,
|
|
148
|
+
forceBlobForSeeking
|
|
149
|
+
]);
|
|
96
150
|
useEffect(()=>{
|
|
97
151
|
const { current: audio } = audioRef;
|
|
98
|
-
if (audio &&
|
|
152
|
+
if (audio && playbackUrl) {
|
|
99
153
|
audio.load();
|
|
100
154
|
const checkDuration = setTimeout(()=>{
|
|
101
155
|
if (audio.duration && Number.isFinite(audio.duration) && audio.duration > 0 && audio.duration !== 1 / 0) {
|
|
@@ -109,11 +163,12 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
109
163
|
return ()=>clearTimeout(checkDuration);
|
|
110
164
|
}
|
|
111
165
|
}, [
|
|
112
|
-
|
|
166
|
+
playbackUrl,
|
|
113
167
|
fallbackDuration
|
|
114
168
|
]);
|
|
115
169
|
useEffect(()=>()=>{
|
|
116
170
|
if (seekedTimeoutRef.current) clearTimeout(seekedTimeoutRef.current);
|
|
171
|
+
if (seekVerifyTimeoutRef.current) clearTimeout(seekVerifyTimeoutRef.current);
|
|
117
172
|
}, []);
|
|
118
173
|
const formatTime = (time)=>{
|
|
119
174
|
if (!Number.isFinite(time) || time < 0) return '0:00';
|
|
@@ -121,17 +176,41 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
121
176
|
const secs = Math.floor(time % 60);
|
|
122
177
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
123
178
|
};
|
|
179
|
+
const performSeek = (targetSeconds)=>{
|
|
180
|
+
const { current: audio } = audioRef;
|
|
181
|
+
if (!audio || !Number.isFinite(targetSeconds)) return false;
|
|
182
|
+
try {
|
|
183
|
+
audio.currentTime = targetSeconds;
|
|
184
|
+
return true;
|
|
185
|
+
} catch (_e) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
124
189
|
const updateProgress = ()=>{
|
|
125
190
|
if (isSeekingRef.current) return;
|
|
126
191
|
const { current: audio } = audioRef;
|
|
127
|
-
if (audio)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
192
|
+
if (!audio) return;
|
|
193
|
+
const currentTime = audio.currentTime;
|
|
194
|
+
if (!Number.isFinite(currentTime) || currentTime < 0) return;
|
|
195
|
+
const requestedSeek = lastRequestedSeekSecondsRef.current;
|
|
196
|
+
if ('number' == typeof requestedSeek) {
|
|
197
|
+
const drift = Math.abs(currentTime - requestedSeek);
|
|
198
|
+
if (drift > SEEK_TOLERANCE_SECONDS) {
|
|
199
|
+
if (seekVerifyRetryCountRef.current < MAX_SEEK_VERIFY_RETRIES) {
|
|
200
|
+
seekVerifyRetryCountRef.current += 1;
|
|
201
|
+
performSeek(requestedSeek);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
lastRequestedSeekSecondsRef.current = null;
|
|
205
|
+
seekVerifyRetryCountRef.current = 0;
|
|
206
|
+
return;
|
|
133
207
|
}
|
|
208
|
+
lastRequestedSeekSecondsRef.current = null;
|
|
209
|
+
seekVerifyRetryCountRef.current = 0;
|
|
134
210
|
}
|
|
211
|
+
const current = formatTime(currentTime);
|
|
212
|
+
setProgress(currentTime);
|
|
213
|
+
setDisplayCurrent(current);
|
|
135
214
|
};
|
|
136
215
|
const handleLoad = ()=>{
|
|
137
216
|
const { current: audio } = audioRef;
|
|
@@ -169,6 +248,8 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
169
248
|
const maxSeekTarget = Math.max(0, totalDuration - END_EPSILON_SECONDS);
|
|
170
249
|
const clamped = Math.min(Math.max(sliderPosition, 0), maxSeekTarget);
|
|
171
250
|
isSeekingRef.current = true;
|
|
251
|
+
lastRequestedSeekSecondsRef.current = clamped;
|
|
252
|
+
seekVerifyRetryCountRef.current = 0;
|
|
172
253
|
if (seekedTimeoutRef.current) clearTimeout(seekedTimeoutRef.current);
|
|
173
254
|
seekedTimeoutRef.current = setTimeout(()=>{
|
|
174
255
|
isSeekingRef.current = false;
|
|
@@ -176,7 +257,15 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
176
257
|
}, 300);
|
|
177
258
|
setProgress(clamped);
|
|
178
259
|
setDisplayCurrent(formatTime(clamped));
|
|
179
|
-
|
|
260
|
+
performSeek(clamped);
|
|
261
|
+
if (seekVerifyTimeoutRef.current) clearTimeout(seekVerifyTimeoutRef.current);
|
|
262
|
+
seekVerifyTimeoutRef.current = window.setTimeout(()=>{
|
|
263
|
+
seekVerifyTimeoutRef.current = null;
|
|
264
|
+
const { current: audio } = audioRef;
|
|
265
|
+
if (!audio || null === lastRequestedSeekSecondsRef.current) return;
|
|
266
|
+
const ct = audio.currentTime ?? 0;
|
|
267
|
+
if (Math.abs(ct - clamped) > SEEK_TOLERANCE_SECONDS) performSeek(clamped);
|
|
268
|
+
}, 150);
|
|
180
269
|
};
|
|
181
270
|
const hideSlider = isCleared && hideSliderOnStop;
|
|
182
271
|
const hideTime = isCleared && hideTimeOnStop;
|
|
@@ -207,7 +296,7 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
207
296
|
alignData: infoPosition,
|
|
208
297
|
children: [
|
|
209
298
|
!hideSlider && (waveform ? /*#__PURE__*/ jsx(Waveform, {
|
|
210
|
-
src:
|
|
299
|
+
src: playbackUrl,
|
|
211
300
|
value: progress,
|
|
212
301
|
maxValue: totalDuration,
|
|
213
302
|
onChange: handleControlGrab,
|
|
@@ -239,7 +328,7 @@ const Audio = ({ src, title, width, infoPosition, centerAlignContent, small, tin
|
|
|
239
328
|
'center' === infoPosition && timeData,
|
|
240
329
|
/*#__PURE__*/ jsx("audio", {
|
|
241
330
|
ref: audioRef,
|
|
242
|
-
src:
|
|
331
|
+
src: playbackUrl,
|
|
243
332
|
onPlay: handleAudioPlay,
|
|
244
333
|
onPause: handleAudioPause,
|
|
245
334
|
onEmptied: handleAudioStop,
|