@exode-team/react-recorder 1.1.12 → 1.1.15
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 +2 -2
- package/dist/components/AudioRecorder.js +50 -0
- package/dist/components/AudioRecorder.js.map +1 -0
- package/dist/components/AudioRecorder.messages.js +47 -0
- package/dist/components/AudioRecorder.messages.js.map +1 -0
- package/dist/components/AudioRecorder.styled.js +16 -0
- package/dist/components/AudioRecorder.styled.js.map +1 -0
- package/dist/components/IdleControl/IdleControl.js +12 -0
- package/dist/components/IdleControl/IdleControl.js.map +1 -0
- package/dist/components/IdleControl/IdleControl.styled.js +68 -0
- package/dist/components/IdleControl/IdleControl.styled.js.map +1 -0
- package/dist/components/RecordingShell/RecordingShell.js +80 -0
- package/dist/components/RecordingShell/RecordingShell.js.map +1 -0
- package/dist/components/RecordingShell/RecordingShell.styled.js +159 -0
- package/dist/components/RecordingShell/RecordingShell.styled.js.map +1 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js +151 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js.map +1 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js +92 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js.map +1 -0
- package/dist/components/VoiceRecorderInput/PreviewBars.js +52 -0
- package/dist/components/VoiceRecorderInput/PreviewBars.js.map +1 -0
- package/dist/components/VoiceRecorderInput/TimelineBars.js +31 -0
- package/dist/components/VoiceRecorderInput/TimelineBars.js.map +1 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js +51 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js.map +1 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js +175 -0
- package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js.map +1 -0
- package/dist/components/WaveformBars/WaveformBars.js +29 -0
- package/dist/components/WaveformBars/WaveformBars.js.map +1 -0
- package/dist/components/WaveformBars/WaveformBars.styled.js +20 -0
- package/dist/components/WaveformBars/WaveformBars.styled.js.map +1 -0
- package/dist/constants.js +17 -0
- package/dist/constants.js.map +1 -0
- package/dist/hooks/useAudioBlobLoader.d.ts +1 -5
- package/dist/hooks/useAudioBlobLoader.d.ts.map +1 -1
- package/dist/hooks/useAudioBlobLoader.js +175 -0
- package/dist/hooks/useAudioBlobLoader.js.map +1 -0
- package/dist/hooks/useAudioPlayback.js +119 -0
- package/dist/hooks/useAudioPlayback.js.map +1 -0
- package/dist/hooks/useAudioRecorderHandlers.js +137 -0
- package/dist/hooks/useAudioRecorderHandlers.js.map +1 -0
- package/dist/hooks/useMediaRecorder.js +127 -0
- package/dist/hooks/useMediaRecorder.js.map +1 -0
- package/dist/hooks/useRecorderVisualization.js +94 -0
- package/dist/hooks/useRecorderVisualization.js.map +1 -0
- package/dist/hooks/useRecordingSender.js +53 -0
- package/dist/hooks/useRecordingSender.js.map +1 -0
- package/dist/hooks/useRecordingTiming.js +93 -0
- package/dist/hooks/useRecordingTiming.js.map +1 -0
- package/dist/hooks/useVoiceMessagePlayer.js +177 -0
- package/dist/hooks/useVoiceMessagePlayer.js.map +1 -0
- package/dist/hooks/useVoiceRecorder.js +153 -0
- package/dist/hooks/useVoiceRecorder.js.map +1 -0
- package/dist/hooks/useVoiceRecorderInput.js +163 -0
- package/dist/hooks/useVoiceRecorderInput.js.map +1 -0
- package/dist/index.cjs +108 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -2612
- package/dist/index.js.map +1 -1
- package/dist/platform/animation-frame-adapter.js +14 -0
- package/dist/platform/animation-frame-adapter.js.map +1 -0
- package/dist/platform/audio-analyzer-adapter.d.ts.map +1 -1
- package/dist/platform/audio-analyzer-adapter.js +74 -0
- package/dist/platform/audio-analyzer-adapter.js.map +1 -0
- package/dist/platform/audio-blob-fetcher.js +19 -0
- package/dist/platform/audio-blob-fetcher.js.map +1 -0
- package/dist/platform/audio-playback-adapter.d.ts.map +1 -1
- package/dist/platform/audio-playback-adapter.js +114 -0
- package/dist/platform/audio-playback-adapter.js.map +1 -0
- package/dist/platform/audio-playback-manager.js +26 -0
- package/dist/platform/audio-playback-manager.js.map +1 -0
- package/dist/platform/media-recorder-adapter.js +23 -0
- package/dist/platform/media-recorder-adapter.js.map +1 -0
- package/dist/platform/microphone.js +24 -0
- package/dist/platform/microphone.js.map +1 -0
- package/dist/platform/mpeg-frame.js +60 -0
- package/dist/platform/mpeg-frame.js.map +1 -0
- package/dist/utils/format.js +13 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/math.js +16 -0
- package/dist/utils/math.js.map +1 -0
- package/dist/utils/pseudo-waveform.d.ts +12 -0
- package/dist/utils/pseudo-waveform.d.ts.map +1 -0
- package/dist/utils/pseudo-waveform.js +40 -0
- package/dist/utils/pseudo-waveform.js.map +1 -0
- package/dist/utils/voice-blob-cache.js +35 -0
- package/dist/utils/voice-blob-cache.js.map +1 -0
- package/dist/utils/voice-playback-chain.js +42 -0
- package/dist/utils/voice-playback-chain.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { requestMicrophoneAccess, classifyMicError } from '../platform/microphone.js';
|
|
3
|
+
import { stopStream, createRecorderSession } from '../platform/media-recorder-adapter.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* useMediaRecorder
|
|
7
|
+
*
|
|
8
|
+
* @author: exode <hello@exode.ru>
|
|
9
|
+
*/
|
|
10
|
+
const useMediaRecorder = (callbacks) => {
|
|
11
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
12
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
13
|
+
const mediaRecorderRef = useRef(null);
|
|
14
|
+
const mediaStreamRef = useRef(null);
|
|
15
|
+
const audioChunksRef = useRef([]);
|
|
16
|
+
const discardRef = useRef(false);
|
|
17
|
+
const needsPreviewRef = useRef(false);
|
|
18
|
+
const cleanupStream = useCallback(() => {
|
|
19
|
+
if (mediaStreamRef.current) {
|
|
20
|
+
stopStream(mediaStreamRef.current);
|
|
21
|
+
mediaStreamRef.current = null;
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
const startRecording = useCallback(async (mimeType) => {
|
|
25
|
+
try {
|
|
26
|
+
const stream = await requestMicrophoneAccess();
|
|
27
|
+
mediaStreamRef.current = stream;
|
|
28
|
+
callbacks.onStreamAcquired(stream);
|
|
29
|
+
audioChunksRef.current = [];
|
|
30
|
+
discardRef.current = false;
|
|
31
|
+
const { recorder } = createRecorderSession(stream, {
|
|
32
|
+
onDataAvailable: (event) => {
|
|
33
|
+
if (event.data.size > 0) {
|
|
34
|
+
audioChunksRef.current.push(event.data);
|
|
35
|
+
callbacks.onDataChunk(event.data);
|
|
36
|
+
}
|
|
37
|
+
if (needsPreviewRef.current) {
|
|
38
|
+
callbacks.onNeedsPreview(audioChunksRef.current);
|
|
39
|
+
needsPreviewRef.current = false;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onStop: () => {
|
|
43
|
+
const discarded = discardRef.current;
|
|
44
|
+
discardRef.current = false;
|
|
45
|
+
mediaRecorderRef.current = null;
|
|
46
|
+
cleanupStream();
|
|
47
|
+
setIsRecording(false);
|
|
48
|
+
setIsPaused(false);
|
|
49
|
+
callbacks.onStopped(audioChunksRef.current, discarded);
|
|
50
|
+
},
|
|
51
|
+
onPause: () => {
|
|
52
|
+
setIsPaused(true);
|
|
53
|
+
callbacks.onPaused();
|
|
54
|
+
},
|
|
55
|
+
onResume: () => {
|
|
56
|
+
setIsPaused(false);
|
|
57
|
+
needsPreviewRef.current = false;
|
|
58
|
+
callbacks.onResumed();
|
|
59
|
+
},
|
|
60
|
+
}, { mimeType });
|
|
61
|
+
mediaRecorderRef.current = recorder;
|
|
62
|
+
recorder.start();
|
|
63
|
+
setIsRecording(true);
|
|
64
|
+
setIsPaused(false);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error('[audio-recorder] Error accessing microphone:', error);
|
|
68
|
+
const kind = classifyMicError(error);
|
|
69
|
+
if (kind === 'permission') {
|
|
70
|
+
callbacks.onPermissionDenied(error);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
callbacks.onError(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, [callbacks, cleanupStream]);
|
|
77
|
+
const stopRecording = useCallback(() => {
|
|
78
|
+
if (!mediaRecorderRef.current || !isRecording) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (mediaRecorderRef.current.state === 'paused') {
|
|
82
|
+
mediaRecorderRef.current.resume();
|
|
83
|
+
}
|
|
84
|
+
mediaRecorderRef.current.stop();
|
|
85
|
+
}, [isRecording]);
|
|
86
|
+
const cancelRecording = useCallback(() => {
|
|
87
|
+
if (!mediaRecorderRef.current) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
discardRef.current = true;
|
|
91
|
+
needsPreviewRef.current = false;
|
|
92
|
+
if (mediaRecorderRef.current.state !== 'inactive') {
|
|
93
|
+
mediaRecorderRef.current.stop();
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
const togglePause = useCallback(() => {
|
|
97
|
+
if (!mediaRecorderRef.current || !isRecording) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (mediaRecorderRef.current.state === 'recording') {
|
|
101
|
+
needsPreviewRef.current = true;
|
|
102
|
+
try {
|
|
103
|
+
mediaRecorderRef.current.requestData();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.warn('[audio-recorder] requestData failed', error);
|
|
107
|
+
}
|
|
108
|
+
mediaRecorderRef.current.pause();
|
|
109
|
+
}
|
|
110
|
+
else if (mediaRecorderRef.current.state === 'paused') {
|
|
111
|
+
needsPreviewRef.current = false;
|
|
112
|
+
mediaRecorderRef.current.resume();
|
|
113
|
+
}
|
|
114
|
+
}, [isRecording]);
|
|
115
|
+
return {
|
|
116
|
+
isPaused,
|
|
117
|
+
isRecording,
|
|
118
|
+
togglePause,
|
|
119
|
+
cleanupStream,
|
|
120
|
+
stopRecording,
|
|
121
|
+
startRecording,
|
|
122
|
+
cancelRecording,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export { useMediaRecorder };
|
|
127
|
+
//# sourceMappingURL=useMediaRecorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMediaRecorder.js","sources":["../../src/hooks/useMediaRecorder.ts"],"sourcesContent":["/**\n * useMediaRecorder\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback, useRef, useState } from 'react';\n\nimport { classifyMicError, createRecorderSession, requestMicrophoneAccess, stopStream } from '../platform';\n\nimport type { MediaRecorderCallbacks } from '../types';\n\n\ninterface UseMediaRecorderResult {\n isRecording: boolean;\n isPaused: boolean;\n startRecording: (mimeType: string) => Promise<void>;\n stopRecording: () => void;\n cancelRecording: () => void;\n togglePause: () => void;\n cleanupStream: () => void;\n}\n\nconst useMediaRecorder = (callbacks: MediaRecorderCallbacks): UseMediaRecorderResult => {\n\n const [ isRecording, setIsRecording ] = useState(false);\n const [ isPaused, setIsPaused ] = useState(false);\n\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const mediaStreamRef = useRef<MediaStream | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const discardRef = useRef(false);\n const needsPreviewRef = useRef(false);\n\n const cleanupStream = useCallback(() => {\n if (mediaStreamRef.current) {\n stopStream(mediaStreamRef.current);\n mediaStreamRef.current = null;\n }\n }, []);\n\n const startRecording = useCallback(async (mimeType: string) => {\n try {\n const stream = await requestMicrophoneAccess();\n\n mediaStreamRef.current = stream;\n callbacks.onStreamAcquired(stream);\n\n audioChunksRef.current = [];\n discardRef.current = false;\n\n const { recorder } = createRecorderSession(stream, {\n onDataAvailable: (event) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n callbacks.onDataChunk(event.data);\n }\n\n if (needsPreviewRef.current) {\n callbacks.onNeedsPreview(audioChunksRef.current);\n needsPreviewRef.current = false;\n }\n },\n onStop: () => {\n const discarded = discardRef.current;\n\n discardRef.current = false;\n mediaRecorderRef.current = null;\n cleanupStream();\n setIsRecording(false);\n setIsPaused(false);\n\n callbacks.onStopped(audioChunksRef.current, discarded);\n },\n onPause: () => {\n setIsPaused(true);\n callbacks.onPaused();\n },\n onResume: () => {\n setIsPaused(false);\n needsPreviewRef.current = false;\n callbacks.onResumed();\n },\n }, { mimeType });\n\n mediaRecorderRef.current = recorder;\n recorder.start();\n setIsRecording(true);\n setIsPaused(false);\n } catch (error) {\n console.error('[audio-recorder] Error accessing microphone:', error);\n\n const kind = classifyMicError(error);\n\n if (kind === 'permission') {\n callbacks.onPermissionDenied(error);\n } else {\n callbacks.onError(error);\n }\n }\n }, [ callbacks, cleanupStream ]);\n\n const stopRecording = useCallback(() => {\n if (!mediaRecorderRef.current || !isRecording) {\n return;\n }\n\n if (mediaRecorderRef.current.state === 'paused') {\n mediaRecorderRef.current.resume();\n }\n\n mediaRecorderRef.current.stop();\n }, [ isRecording ]);\n\n const cancelRecording = useCallback(() => {\n if (!mediaRecorderRef.current) {\n return;\n }\n\n discardRef.current = true;\n needsPreviewRef.current = false;\n\n if (mediaRecorderRef.current.state !== 'inactive') {\n mediaRecorderRef.current.stop();\n }\n }, []);\n\n const togglePause = useCallback(() => {\n if (!mediaRecorderRef.current || !isRecording) {\n return;\n }\n\n if (mediaRecorderRef.current.state === 'recording') {\n needsPreviewRef.current = true;\n\n try {\n mediaRecorderRef.current.requestData();\n } catch (error) {\n console.warn('[audio-recorder] requestData failed', error);\n }\n\n mediaRecorderRef.current.pause();\n } else if (mediaRecorderRef.current.state === 'paused') {\n needsPreviewRef.current = false;\n mediaRecorderRef.current.resume();\n }\n }, [ isRecording ]);\n\n return {\n isPaused,\n isRecording,\n togglePause,\n cleanupStream,\n stopRecording,\n startRecording,\n cancelRecording,\n };\n};\n\n\nexport { useMediaRecorder };\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { createInitialLevels } from '../constants.js';
|
|
3
|
+
import { createStreamAnalyzer, decodeAudioLevels, readFrequencyLevels } from '../platform/audio-analyzer-adapter.js';
|
|
4
|
+
import { cancelFrame, scheduleFrame } from '../platform/animation-frame-adapter.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* useRecorderVisualization
|
|
8
|
+
*
|
|
9
|
+
* @author: exode <hello@exode.ru>
|
|
10
|
+
*/
|
|
11
|
+
const useRecorderVisualization = () => {
|
|
12
|
+
const [audioLevels, setAudioLevels] = useState(createInitialLevels);
|
|
13
|
+
const [previewLevels, setPreviewLevels] = useState(createInitialLevels);
|
|
14
|
+
const analyzerRef = useRef(null);
|
|
15
|
+
const animationFrameRef = useRef(null);
|
|
16
|
+
const resetAudioLevels = useCallback(() => {
|
|
17
|
+
setAudioLevels(createInitialLevels);
|
|
18
|
+
}, []);
|
|
19
|
+
const resetPreviewLevels = useCallback(() => {
|
|
20
|
+
setPreviewLevels(createInitialLevels);
|
|
21
|
+
}, []);
|
|
22
|
+
const stopVisualization = useCallback((options) => {
|
|
23
|
+
var _a, _b;
|
|
24
|
+
if (animationFrameRef.current) {
|
|
25
|
+
cancelFrame(animationFrameRef.current);
|
|
26
|
+
animationFrameRef.current = null;
|
|
27
|
+
}
|
|
28
|
+
if (((_a = options === null || options === void 0 ? void 0 : options.closeContext) !== null && _a !== void 0 ? _a : true) && analyzerRef.current) {
|
|
29
|
+
void analyzerRef.current.context.close();
|
|
30
|
+
analyzerRef.current = null;
|
|
31
|
+
}
|
|
32
|
+
if ((_b = options === null || options === void 0 ? void 0 : options.resetLevels) !== null && _b !== void 0 ? _b : true) {
|
|
33
|
+
resetAudioLevels();
|
|
34
|
+
}
|
|
35
|
+
}, [resetAudioLevels]);
|
|
36
|
+
const visualize = useCallback(() => {
|
|
37
|
+
if (!analyzerRef.current) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { analyser } = analyzerRef.current;
|
|
41
|
+
const draw = () => {
|
|
42
|
+
animationFrameRef.current = scheduleFrame(draw);
|
|
43
|
+
const levels = readFrequencyLevels(analyser);
|
|
44
|
+
setAudioLevels(levels);
|
|
45
|
+
};
|
|
46
|
+
draw();
|
|
47
|
+
}, []);
|
|
48
|
+
const resumeVisualization = useCallback(() => {
|
|
49
|
+
if (analyzerRef.current && !animationFrameRef.current) {
|
|
50
|
+
visualize();
|
|
51
|
+
}
|
|
52
|
+
}, [visualize]);
|
|
53
|
+
const initializeVisualization = useCallback((stream) => {
|
|
54
|
+
analyzerRef.current = createStreamAnalyzer(stream);
|
|
55
|
+
visualize();
|
|
56
|
+
}, [visualize]);
|
|
57
|
+
const generatePreviewLevels = useCallback(async (blob) => {
|
|
58
|
+
try {
|
|
59
|
+
const levels = await decodeAudioLevels(blob);
|
|
60
|
+
setAudioLevels(levels);
|
|
61
|
+
setPreviewLevels(levels);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.warn('[audio-recorder] Failed to build waveform preview', error);
|
|
65
|
+
setPreviewLevels(createInitialLevels());
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
const cleanupVisualization = useCallback(() => {
|
|
69
|
+
if (animationFrameRef.current) {
|
|
70
|
+
cancelFrame(animationFrameRef.current);
|
|
71
|
+
}
|
|
72
|
+
animationFrameRef.current = null;
|
|
73
|
+
if (analyzerRef.current) {
|
|
74
|
+
void analyzerRef.current.context.close();
|
|
75
|
+
analyzerRef.current = null;
|
|
76
|
+
}
|
|
77
|
+
resetAudioLevels();
|
|
78
|
+
resetPreviewLevels();
|
|
79
|
+
}, [resetAudioLevels, resetPreviewLevels]);
|
|
80
|
+
return {
|
|
81
|
+
audioLevels,
|
|
82
|
+
previewLevels,
|
|
83
|
+
resetAudioLevels,
|
|
84
|
+
stopVisualization,
|
|
85
|
+
resetPreviewLevels,
|
|
86
|
+
resumeVisualization,
|
|
87
|
+
cleanupVisualization,
|
|
88
|
+
generatePreviewLevels,
|
|
89
|
+
initializeVisualization,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export { useRecorderVisualization };
|
|
94
|
+
//# sourceMappingURL=useRecorderVisualization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRecorderVisualization.js","sources":["../../src/hooks/useRecorderVisualization.ts"],"sourcesContent":["/**\n * useRecorderVisualization\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback, useRef, useState } from 'react';\n\nimport { createInitialLevels } from '../constants';\n\nimport type { StreamAnalyzer } from '../platform';\nimport { cancelFrame, createStreamAnalyzer, decodeAudioLevels, readFrequencyLevels, scheduleFrame } from '../platform';\n\n\ninterface UseRecorderVisualizationResult {\n audioLevels: number[];\n previewLevels: number[];\n resetAudioLevels: () => void;\n resetPreviewLevels: () => void;\n resumeVisualization: () => void;\n cleanupVisualization: () => void;\n generatePreviewLevels: (blob: Blob) => Promise<void>;\n initializeVisualization: (stream: MediaStream) => void;\n stopVisualization: (options?: { resetLevels?: boolean; closeContext?: boolean }) => void;\n}\n\n\nconst useRecorderVisualization = (): UseRecorderVisualizationResult => {\n\n const [ audioLevels, setAudioLevels ] = useState<number[]>(createInitialLevels);\n const [ previewLevels, setPreviewLevels ] = useState<number[]>(createInitialLevels);\n\n const analyzerRef = useRef<StreamAnalyzer | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n const resetAudioLevels = useCallback(() => {\n setAudioLevels(createInitialLevels);\n }, []);\n\n const resetPreviewLevels = useCallback(() => {\n setPreviewLevels(createInitialLevels);\n }, []);\n\n const stopVisualization = useCallback(\n (options?: { resetLevels?: boolean; closeContext?: boolean }) => {\n if (animationFrameRef.current) {\n cancelFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n if ((options?.closeContext ?? true) && analyzerRef.current) {\n void analyzerRef.current.context.close();\n analyzerRef.current = null;\n }\n\n if (options?.resetLevels ?? true) {\n resetAudioLevels();\n }\n },\n [ resetAudioLevels ],\n );\n\n const visualize = useCallback(() => {\n if (!analyzerRef.current) {\n return;\n }\n\n const { analyser } = analyzerRef.current;\n\n const draw = () => {\n animationFrameRef.current = scheduleFrame(draw);\n\n const levels = readFrequencyLevels(analyser);\n setAudioLevels(levels);\n };\n\n draw();\n }, []);\n\n const resumeVisualization = useCallback(() => {\n if (analyzerRef.current && !animationFrameRef.current) {\n visualize();\n }\n }, [ visualize ]);\n\n const initializeVisualization = useCallback(\n (stream: MediaStream) => {\n analyzerRef.current = createStreamAnalyzer(stream);\n visualize();\n },\n [ visualize ],\n );\n\n const generatePreviewLevels = useCallback(async (blob: Blob) => {\n try {\n const levels = await decodeAudioLevels(blob);\n\n setAudioLevels(levels);\n setPreviewLevels(levels);\n } catch (error) {\n console.warn('[audio-recorder] Failed to build waveform preview', error);\n setPreviewLevels(createInitialLevels());\n }\n }, []);\n\n const cleanupVisualization = useCallback(() => {\n if (animationFrameRef.current) {\n cancelFrame(animationFrameRef.current);\n }\n\n animationFrameRef.current = null;\n\n if (analyzerRef.current) {\n void analyzerRef.current.context.close();\n analyzerRef.current = null;\n }\n\n resetAudioLevels();\n resetPreviewLevels();\n }, [ resetAudioLevels, resetPreviewLevels ]);\n\n return {\n audioLevels,\n previewLevels,\n resetAudioLevels,\n stopVisualization,\n resetPreviewLevels,\n resumeVisualization,\n cleanupVisualization,\n generatePreviewLevels,\n initializeVisualization,\n };\n};\n\n\nexport { useRecorderVisualization };\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useRecordingSender — handles sending a recorded voice message via an upload strategy.
|
|
5
|
+
*
|
|
6
|
+
* @author: exode <hello@exode.ru>
|
|
7
|
+
*/
|
|
8
|
+
const useRecordingSender = (options) => {
|
|
9
|
+
const { uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, getDurationMs, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
|
|
10
|
+
const sendRecording = useCallback(async () => {
|
|
11
|
+
if (!audioBlob) {
|
|
12
|
+
const error = new Error('Voice recording is not available');
|
|
13
|
+
onRecordingError === null || onRecordingError === void 0 ? void 0 : onRecordingError(error);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!uploader) {
|
|
17
|
+
const error = new Error('Uploader strategy is not defined');
|
|
18
|
+
onRecordingError === null || onRecordingError === void 0 ? void 0 : onRecordingError(error);
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const durationMs = getDurationMs();
|
|
23
|
+
const durationSec = durationMs / 1000;
|
|
24
|
+
const file = new File([audioBlob], fileName, {
|
|
25
|
+
type: audioBlob.type || mimeType,
|
|
26
|
+
});
|
|
27
|
+
const uploaded = await uploader(file, { uploadId });
|
|
28
|
+
await onUploaded(uploaded, durationSec);
|
|
29
|
+
if (autoResetAfterUpload) {
|
|
30
|
+
deleteRecording();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
onRecordingError === null || onRecordingError === void 0 ? void 0 : onRecordingError(error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}, [
|
|
38
|
+
uploadId,
|
|
39
|
+
uploader,
|
|
40
|
+
fileName,
|
|
41
|
+
audioBlob,
|
|
42
|
+
mimeType,
|
|
43
|
+
onUploaded,
|
|
44
|
+
getDurationMs,
|
|
45
|
+
deleteRecording,
|
|
46
|
+
onRecordingError,
|
|
47
|
+
autoResetAfterUpload,
|
|
48
|
+
]);
|
|
49
|
+
return { sendRecording };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export { useRecordingSender };
|
|
53
|
+
//# sourceMappingURL=useRecordingSender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRecordingSender.js","sources":["../../src/hooks/useRecordingSender.ts"],"sourcesContent":["/**\n * useRecordingSender — handles sending a recorded voice message via an upload strategy.\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback } from 'react';\n\nimport type { UploadStrategy } from '../types';\n\n\ninterface UseRecordingSenderOptions<TFile> {\n fileName: string;\n mimeType: string;\n audioBlob: Blob | null;\n uploadId: string;\n getDurationMs: () => number;\n uploader?: UploadStrategy<TFile>;\n onUploaded: (file: TFile, duration: number) => void | Promise<void>;\n deleteRecording: () => void;\n autoResetAfterUpload: boolean;\n onRecordingError?: (error: unknown) => void;\n}\n\n\nconst useRecordingSender = <TFile = unknown>(options: UseRecordingSenderOptions<TFile>) => {\n\n const {\n uploader,\n fileName,\n mimeType,\n uploadId,\n audioBlob,\n onUploaded,\n getDurationMs,\n onRecordingError,\n deleteRecording,\n autoResetAfterUpload,\n } = options;\n\n const sendRecording = useCallback(async () => {\n if (!audioBlob) {\n const error = new Error('Voice recording is not available');\n\n onRecordingError?.(error);\n\n return;\n }\n\n if (!uploader) {\n const error = new Error('Uploader strategy is not defined');\n\n onRecordingError?.(error);\n throw error;\n }\n\n try {\n const durationMs = getDurationMs();\n const durationSec = durationMs / 1000;\n\n const file = new File([ audioBlob ], fileName, {\n type: audioBlob.type || mimeType,\n });\n\n const uploaded = await uploader(file, { uploadId });\n await onUploaded(uploaded as TFile, durationSec);\n\n if (autoResetAfterUpload) {\n deleteRecording();\n }\n } catch (error) {\n onRecordingError?.(error);\n throw error;\n }\n }, [\n uploadId,\n uploader,\n fileName,\n audioBlob,\n mimeType,\n onUploaded,\n getDurationMs,\n deleteRecording,\n onRecordingError,\n autoResetAfterUpload,\n ]);\n\n return { sendRecording };\n};\n\n\nexport { useRecordingSender };\n"],"names":[],"mappings":";;AAAA;;;;AAIG;AAqBH,MAAM,kBAAkB,GAAG,CAAkB,OAAyC,KAAI;IAEtF,MAAM,EACF,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,oBAAoB,GACvB,GAAG,OAAO;AAEX,IAAA,MAAM,aAAa,GAAG,WAAW,CAAC,YAAW;QACzC,IAAI,CAAC,SAAS,EAAE;AACZ,YAAA,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,kCAAkC,CAAC;AAE3D,YAAA,gBAAgB,aAAhB,gBAAgB,KAAA,MAAA,GAAA,MAAA,GAAhB,gBAAgB,CAAG,KAAK,CAAC;YAEzB;QACJ;QAEA,IAAI,CAAC,QAAQ,EAAE;AACX,YAAA,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,kCAAkC,CAAC;AAE3D,YAAA,gBAAgB,aAAhB,gBAAgB,KAAA,MAAA,GAAA,MAAA,GAAhB,gBAAgB,CAAG,KAAK,CAAC;AACzB,YAAA,MAAM,KAAK;QACf;AAEA,QAAA,IAAI;AACA,YAAA,MAAM,UAAU,GAAG,aAAa,EAAE;AAClC,YAAA,MAAM,WAAW,GAAG,UAAU,GAAG,IAAI;YAErC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAE,SAAS,CAAE,EAAE,QAAQ,EAAE;AAC3C,gBAAA,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,QAAQ;AACnC,aAAA,CAAC;YAEF,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC;AACnD,YAAA,MAAM,UAAU,CAAC,QAAiB,EAAE,WAAW,CAAC;YAEhD,IAAI,oBAAoB,EAAE;AACtB,gBAAA,eAAe,EAAE;YACrB;QACJ;QAAE,OAAO,KAAK,EAAE;AACZ,YAAA,gBAAgB,aAAhB,gBAAgB,KAAA,MAAA,GAAA,MAAA,GAAhB,gBAAgB,CAAG,KAAK,CAAC;AACzB,YAAA,MAAM,KAAK;QACf;AACJ,IAAA,CAAC,EAAE;QACC,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,SAAS;QACT,QAAQ;QACR,UAAU;QACV,aAAa;QACb,eAAe;QACf,gBAAgB;QAChB,oBAAoB;AACvB,KAAA,CAAC;IAEF,OAAO,EAAE,aAAa,EAAE;AAC5B;;;;"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { roundTo } from '../utils/math.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* useRecordingTiming
|
|
6
|
+
*
|
|
7
|
+
* @author: exode <hello@exode.ru>
|
|
8
|
+
*/
|
|
9
|
+
const useRecordingTiming = () => {
|
|
10
|
+
const [lastDuration, setLastDuration] = useState(0);
|
|
11
|
+
const [recordingTime, setRecordingTime] = useState(0);
|
|
12
|
+
const timerRef = useRef(null);
|
|
13
|
+
const recordingStartRef = useRef(null);
|
|
14
|
+
const recordingAccumulatedRef = useRef(0);
|
|
15
|
+
const stopTicker = useCallback(() => {
|
|
16
|
+
if (timerRef.current) {
|
|
17
|
+
clearInterval(timerRef.current);
|
|
18
|
+
timerRef.current = null;
|
|
19
|
+
}
|
|
20
|
+
}, []);
|
|
21
|
+
const startTicker = useCallback(() => {
|
|
22
|
+
if (timerRef.current) {
|
|
23
|
+
clearInterval(timerRef.current);
|
|
24
|
+
}
|
|
25
|
+
timerRef.current = setInterval(() => {
|
|
26
|
+
if (recordingStartRef.current === null) {
|
|
27
|
+
setRecordingTime(roundTo(recordingAccumulatedRef.current));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const elapsed = (performance.now() - recordingStartRef.current) / 1000 +
|
|
31
|
+
recordingAccumulatedRef.current;
|
|
32
|
+
setRecordingTime(roundTo(elapsed));
|
|
33
|
+
}, 100);
|
|
34
|
+
}, []);
|
|
35
|
+
const startNewSession = useCallback(() => {
|
|
36
|
+
recordingAccumulatedRef.current = 0;
|
|
37
|
+
setRecordingTime(0);
|
|
38
|
+
setLastDuration(0);
|
|
39
|
+
recordingStartRef.current = performance.now();
|
|
40
|
+
startTicker();
|
|
41
|
+
}, [startTicker]);
|
|
42
|
+
const resumeSession = useCallback(() => {
|
|
43
|
+
recordingStartRef.current = performance.now();
|
|
44
|
+
startTicker();
|
|
45
|
+
}, [startTicker]);
|
|
46
|
+
const pauseSession = useCallback(() => {
|
|
47
|
+
if (recordingStartRef.current === null) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const elapsed = (performance.now() - recordingStartRef.current) / 1000;
|
|
51
|
+
recordingAccumulatedRef.current += elapsed;
|
|
52
|
+
recordingStartRef.current = null;
|
|
53
|
+
setRecordingTime(roundTo(recordingAccumulatedRef.current));
|
|
54
|
+
stopTicker();
|
|
55
|
+
}, [stopTicker]);
|
|
56
|
+
const getElapsedMs = useCallback(() => {
|
|
57
|
+
const accumulated = recordingAccumulatedRef.current;
|
|
58
|
+
const elapsed = recordingStartRef.current !== null
|
|
59
|
+
? (performance.now() - recordingStartRef.current) / 1000 + accumulated
|
|
60
|
+
: accumulated;
|
|
61
|
+
return Math.round(elapsed * 1000);
|
|
62
|
+
}, []);
|
|
63
|
+
const resetSession = useCallback(() => {
|
|
64
|
+
const accumulated = recordingAccumulatedRef.current;
|
|
65
|
+
const elapsed = recordingStartRef.current !== null
|
|
66
|
+
? (performance.now() - recordingStartRef.current) / 1000 + accumulated
|
|
67
|
+
: accumulated;
|
|
68
|
+
if (elapsed > 0) {
|
|
69
|
+
setLastDuration(roundTo(elapsed));
|
|
70
|
+
}
|
|
71
|
+
stopTicker();
|
|
72
|
+
recordingStartRef.current = null;
|
|
73
|
+
recordingAccumulatedRef.current = 0;
|
|
74
|
+
setRecordingTime(0);
|
|
75
|
+
}, [stopTicker]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
return () => {
|
|
78
|
+
stopTicker();
|
|
79
|
+
};
|
|
80
|
+
}, [stopTicker]);
|
|
81
|
+
return {
|
|
82
|
+
lastDuration,
|
|
83
|
+
getElapsedMs,
|
|
84
|
+
pauseSession,
|
|
85
|
+
resetSession,
|
|
86
|
+
recordingTime,
|
|
87
|
+
resumeSession,
|
|
88
|
+
startNewSession,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export { useRecordingTiming };
|
|
93
|
+
//# sourceMappingURL=useRecordingTiming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRecordingTiming.js","sources":["../../src/hooks/useRecordingTiming.ts"],"sourcesContent":["/**\n * useRecordingTiming\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { roundTo } from '../utils';\n\n\ninterface UseRecordingTimingResult {\n recordingTime: number;\n lastDuration: number;\n getElapsedMs: () => number;\n pauseSession: () => void;\n resetSession: () => void;\n resumeSession: () => void;\n startNewSession: () => void;\n}\n\nconst useRecordingTiming = (): UseRecordingTimingResult => {\n\n const [ lastDuration, setLastDuration ] = useState(0);\n const [ recordingTime, setRecordingTime ] = useState(0);\n\n const timerRef = useRef<NodeJS.Timeout | null>(null);\n const recordingStartRef = useRef<number | null>(null);\n const recordingAccumulatedRef = useRef(0);\n\n const stopTicker = useCallback(() => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n timerRef.current = null;\n }\n }, []);\n\n const startTicker = useCallback(() => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n }\n\n timerRef.current = setInterval(() => {\n if (recordingStartRef.current === null) {\n setRecordingTime(roundTo(recordingAccumulatedRef.current));\n\n return;\n }\n\n const elapsed =\n (performance.now() - recordingStartRef.current) / 1000 +\n recordingAccumulatedRef.current;\n setRecordingTime(roundTo(elapsed));\n }, 100);\n }, []);\n\n const startNewSession = useCallback(() => {\n recordingAccumulatedRef.current = 0;\n setRecordingTime(0);\n setLastDuration(0);\n\n recordingStartRef.current = performance.now();\n startTicker();\n }, [ startTicker ]);\n\n const resumeSession = useCallback(() => {\n recordingStartRef.current = performance.now();\n startTicker();\n }, [ startTicker ]);\n\n const pauseSession = useCallback(() => {\n if (recordingStartRef.current === null) {\n return;\n }\n\n const elapsed = (performance.now() - recordingStartRef.current) / 1000;\n recordingAccumulatedRef.current += elapsed;\n recordingStartRef.current = null;\n\n setRecordingTime(roundTo(recordingAccumulatedRef.current));\n stopTicker();\n }, [ stopTicker ]);\n\n const getElapsedMs = useCallback(() => {\n const accumulated = recordingAccumulatedRef.current;\n\n const elapsed = recordingStartRef.current !== null\n ? (performance.now() - recordingStartRef.current) / 1000 + accumulated\n : accumulated;\n\n return Math.round(elapsed * 1000);\n }, []);\n\n const resetSession = useCallback(() => {\n const accumulated = recordingAccumulatedRef.current;\n\n const elapsed = recordingStartRef.current !== null\n ? (performance.now() - recordingStartRef.current) / 1000 + accumulated\n : accumulated;\n\n if (elapsed > 0) {\n setLastDuration(roundTo(elapsed));\n }\n\n stopTicker();\n recordingStartRef.current = null;\n recordingAccumulatedRef.current = 0;\n setRecordingTime(0);\n }, [ stopTicker ]);\n\n useEffect(() => {\n return () => {\n stopTicker();\n };\n }, [ stopTicker ]);\n\n return {\n lastDuration,\n getElapsedMs,\n pauseSession,\n resetSession,\n recordingTime,\n resumeSession,\n startNewSession,\n };\n};\n\n\nexport { useRecordingTiming };\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { formatTime } from '../utils/format.js';
|
|
3
|
+
import { audioPlaybackManager } from '../platform/audio-playback-manager.js';
|
|
4
|
+
import { useAudioBlobLoader } from './useAudioBlobLoader.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* useVoiceMessagePlayer
|
|
8
|
+
*
|
|
9
|
+
* @author: exode <hello@exode.ru>
|
|
10
|
+
*/
|
|
11
|
+
const useVoiceMessagePlayer = (options) => {
|
|
12
|
+
const { src, onEnded, onPreviousTrack, onNextTrack, waveformData, preloadedBlob, duration: externalDuration, } = options;
|
|
13
|
+
const onEndedRef = useRef(onEnded);
|
|
14
|
+
const onPreviousTrackRef = useRef(onPreviousTrack);
|
|
15
|
+
const onNextTrackRef = useRef(onNextTrack);
|
|
16
|
+
onEndedRef.current = onEnded;
|
|
17
|
+
onPreviousTrackRef.current = onPreviousTrack;
|
|
18
|
+
onNextTrackRef.current = onNextTrack;
|
|
19
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
20
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
21
|
+
const [duration, setDuration] = useState(externalDuration || 0);
|
|
22
|
+
const progress = duration > 0 ? currentTime / duration : 0;
|
|
23
|
+
/** Stop this player (called by manager when another player starts) */
|
|
24
|
+
const stopThis = useCallback(() => {
|
|
25
|
+
const audio = audioRef.current;
|
|
26
|
+
if (audio && !audio.paused) {
|
|
27
|
+
audio.pause();
|
|
28
|
+
}
|
|
29
|
+
setIsPlaying(false);
|
|
30
|
+
}, []);
|
|
31
|
+
/** Subscribe to global manager on mount */
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
return audioPlaybackManager.subscribe(stopThis);
|
|
34
|
+
}, [stopThis]);
|
|
35
|
+
const { audioRef, isLoading, isLoaded, waveformLevels, ensureAudioLoaded, } = useAudioBlobLoader({
|
|
36
|
+
src,
|
|
37
|
+
waveformData,
|
|
38
|
+
preloadedBlob,
|
|
39
|
+
playbackCallbacks: {
|
|
40
|
+
onLoadedMetadata: (dur) => {
|
|
41
|
+
if (!externalDuration && dur && isFinite(dur) && dur > 0.5) {
|
|
42
|
+
setDuration(dur);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
onTimeUpdate: (time) => setCurrentTime(time),
|
|
46
|
+
onEnded: () => {
|
|
47
|
+
var _a;
|
|
48
|
+
setIsPlaying(false);
|
|
49
|
+
setCurrentTime(0);
|
|
50
|
+
(_a = onEndedRef.current) === null || _a === void 0 ? void 0 : _a.call(onEndedRef);
|
|
51
|
+
},
|
|
52
|
+
onPlay: () => setIsPlaying(true),
|
|
53
|
+
onPause: () => setIsPlaying(false),
|
|
54
|
+
onStop: () => {
|
|
55
|
+
setIsPlaying(false);
|
|
56
|
+
setCurrentTime(0);
|
|
57
|
+
},
|
|
58
|
+
onPreviousTrack: () => { var _a; return (_a = onPreviousTrackRef.current) === null || _a === void 0 ? void 0 : _a.call(onPreviousTrackRef); },
|
|
59
|
+
onNextTrack: () => { var _a; return (_a = onNextTrackRef.current) === null || _a === void 0 ? void 0 : _a.call(onNextTrackRef); },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
/** Pre-fetch audio without playing (for viewport preloading) */
|
|
63
|
+
const prefetch = useCallback(() => {
|
|
64
|
+
if (isLoading || audioRef.current) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
void ensureAudioLoaded();
|
|
68
|
+
}, [isLoading, ensureAudioLoaded]);
|
|
69
|
+
/** Load audio and auto-play after loaded */
|
|
70
|
+
const preload = useCallback(() => {
|
|
71
|
+
if (isLoading || audioRef.current) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
audioPlaybackManager.stopOthers(stopThis);
|
|
75
|
+
void ensureAudioLoaded().then(() => {
|
|
76
|
+
const audio = audioRef.current;
|
|
77
|
+
if (audio) {
|
|
78
|
+
audio.play()
|
|
79
|
+
.then(() => setIsPlaying(true))
|
|
80
|
+
.catch(() => setIsPlaying(false));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}, [isLoading, ensureAudioLoaded, stopThis, audioRef]);
|
|
84
|
+
/** Programmatically start playback (used for chained playback) */
|
|
85
|
+
const play = useCallback(() => {
|
|
86
|
+
const audio = audioRef.current;
|
|
87
|
+
if (audio) {
|
|
88
|
+
audioPlaybackManager.stopOthers(stopThis);
|
|
89
|
+
audio.play()
|
|
90
|
+
.then(() => setIsPlaying(true))
|
|
91
|
+
.catch(() => setIsPlaying(false));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
preload();
|
|
95
|
+
}
|
|
96
|
+
}, [stopThis, preload, audioRef]);
|
|
97
|
+
/** Toggle play/pause (only works after audio is loaded) */
|
|
98
|
+
const togglePlayback = useCallback(() => {
|
|
99
|
+
const audio = audioRef.current;
|
|
100
|
+
if (!audio || isLoading) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (audio.paused) {
|
|
104
|
+
audioPlaybackManager.stopOthers(stopThis);
|
|
105
|
+
audio.play()
|
|
106
|
+
.then(() => setIsPlaying(true))
|
|
107
|
+
.catch(() => setIsPlaying(false));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
audio.pause();
|
|
111
|
+
setIsPlaying(false);
|
|
112
|
+
}
|
|
113
|
+
}, [isLoading, stopThis, audioRef]);
|
|
114
|
+
const seekTo = useCallback((seekPercent) => {
|
|
115
|
+
const audio = audioRef.current;
|
|
116
|
+
if (!audio || !isFinite(audio.duration)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const time = seekPercent * audio.duration;
|
|
120
|
+
audio.currentTime = time;
|
|
121
|
+
setCurrentTime(time);
|
|
122
|
+
}, [audioRef]);
|
|
123
|
+
/** Seek to a specific time in seconds and start playback */
|
|
124
|
+
const seekToTime = useCallback((seconds) => {
|
|
125
|
+
const audio = audioRef.current;
|
|
126
|
+
if (audio && isFinite(audio.duration)) {
|
|
127
|
+
audioPlaybackManager.stopOthers(stopThis);
|
|
128
|
+
audio.currentTime = Math.max(0, Math.min(seconds, audio.duration));
|
|
129
|
+
setCurrentTime(audio.currentTime);
|
|
130
|
+
audio.play()
|
|
131
|
+
.then(() => setIsPlaying(true))
|
|
132
|
+
.catch(() => setIsPlaying(false));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
void ensureAudioLoaded().then(() => {
|
|
136
|
+
const loadedAudio = audioRef.current;
|
|
137
|
+
if (loadedAudio) {
|
|
138
|
+
audioPlaybackManager.stopOthers(stopThis);
|
|
139
|
+
loadedAudio.currentTime = Math.max(0, Math.min(seconds, loadedAudio.duration || seconds));
|
|
140
|
+
setCurrentTime(loadedAudio.currentTime);
|
|
141
|
+
loadedAudio.play()
|
|
142
|
+
.then(() => setIsPlaying(true))
|
|
143
|
+
.catch(() => setIsPlaying(false));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}, [
|
|
148
|
+
audioRef,
|
|
149
|
+
stopThis,
|
|
150
|
+
ensureAudioLoaded,
|
|
151
|
+
]);
|
|
152
|
+
/** Sync external duration */
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (externalDuration) {
|
|
155
|
+
setDuration(externalDuration);
|
|
156
|
+
}
|
|
157
|
+
}, [externalDuration]);
|
|
158
|
+
return {
|
|
159
|
+
play,
|
|
160
|
+
progress,
|
|
161
|
+
duration,
|
|
162
|
+
isLoaded,
|
|
163
|
+
isPlaying,
|
|
164
|
+
isLoading,
|
|
165
|
+
currentTime,
|
|
166
|
+
waveformLevels,
|
|
167
|
+
seekTo,
|
|
168
|
+
seekToTime,
|
|
169
|
+
prefetch,
|
|
170
|
+
preload,
|
|
171
|
+
formatTime,
|
|
172
|
+
togglePlayback,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export { useVoiceMessagePlayer };
|
|
177
|
+
//# sourceMappingURL=useVoiceMessagePlayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useVoiceMessagePlayer.js","sources":["../../src/hooks/useVoiceMessagePlayer.ts"],"sourcesContent":["/**\n * useVoiceMessagePlayer\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { formatTime } from '../utils';\nimport { audioPlaybackManager } from '../platform/audio-playback-manager';\n\nimport { useAudioBlobLoader } from './useAudioBlobLoader';\n\nimport type { UseVoiceMessagePlayerOptions, UseVoiceMessagePlayerResult } from '../types';\n\n\nconst useVoiceMessagePlayer = (options: UseVoiceMessagePlayerOptions): UseVoiceMessagePlayerResult => {\n\n const {\n src,\n onEnded,\n onPreviousTrack,\n onNextTrack,\n waveformData,\n preloadedBlob,\n duration: externalDuration,\n } = options;\n\n const onEndedRef = useRef(onEnded);\n const onPreviousTrackRef = useRef(onPreviousTrack);\n const onNextTrackRef = useRef(onNextTrack);\n\n onEndedRef.current = onEnded;\n onPreviousTrackRef.current = onPreviousTrack;\n onNextTrackRef.current = onNextTrack;\n\n const [ isPlaying, setIsPlaying ] = useState(false);\n const [ currentTime, setCurrentTime ] = useState(0);\n const [ duration, setDuration ] = useState(externalDuration || 0);\n\n const progress = duration > 0 ? currentTime / duration : 0;\n\n /** Stop this player (called by manager when another player starts) */\n const stopThis = useCallback(() => {\n const audio = audioRef.current;\n\n if (audio && !audio.paused) {\n audio.pause();\n }\n\n setIsPlaying(false);\n }, []);\n\n /** Subscribe to global manager on mount */\n useEffect(() => {\n return audioPlaybackManager.subscribe(stopThis);\n }, [ stopThis ]);\n\n const {\n audioRef,\n isLoading,\n isLoaded,\n waveformLevels,\n ensureAudioLoaded,\n } = useAudioBlobLoader({\n src,\n waveformData,\n preloadedBlob,\n playbackCallbacks: {\n onLoadedMetadata: (dur) => {\n if (!externalDuration && dur && isFinite(dur) && dur > 0.5) {\n setDuration(dur);\n }\n },\n onTimeUpdate: (time) => setCurrentTime(time),\n onEnded: () => {\n setIsPlaying(false);\n setCurrentTime(0);\n onEndedRef.current?.();\n },\n onPlay: () => setIsPlaying(true),\n onPause: () => setIsPlaying(false),\n onStop: () => {\n setIsPlaying(false);\n setCurrentTime(0);\n },\n onPreviousTrack: () => onPreviousTrackRef.current?.(),\n onNextTrack: () => onNextTrackRef.current?.(),\n },\n });\n\n /** Pre-fetch audio without playing (for viewport preloading) */\n const prefetch = useCallback(() => {\n if (isLoading || audioRef.current) {\n return;\n }\n\n void ensureAudioLoaded();\n }, [ isLoading, ensureAudioLoaded ]);\n\n /** Load audio and auto-play after loaded */\n const preload = useCallback(() => {\n if (isLoading || audioRef.current) {\n return;\n }\n\n audioPlaybackManager.stopOthers(stopThis);\n\n void ensureAudioLoaded().then(() => {\n const audio = audioRef.current;\n\n if (audio) {\n audio.play()\n .then(() => setIsPlaying(true))\n .catch(() => setIsPlaying(false));\n }\n });\n }, [ isLoading, ensureAudioLoaded, stopThis, audioRef ]);\n\n /** Programmatically start playback (used for chained playback) */\n const play = useCallback(() => {\n const audio = audioRef.current;\n\n if (audio) {\n audioPlaybackManager.stopOthers(stopThis);\n audio.play()\n .then(() => setIsPlaying(true))\n .catch(() => setIsPlaying(false));\n } else {\n preload();\n }\n }, [ stopThis, preload, audioRef ]);\n\n /** Toggle play/pause (only works after audio is loaded) */\n const togglePlayback = useCallback(() => {\n const audio = audioRef.current;\n\n if (!audio || isLoading) {\n return;\n }\n\n if (audio.paused) {\n audioPlaybackManager.stopOthers(stopThis);\n audio.play()\n .then(() => setIsPlaying(true))\n .catch(() => setIsPlaying(false));\n } else {\n audio.pause();\n setIsPlaying(false);\n }\n }, [ isLoading, stopThis, audioRef ]);\n\n const seekTo = useCallback((seekPercent: number) => {\n const audio = audioRef.current;\n\n if (!audio || !isFinite(audio.duration)) {\n return;\n }\n\n const time = seekPercent * audio.duration;\n\n audio.currentTime = time;\n setCurrentTime(time);\n }, [ audioRef ]);\n\n /** Seek to a specific time in seconds and start playback */\n const seekToTime = useCallback((seconds: number) => {\n const audio = audioRef.current;\n\n if (audio && isFinite(audio.duration)) {\n audioPlaybackManager.stopOthers(stopThis);\n\n audio.currentTime = Math.max(0, Math.min(seconds, audio.duration));\n setCurrentTime(audio.currentTime);\n\n audio.play()\n .then(() => setIsPlaying(true))\n .catch(() => setIsPlaying(false));\n } else {\n void ensureAudioLoaded().then(() => {\n const loadedAudio = audioRef.current;\n\n if (loadedAudio) {\n audioPlaybackManager.stopOthers(stopThis);\n\n loadedAudio.currentTime = Math.max(0, Math.min(\n seconds,\n loadedAudio.duration || seconds,\n ));\n\n setCurrentTime(loadedAudio.currentTime);\n\n loadedAudio.play()\n .then(() => setIsPlaying(true))\n .catch(() => setIsPlaying(false));\n }\n });\n }\n }, [\n audioRef,\n stopThis,\n ensureAudioLoaded,\n ]);\n\n /** Sync external duration */\n useEffect(() => {\n if (externalDuration) {\n setDuration(externalDuration);\n }\n }, [ externalDuration ]);\n\n return {\n play,\n progress,\n duration,\n isLoaded,\n isPlaying,\n isLoading,\n currentTime,\n waveformLevels,\n seekTo,\n seekToTime,\n prefetch,\n preload,\n formatTime,\n togglePlayback,\n };\n};\n\n\nexport { useVoiceMessagePlayer };\n"],"names":[],"mappings":"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"}
|