react-audio-wavekit 0.1.8 → 0.2.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 +11 -3
- package/dist/index.d.ts +7 -1
- package/dist/waveform/index.cjs +16 -1
- package/dist/waveform/index.js +16 -1
- package/dist/waveform/waveform-renderer.cjs +58 -11
- package/dist/waveform/waveform-renderer.js +58 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Visualize existing audio files (mp3, wav, etc.) with playhead and seek support.
|
|
|
32
32
|
|
|
33
33
|

|
|
34
34
|
|
|
35
|
-
Static waveform visualization with playhead and
|
|
35
|
+
Static waveform visualization with playhead and drag-to-seek.
|
|
36
36
|
|
|
37
37
|
[▶ Demo](https://react-audio-wavekit.netlify.app/?path=/story/waveform-audiowaveform--default)
|
|
38
38
|
|
|
@@ -41,7 +41,12 @@ Static waveform visualization with playhead and click-to-seek.
|
|
|
41
41
|
blob={audioBlob}
|
|
42
42
|
currentTime={currentTime}
|
|
43
43
|
duration={duration}
|
|
44
|
-
|
|
44
|
+
onSeekStart={() => audio.pause()}
|
|
45
|
+
onSeekDrag={(time) => setCurrentTime(time)}
|
|
46
|
+
onSeekEnd={(time) => {
|
|
47
|
+
audio.currentTime = time;
|
|
48
|
+
audio.play();
|
|
49
|
+
}}
|
|
45
50
|
/>
|
|
46
51
|
```
|
|
47
52
|
|
|
@@ -51,7 +56,10 @@ Static waveform visualization with playhead and click-to-seek.
|
|
|
51
56
|
| `peaks` | `number[]` | - | Pre-computed peaks (0-1 range, skips decoding) |
|
|
52
57
|
| `currentTime` | `number` | - | Current playback time in seconds |
|
|
53
58
|
| `duration` | `number` | - | Total audio duration in seconds |
|
|
54
|
-
| `onSeek` | `(time: number) => void` | - | Callback
|
|
59
|
+
| `onSeek` | `(time: number) => void` | - | Callback for simple click-to-seek |
|
|
60
|
+
| `onSeekStart` | `() => void` | - | Callback when drag starts (pause playback) |
|
|
61
|
+
| `onSeekDrag` | `(time: number) => void` | - | Callback during drag (real-time updates) |
|
|
62
|
+
| `onSeekEnd` | `(time: number) => void` | - | Callback when drag ends (resume playback) |
|
|
55
63
|
| `suspense` | `boolean` | `false` | Enable React Suspense mode |
|
|
56
64
|
| `appearance` | `AudioWaveformAppearance` | - | See [Appearance Options](#appearance-options) |
|
|
57
65
|
|
package/dist/index.d.ts
CHANGED
|
@@ -29,8 +29,14 @@ declare interface AudioWaveformProps extends React.CanvasHTMLAttributes<HTMLCanv
|
|
|
29
29
|
currentTime?: number;
|
|
30
30
|
/** Total audio duration in seconds (required for playhead positioning) */
|
|
31
31
|
duration?: number;
|
|
32
|
-
/** Callback when user clicks/seeks on waveform */
|
|
32
|
+
/** Callback when user clicks/seeks on waveform (simple seek) */
|
|
33
33
|
onSeek?: (time: number) => void;
|
|
34
|
+
/** Callback when drag-to-seek starts (use to pause playback) */
|
|
35
|
+
onSeekStart?: () => void;
|
|
36
|
+
/** Callback during drag-to-seek with current time (real-time updates) */
|
|
37
|
+
onSeekDrag?: (time: number) => void;
|
|
38
|
+
/** Callback when drag-to-seek ends (use to resume playback) */
|
|
39
|
+
onSeekEnd?: (time: number) => void;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
declare interface AudioWaveformRef {
|
package/dist/waveform/index.cjs
CHANGED
|
@@ -9,7 +9,19 @@ const getInitialSampleCount = () => {
|
|
|
9
9
|
if (typeof window === "undefined") return 500;
|
|
10
10
|
return Math.max(500, Math.ceil(window.innerWidth));
|
|
11
11
|
};
|
|
12
|
-
const AudioWaveform = react.forwardRef(function AudioWaveform2({
|
|
12
|
+
const AudioWaveform = react.forwardRef(function AudioWaveform2({
|
|
13
|
+
blob,
|
|
14
|
+
peaks: precomputedPeaks,
|
|
15
|
+
appearance,
|
|
16
|
+
suspense = false,
|
|
17
|
+
currentTime,
|
|
18
|
+
duration,
|
|
19
|
+
onSeek,
|
|
20
|
+
onSeekStart,
|
|
21
|
+
onSeekDrag,
|
|
22
|
+
onSeekEnd,
|
|
23
|
+
...props
|
|
24
|
+
}, ref) {
|
|
13
25
|
const [decodedPeaks, setDecodedPeaks] = react.useState(null);
|
|
14
26
|
const [error, setError] = react.useState(null);
|
|
15
27
|
const blobRef = react.useRef(null);
|
|
@@ -63,6 +75,9 @@ const AudioWaveform = react.forwardRef(function AudioWaveform2({ blob, peaks: pr
|
|
|
63
75
|
currentTime,
|
|
64
76
|
duration,
|
|
65
77
|
onSeek,
|
|
78
|
+
onSeekStart,
|
|
79
|
+
onSeekDrag,
|
|
80
|
+
onSeekEnd,
|
|
66
81
|
...props
|
|
67
82
|
}
|
|
68
83
|
);
|
package/dist/waveform/index.js
CHANGED
|
@@ -7,7 +7,19 @@ const getInitialSampleCount = () => {
|
|
|
7
7
|
if (typeof window === "undefined") return 500;
|
|
8
8
|
return Math.max(500, Math.ceil(window.innerWidth));
|
|
9
9
|
};
|
|
10
|
-
const AudioWaveform = forwardRef(function AudioWaveform2({
|
|
10
|
+
const AudioWaveform = forwardRef(function AudioWaveform2({
|
|
11
|
+
blob,
|
|
12
|
+
peaks: precomputedPeaks,
|
|
13
|
+
appearance,
|
|
14
|
+
suspense = false,
|
|
15
|
+
currentTime,
|
|
16
|
+
duration,
|
|
17
|
+
onSeek,
|
|
18
|
+
onSeekStart,
|
|
19
|
+
onSeekDrag,
|
|
20
|
+
onSeekEnd,
|
|
21
|
+
...props
|
|
22
|
+
}, ref) {
|
|
11
23
|
const [decodedPeaks, setDecodedPeaks] = useState(null);
|
|
12
24
|
const [error, setError] = useState(null);
|
|
13
25
|
const blobRef = useRef(null);
|
|
@@ -61,6 +73,9 @@ const AudioWaveform = forwardRef(function AudioWaveform2({ blob, peaks: precompu
|
|
|
61
73
|
currentTime,
|
|
62
74
|
duration,
|
|
63
75
|
onSeek,
|
|
76
|
+
onSeekStart,
|
|
77
|
+
onSeekDrag,
|
|
78
|
+
onSeekEnd,
|
|
64
79
|
...props
|
|
65
80
|
}
|
|
66
81
|
);
|
|
@@ -3,10 +3,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
4
|
const react = require("react");
|
|
5
5
|
const constants = require("../constants.cjs");
|
|
6
|
-
const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onClick, style, ...props }, ref) {
|
|
6
|
+
const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onSeekStart, onSeekDrag, onSeekEnd, onClick, style, ...props }, ref) {
|
|
7
7
|
const canvasRef = react.useRef(null);
|
|
8
8
|
const sizeRef = react.useRef({ width: 0, height: 0 });
|
|
9
9
|
const rafRef = react.useRef(0);
|
|
10
|
+
const isDraggingRef = react.useRef(false);
|
|
10
11
|
react.useImperativeHandle(ref, () => ({
|
|
11
12
|
canvas: canvasRef.current
|
|
12
13
|
}));
|
|
@@ -77,18 +78,62 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
|
|
|
77
78
|
react.useEffect(() => {
|
|
78
79
|
drawWaveform();
|
|
79
80
|
}, [drawWaveform]);
|
|
81
|
+
const getTimeFromPosition = react.useCallback(
|
|
82
|
+
(clientX) => {
|
|
83
|
+
const canvas = canvasRef.current;
|
|
84
|
+
if (!canvas || !duration || duration <= 0) return 0;
|
|
85
|
+
const rect = canvas.getBoundingClientRect();
|
|
86
|
+
const x = clientX - rect.left;
|
|
87
|
+
const ratio = Math.max(0, Math.min(x / rect.width, 1));
|
|
88
|
+
return ratio * duration;
|
|
89
|
+
},
|
|
90
|
+
[duration]
|
|
91
|
+
);
|
|
92
|
+
react.useEffect(() => {
|
|
93
|
+
const handleMouseMove = (e) => {
|
|
94
|
+
if (!isDraggingRef.current) return;
|
|
95
|
+
const time = getTimeFromPosition(e.clientX);
|
|
96
|
+
onSeekDrag?.(time);
|
|
97
|
+
};
|
|
98
|
+
const handleMouseUp = (e) => {
|
|
99
|
+
if (!isDraggingRef.current) return;
|
|
100
|
+
isDraggingRef.current = false;
|
|
101
|
+
document.body.style.cursor = "";
|
|
102
|
+
document.body.style.userSelect = "";
|
|
103
|
+
const time = getTimeFromPosition(e.clientX);
|
|
104
|
+
onSeekEnd?.(time);
|
|
105
|
+
};
|
|
106
|
+
if (onSeekDrag || onSeekEnd) {
|
|
107
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
108
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
109
|
+
}
|
|
110
|
+
return () => {
|
|
111
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
112
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
113
|
+
};
|
|
114
|
+
}, [getTimeFromPosition, onSeekDrag, onSeekEnd]);
|
|
115
|
+
const handleMouseDown = react.useCallback(
|
|
116
|
+
(e) => {
|
|
117
|
+
if (!duration || duration <= 0) return;
|
|
118
|
+
if (onSeekStart || onSeekDrag || onSeekEnd) {
|
|
119
|
+
isDraggingRef.current = true;
|
|
120
|
+
document.body.style.cursor = "grabbing";
|
|
121
|
+
document.body.style.userSelect = "none";
|
|
122
|
+
onSeekStart?.();
|
|
123
|
+
const time = getTimeFromPosition(e.clientX);
|
|
124
|
+
onSeekDrag?.(time);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
|
|
128
|
+
);
|
|
80
129
|
const handleClick = react.useCallback(
|
|
81
130
|
(e) => {
|
|
131
|
+
if (onSeekStart || onSeekDrag || onSeekEnd) return;
|
|
82
132
|
if (!onSeek || !duration || duration <= 0) return;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const rect = canvas.getBoundingClientRect();
|
|
86
|
-
const x = e.clientX - rect.left;
|
|
87
|
-
const clickRatio = x / rect.width;
|
|
88
|
-
const newTime = Math.max(0, Math.min(clickRatio * duration, duration));
|
|
89
|
-
onSeek(newTime);
|
|
133
|
+
const time = getTimeFromPosition(e.clientX);
|
|
134
|
+
onSeek(time);
|
|
90
135
|
},
|
|
91
|
-
[onSeek, duration]
|
|
136
|
+
[onSeek, duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
|
|
92
137
|
);
|
|
93
138
|
const handleKeyDown = react.useCallback(
|
|
94
139
|
(e) => {
|
|
@@ -124,7 +169,8 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
|
|
|
124
169
|
}
|
|
125
170
|
return `${secs}초`;
|
|
126
171
|
};
|
|
127
|
-
const isInteractive = !!onSeek && !!duration && duration > 0;
|
|
172
|
+
const isInteractive = (!!onSeek || !!onSeekStart || !!onSeekDrag || !!onSeekEnd) && !!duration && duration > 0;
|
|
173
|
+
const isDragEnabled = !!onSeekStart || !!onSeekDrag || !!onSeekEnd;
|
|
128
174
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
129
175
|
"canvas",
|
|
130
176
|
{
|
|
@@ -136,9 +182,10 @@ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, ap
|
|
|
136
182
|
"aria-valuenow": isInteractive ? Math.floor(currentTime ?? 0) : void 0,
|
|
137
183
|
"aria-valuetext": isInteractive ? `${formatTimeForScreen(currentTime ?? 0)} / ${formatTimeForScreen(duration)}` : void 0,
|
|
138
184
|
tabIndex: isInteractive ? 0 : -1,
|
|
185
|
+
onMouseDown: isDragEnabled ? handleMouseDown : void 0,
|
|
139
186
|
onClick: handleClick,
|
|
140
187
|
onKeyDown: isInteractive ? handleKeyDown : void 0,
|
|
141
|
-
style: { cursor: isInteractive ? "pointer" : void 0, ...style },
|
|
188
|
+
style: { cursor: isInteractive ? isDragEnabled ? "grab" : "pointer" : void 0, ...style },
|
|
142
189
|
...props
|
|
143
190
|
}
|
|
144
191
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect } from "react";
|
|
3
3
|
import { DEFAULT_WAVEFORM_APPEARANCE, DEFAULT_PLAYHEAD_APPEARANCE } from "../constants.js";
|
|
4
|
-
const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onClick, style, ...props }, ref) {
|
|
4
|
+
const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onSeekStart, onSeekDrag, onSeekEnd, onClick, style, ...props }, ref) {
|
|
5
5
|
const canvasRef = useRef(null);
|
|
6
6
|
const sizeRef = useRef({ width: 0, height: 0 });
|
|
7
7
|
const rafRef = useRef(0);
|
|
8
|
+
const isDraggingRef = useRef(false);
|
|
8
9
|
useImperativeHandle(ref, () => ({
|
|
9
10
|
canvas: canvasRef.current
|
|
10
11
|
}));
|
|
@@ -75,18 +76,62 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
|
|
|
75
76
|
useEffect(() => {
|
|
76
77
|
drawWaveform();
|
|
77
78
|
}, [drawWaveform]);
|
|
79
|
+
const getTimeFromPosition = useCallback(
|
|
80
|
+
(clientX) => {
|
|
81
|
+
const canvas = canvasRef.current;
|
|
82
|
+
if (!canvas || !duration || duration <= 0) return 0;
|
|
83
|
+
const rect = canvas.getBoundingClientRect();
|
|
84
|
+
const x = clientX - rect.left;
|
|
85
|
+
const ratio = Math.max(0, Math.min(x / rect.width, 1));
|
|
86
|
+
return ratio * duration;
|
|
87
|
+
},
|
|
88
|
+
[duration]
|
|
89
|
+
);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const handleMouseMove = (e) => {
|
|
92
|
+
if (!isDraggingRef.current) return;
|
|
93
|
+
const time = getTimeFromPosition(e.clientX);
|
|
94
|
+
onSeekDrag?.(time);
|
|
95
|
+
};
|
|
96
|
+
const handleMouseUp = (e) => {
|
|
97
|
+
if (!isDraggingRef.current) return;
|
|
98
|
+
isDraggingRef.current = false;
|
|
99
|
+
document.body.style.cursor = "";
|
|
100
|
+
document.body.style.userSelect = "";
|
|
101
|
+
const time = getTimeFromPosition(e.clientX);
|
|
102
|
+
onSeekEnd?.(time);
|
|
103
|
+
};
|
|
104
|
+
if (onSeekDrag || onSeekEnd) {
|
|
105
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
106
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
107
|
+
}
|
|
108
|
+
return () => {
|
|
109
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
110
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
111
|
+
};
|
|
112
|
+
}, [getTimeFromPosition, onSeekDrag, onSeekEnd]);
|
|
113
|
+
const handleMouseDown = useCallback(
|
|
114
|
+
(e) => {
|
|
115
|
+
if (!duration || duration <= 0) return;
|
|
116
|
+
if (onSeekStart || onSeekDrag || onSeekEnd) {
|
|
117
|
+
isDraggingRef.current = true;
|
|
118
|
+
document.body.style.cursor = "grabbing";
|
|
119
|
+
document.body.style.userSelect = "none";
|
|
120
|
+
onSeekStart?.();
|
|
121
|
+
const time = getTimeFromPosition(e.clientX);
|
|
122
|
+
onSeekDrag?.(time);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
|
|
126
|
+
);
|
|
78
127
|
const handleClick = useCallback(
|
|
79
128
|
(e) => {
|
|
129
|
+
if (onSeekStart || onSeekDrag || onSeekEnd) return;
|
|
80
130
|
if (!onSeek || !duration || duration <= 0) return;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const rect = canvas.getBoundingClientRect();
|
|
84
|
-
const x = e.clientX - rect.left;
|
|
85
|
-
const clickRatio = x / rect.width;
|
|
86
|
-
const newTime = Math.max(0, Math.min(clickRatio * duration, duration));
|
|
87
|
-
onSeek(newTime);
|
|
131
|
+
const time = getTimeFromPosition(e.clientX);
|
|
132
|
+
onSeek(time);
|
|
88
133
|
},
|
|
89
|
-
[onSeek, duration]
|
|
134
|
+
[onSeek, duration, onSeekStart, onSeekDrag, onSeekEnd, getTimeFromPosition]
|
|
90
135
|
);
|
|
91
136
|
const handleKeyDown = useCallback(
|
|
92
137
|
(e) => {
|
|
@@ -122,7 +167,8 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
|
|
|
122
167
|
}
|
|
123
168
|
return `${secs}초`;
|
|
124
169
|
};
|
|
125
|
-
const isInteractive = !!onSeek && !!duration && duration > 0;
|
|
170
|
+
const isInteractive = (!!onSeek || !!onSeekStart || !!onSeekDrag || !!onSeekEnd) && !!duration && duration > 0;
|
|
171
|
+
const isDragEnabled = !!onSeekStart || !!onSeekDrag || !!onSeekEnd;
|
|
126
172
|
return /* @__PURE__ */ jsx(
|
|
127
173
|
"canvas",
|
|
128
174
|
{
|
|
@@ -134,9 +180,10 @@ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearan
|
|
|
134
180
|
"aria-valuenow": isInteractive ? Math.floor(currentTime ?? 0) : void 0,
|
|
135
181
|
"aria-valuetext": isInteractive ? `${formatTimeForScreen(currentTime ?? 0)} / ${formatTimeForScreen(duration)}` : void 0,
|
|
136
182
|
tabIndex: isInteractive ? 0 : -1,
|
|
183
|
+
onMouseDown: isDragEnabled ? handleMouseDown : void 0,
|
|
137
184
|
onClick: handleClick,
|
|
138
185
|
onKeyDown: isInteractive ? handleKeyDown : void 0,
|
|
139
|
-
style: { cursor: isInteractive ? "pointer" : void 0, ...style },
|
|
186
|
+
style: { cursor: isInteractive ? isDragEnabled ? "grab" : "pointer" : void 0, ...style },
|
|
140
187
|
...props
|
|
141
188
|
}
|
|
142
189
|
);
|