@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.
Files changed (92) hide show
  1. package/README.md +2 -2
  2. package/dist/components/AudioRecorder.js +50 -0
  3. package/dist/components/AudioRecorder.js.map +1 -0
  4. package/dist/components/AudioRecorder.messages.js +47 -0
  5. package/dist/components/AudioRecorder.messages.js.map +1 -0
  6. package/dist/components/AudioRecorder.styled.js +16 -0
  7. package/dist/components/AudioRecorder.styled.js.map +1 -0
  8. package/dist/components/IdleControl/IdleControl.js +12 -0
  9. package/dist/components/IdleControl/IdleControl.js.map +1 -0
  10. package/dist/components/IdleControl/IdleControl.styled.js +68 -0
  11. package/dist/components/IdleControl/IdleControl.styled.js.map +1 -0
  12. package/dist/components/RecordingShell/RecordingShell.js +80 -0
  13. package/dist/components/RecordingShell/RecordingShell.js.map +1 -0
  14. package/dist/components/RecordingShell/RecordingShell.styled.js +159 -0
  15. package/dist/components/RecordingShell/RecordingShell.styled.js.map +1 -0
  16. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js +151 -0
  17. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.js.map +1 -0
  18. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js +92 -0
  19. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.styled.js.map +1 -0
  20. package/dist/components/VoiceRecorderInput/PreviewBars.js +52 -0
  21. package/dist/components/VoiceRecorderInput/PreviewBars.js.map +1 -0
  22. package/dist/components/VoiceRecorderInput/TimelineBars.js +31 -0
  23. package/dist/components/VoiceRecorderInput/TimelineBars.js.map +1 -0
  24. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js +51 -0
  25. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.js.map +1 -0
  26. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js +175 -0
  27. package/dist/components/VoiceRecorderInput/VoiceRecorderInput.styled.js.map +1 -0
  28. package/dist/components/WaveformBars/WaveformBars.js +29 -0
  29. package/dist/components/WaveformBars/WaveformBars.js.map +1 -0
  30. package/dist/components/WaveformBars/WaveformBars.styled.js +20 -0
  31. package/dist/components/WaveformBars/WaveformBars.styled.js.map +1 -0
  32. package/dist/constants.js +17 -0
  33. package/dist/constants.js.map +1 -0
  34. package/dist/hooks/useAudioBlobLoader.d.ts +1 -5
  35. package/dist/hooks/useAudioBlobLoader.d.ts.map +1 -1
  36. package/dist/hooks/useAudioBlobLoader.js +175 -0
  37. package/dist/hooks/useAudioBlobLoader.js.map +1 -0
  38. package/dist/hooks/useAudioPlayback.js +119 -0
  39. package/dist/hooks/useAudioPlayback.js.map +1 -0
  40. package/dist/hooks/useAudioRecorderHandlers.js +137 -0
  41. package/dist/hooks/useAudioRecorderHandlers.js.map +1 -0
  42. package/dist/hooks/useMediaRecorder.js +127 -0
  43. package/dist/hooks/useMediaRecorder.js.map +1 -0
  44. package/dist/hooks/useRecorderVisualization.js +94 -0
  45. package/dist/hooks/useRecorderVisualization.js.map +1 -0
  46. package/dist/hooks/useRecordingSender.js +53 -0
  47. package/dist/hooks/useRecordingSender.js.map +1 -0
  48. package/dist/hooks/useRecordingTiming.js +93 -0
  49. package/dist/hooks/useRecordingTiming.js.map +1 -0
  50. package/dist/hooks/useVoiceMessagePlayer.js +177 -0
  51. package/dist/hooks/useVoiceMessagePlayer.js.map +1 -0
  52. package/dist/hooks/useVoiceRecorder.js +153 -0
  53. package/dist/hooks/useVoiceRecorder.js.map +1 -0
  54. package/dist/hooks/useVoiceRecorderInput.js +163 -0
  55. package/dist/hooks/useVoiceRecorderInput.js.map +1 -0
  56. package/dist/index.cjs +108 -61
  57. package/dist/index.cjs.map +1 -1
  58. package/dist/index.js +13 -2612
  59. package/dist/index.js.map +1 -1
  60. package/dist/platform/animation-frame-adapter.js +14 -0
  61. package/dist/platform/animation-frame-adapter.js.map +1 -0
  62. package/dist/platform/audio-analyzer-adapter.d.ts.map +1 -1
  63. package/dist/platform/audio-analyzer-adapter.js +74 -0
  64. package/dist/platform/audio-analyzer-adapter.js.map +1 -0
  65. package/dist/platform/audio-blob-fetcher.js +19 -0
  66. package/dist/platform/audio-blob-fetcher.js.map +1 -0
  67. package/dist/platform/audio-playback-adapter.d.ts.map +1 -1
  68. package/dist/platform/audio-playback-adapter.js +114 -0
  69. package/dist/platform/audio-playback-adapter.js.map +1 -0
  70. package/dist/platform/audio-playback-manager.js +26 -0
  71. package/dist/platform/audio-playback-manager.js.map +1 -0
  72. package/dist/platform/media-recorder-adapter.js +23 -0
  73. package/dist/platform/media-recorder-adapter.js.map +1 -0
  74. package/dist/platform/microphone.js +24 -0
  75. package/dist/platform/microphone.js.map +1 -0
  76. package/dist/platform/mpeg-frame.js +60 -0
  77. package/dist/platform/mpeg-frame.js.map +1 -0
  78. package/dist/utils/format.js +13 -0
  79. package/dist/utils/format.js.map +1 -0
  80. package/dist/utils/index.d.ts +1 -0
  81. package/dist/utils/index.d.ts.map +1 -1
  82. package/dist/utils/math.js +16 -0
  83. package/dist/utils/math.js.map +1 -0
  84. package/dist/utils/pseudo-waveform.d.ts +12 -0
  85. package/dist/utils/pseudo-waveform.d.ts.map +1 -0
  86. package/dist/utils/pseudo-waveform.js +40 -0
  87. package/dist/utils/pseudo-waveform.js.map +1 -0
  88. package/dist/utils/voice-blob-cache.js +35 -0
  89. package/dist/utils/voice-blob-cache.js.map +1 -0
  90. package/dist/utils/voice-playback-chain.js +42 -0
  91. package/dist/utils/voice-playback-chain.js.map +1 -0
  92. package/package.json +2 -1
@@ -0,0 +1,29 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import 'react';
3
+ import { clampValue } from '../../utils/math.js';
4
+ import { Bars, Bar } from './WaveformBars.styled.js';
5
+
6
+ const WaveformBars = (props) => {
7
+ const { values, progress, styleInterpolator, scale = 40, minHeight = 6, divider = 100, } = props;
8
+ return (jsx(Bars, { children: values.map((value, index) => {
9
+ const normalized = clampValue(value);
10
+ const height = Math.max(minHeight, (normalized / divider) * scale);
11
+ const barProgress = values.length > 1 ? index / (values.length - 1) : 0;
12
+ const isPlayed = (progress !== null && progress !== void 0 ? progress : 0) > 0 && barProgress <= (progress !== null && progress !== void 0 ? progress : 0);
13
+ const interpolated = styleInterpolator === null || styleInterpolator === void 0 ? void 0 : styleInterpolator({
14
+ normalized,
15
+ height,
16
+ index,
17
+ progress: progress !== null && progress !== void 0 ? progress : 0,
18
+ barProgress,
19
+ isPlayed,
20
+ });
21
+ return (jsx(Bar, { style: {
22
+ height: `${height}px`,
23
+ ...interpolated,
24
+ } }, index));
25
+ }) }));
26
+ };
27
+
28
+ export { WaveformBars };
29
+ //# sourceMappingURL=WaveformBars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WaveformBars.js","sources":["../../../src/components/WaveformBars/WaveformBars.tsx"],"sourcesContent":["/**\n * WaveformBars\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport React from 'react';\n\nimport { clampValue } from '../../utils/math';\n\nimport { Bar, Bars } from './WaveformBars.styled';\n\n\ninterface Props {\n scale?: number;\n divider?: number;\n values: number[];\n progress?: number;\n minHeight?: number;\n styleInterpolator?: (context: {\n normalized: number;\n height: number;\n index: number;\n progress: number;\n barProgress: number;\n isPlayed: boolean;\n }) => React.CSSProperties;\n}\n\n\nconst WaveformBars = (props: Props) => {\n\n const {\n values,\n progress,\n styleInterpolator,\n scale = 40,\n minHeight = 6,\n divider = 100,\n } = props;\n\n return (\n <Bars>\n {values.map((value, index) => {\n const normalized = clampValue(value);\n const height = Math.max(minHeight, (normalized / divider) * scale);\n const barProgress = values.length > 1 ? index / (values.length - 1) : 0;\n const isPlayed = (progress ?? 0) > 0 && barProgress <= (progress ?? 0);\n const interpolated = styleInterpolator?.({\n normalized,\n height,\n index,\n progress: progress ?? 0,\n barProgress,\n isPlayed,\n });\n\n return (\n <Bar key={index} style={{\n height: `${height}px`,\n ...interpolated,\n }}/>\n );\n })}\n </Bars>\n );\n};\n\n\nexport { WaveformBars };"],"names":["_jsx"],"mappings":";;;;;AA8BA,MAAM,YAAY,GAAG,CAAC,KAAY,KAAI;IAElC,MAAM,EACF,MAAM,EACN,QAAQ,EACR,iBAAiB,EACjB,KAAK,GAAG,EAAE,EACV,SAAS,GAAG,CAAC,EACb,OAAO,GAAG,GAAG,GAChB,GAAG,KAAK;AAET,IAAA,QACIA,GAAA,CAAC,IAAI,EAAA,EAAA,QAAA,EACA,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAI;AACzB,YAAA,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC;AACpC,YAAA,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,UAAU,GAAG,OAAO,IAAI,KAAK,CAAC;YAClE,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC;YACvE,MAAM,QAAQ,GAAG,CAAC,QAAQ,KAAA,IAAA,IAAR,QAAQ,KAAA,MAAA,GAAR,QAAQ,GAAI,CAAC,IAAI,CAAC,IAAI,WAAW,KAAK,QAAQ,KAAA,IAAA,IAAR,QAAQ,KAAA,MAAA,GAAR,QAAQ,GAAI,CAAC,CAAC;AACtE,YAAA,MAAM,YAAY,GAAG,iBAAiB,aAAjB,iBAAiB,KAAA,MAAA,GAAA,MAAA,GAAjB,iBAAiB,CAAG;gBACrC,UAAU;gBACV,MAAM;gBACN,KAAK;AACL,gBAAA,QAAQ,EAAE,QAAQ,KAAA,IAAA,IAAR,QAAQ,KAAA,MAAA,GAAR,QAAQ,GAAI,CAAC;gBACvB,WAAW;gBACX,QAAQ;AACX,aAAA,CAAC;AAEF,YAAA,QACIA,GAAA,CAAC,GAAG,EAAA,EAAa,KAAK,EAAE;oBACpB,MAAM,EAAE,CAAA,EAAG,MAAM,CAAA,EAAA,CAAI;AACrB,oBAAA,GAAG,YAAY;iBAClB,EAAA,EAHS,KAAK,CAGX;QAEZ,CAAC,CAAC,EAAA,CACC;AAEf;;;;"}
@@ -0,0 +1,20 @@
1
+ import styled from 'styled-components';
2
+
3
+ const Bars = styled.div `
4
+ position: absolute;
5
+ inset: 0;
6
+ display: flex;
7
+ align-items: flex-end;
8
+ gap: 3px;
9
+ `;
10
+ const Bar = styled.div `
11
+ flex: 1;
12
+ min-width: 2px;
13
+ border-radius: 9999px;
14
+ background: var(--white, #ffffff);
15
+ opacity: 0.6;
16
+ transition: height 120ms ease, opacity 120ms ease;
17
+ `;
18
+
19
+ export { Bar, Bars };
20
+ //# sourceMappingURL=WaveformBars.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WaveformBars.styled.js","sources":["../../../src/components/WaveformBars/WaveformBars.styled.tsx"],"sourcesContent":["\nimport styled from 'styled-components';\n\nexport const Bars = styled.div`\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-end;\n gap: 3px;\n`;\n\nexport const Bar = styled.div`\n flex: 1;\n min-width: 2px;\n border-radius: 9999px;\n background: var(--white, #ffffff);\n opacity: 0.6;\n transition: height 120ms ease, opacity 120ms ease;\n`;\n"],"names":[],"mappings":";;AAGO,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;AAQvB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * AudioRecorder constants
3
+ *
4
+ * @author: exode <hello@exode.ru>
5
+ */
6
+ const AUDIO_LEVEL_BARS = 40;
7
+ /** Waveform decoding resamples to this rate — only the amplitude envelope matters */
8
+ const WAVEFORM_SAMPLE_RATE = 44100;
9
+ const createInitialLevels = () => Array.from({ length: AUDIO_LEVEL_BARS }, () => 0);
10
+ const TIMELINE_SAMPLE_INTERVAL = 150;
11
+ const TIMELINE_BAR_WIDTH = 3;
12
+ const TIMELINE_BAR_GAP = 2;
13
+ const TIMELINE_BAR_MIN_HEIGHT = 3;
14
+ const TIMELINE_BAR_MAX_HEIGHT = 24;
15
+
16
+ export { AUDIO_LEVEL_BARS, TIMELINE_BAR_GAP, TIMELINE_BAR_MAX_HEIGHT, TIMELINE_BAR_MIN_HEIGHT, TIMELINE_BAR_WIDTH, TIMELINE_SAMPLE_INTERVAL, WAVEFORM_SAMPLE_RATE, createInitialLevels };
17
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sources":["../src/constants.ts"],"sourcesContent":["/**\n * AudioRecorder constants\n *\n * @author: exode <hello@exode.ru>\n */\n\nexport const AUDIO_LEVEL_BARS = 40;\n\n/** Waveform decoding resamples to this rate — only the amplitude envelope matters */\nexport const WAVEFORM_SAMPLE_RATE = 44100;\n\nexport const createInitialLevels = (): number[] => Array.from({ length: AUDIO_LEVEL_BARS }, () => 0);\n\nexport const TIMELINE_SAMPLE_INTERVAL = 150;\nexport const TIMELINE_BAR_WIDTH = 3;\nexport const TIMELINE_BAR_GAP = 2;\nexport const TIMELINE_BAR_MIN_HEIGHT = 3;\nexport const TIMELINE_BAR_MAX_HEIGHT = 24;\n"],"names":[],"mappings":"AAAA;;;;AAIG;AAEI,MAAM,gBAAgB,GAAG;AAEhC;AACO,MAAM,oBAAoB,GAAG;MAEvB,mBAAmB,GAAG,MAAgB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,MAAM,CAAC;AAE5F,MAAM,wBAAwB,GAAG;AACjC,MAAM,kBAAkB,GAAG;AAC3B,MAAM,gBAAgB,GAAG;AACzB,MAAM,uBAAuB,GAAG;AAChC,MAAM,uBAAuB,GAAG;;;;"}
@@ -11,16 +11,12 @@ interface UseAudioBlobLoaderOptions {
11
11
  preloadedBlob?: Blob | null;
12
12
  playbackCallbacks: PlaybackCallbacks;
13
13
  }
14
- interface EnsureAudioLoadedOptions {
15
- /** Retry a src that already failed — only on explicit user intent, not on auto-prefetch */
16
- retryFailed?: boolean;
17
- }
18
14
  interface UseAudioBlobLoaderResult {
19
15
  audioRef: React.MutableRefObject<HTMLAudioElement | null>;
20
16
  isLoading: boolean;
21
17
  isLoaded: boolean;
22
18
  waveformLevels: number[];
23
- ensureAudioLoaded: (options?: EnsureAudioLoadedOptions) => Promise<void>;
19
+ ensureAudioLoaded: () => Promise<void>;
24
20
  }
25
21
  declare const useAudioBlobLoader: (options: UseAudioBlobLoaderOptions) => UseAudioBlobLoaderResult;
26
22
  export { useAudioBlobLoader };
@@ -1 +1 @@
1
- {"version":3,"file":"useAudioBlobLoader.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioBlobLoader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,UAAU,yBAAyB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,iBAAiB,CAAC;CACxC;AAED,UAAU,wBAAwB;IAC9B,2FAA2F;IAC3F,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,UAAU,wBAAwB;IAC9B,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,CAAC,OAAO,CAAC,EAAE,wBAAwB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,QAAA,MAAM,kBAAkB,GAAI,SAAS,yBAAyB,KAAG,wBA4KhE,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"useAudioBlobLoader.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioBlobLoader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,UAAU,yBAAyB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,iBAAiB,CAAC;CACxC;AAED,UAAU,wBAAwB;IAC9B,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,QAAA,MAAM,kBAAkB,GAAI,SAAS,yBAAyB,KAAG,wBAyLhE,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,175 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { createInitialLevels } from '../constants.js';
3
+ import { createPseudoLevels } from '../utils/pseudo-waveform.js';
4
+ import { createAudioPlayback, revokeObjectURL, createObjectURL, cleanupAudio } from '../platform/audio-playback-adapter.js';
5
+ import { decodeAudioLevels } from '../platform/audio-analyzer-adapter.js';
6
+ import { fetchAudioBlob } from '../platform/audio-blob-fetcher.js';
7
+
8
+ /**
9
+ * useAudioBlobLoader — loads audio blob from URL or preloaded data,
10
+ * creates object URL and decodes waveform levels.
11
+ *
12
+ * @author: exode <hello@exode.ru>
13
+ */
14
+ const useAudioBlobLoader = (options) => {
15
+ const { src, waveformData, preloadedBlob, playbackCallbacks } = options;
16
+ const [isLoading, setIsLoading] = useState(false);
17
+ const [isLoaded, setIsLoaded] = useState(false);
18
+ const [waveformLevels, setWaveformLevels] = useState(waveformData || createInitialLevels);
19
+ const audioRef = useRef(null);
20
+ const objectUrlRef = useRef(null);
21
+ const abortControllerRef = useRef(null);
22
+ const waveformTokenRef = useRef(0);
23
+ const waveformDataRef = useRef(waveformData);
24
+ const callbacksRef = useRef(playbackCallbacks);
25
+ waveformDataRef.current = waveformData;
26
+ callbacksRef.current = playbackCallbacks;
27
+ const createAudioFromUrl = useCallback((url) => {
28
+ const { audio } = createAudioPlayback(url, {
29
+ onLoadedMetadata: (dur) => callbacksRef.current.onLoadedMetadata(dur),
30
+ onTimeUpdate: (time) => callbacksRef.current.onTimeUpdate(time),
31
+ onEnded: () => callbacksRef.current.onEnded(),
32
+ onPlay: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPlay) === null || _b === void 0 ? void 0 : _b.call(_a); },
33
+ onPause: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPause) === null || _b === void 0 ? void 0 : _b.call(_a); },
34
+ onStop: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onStop) === null || _b === void 0 ? void 0 : _b.call(_a); },
35
+ onSeekBackward: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onSeekBackward) === null || _b === void 0 ? void 0 : _b.call(_a); },
36
+ onSeekForward: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onSeekForward) === null || _b === void 0 ? void 0 : _b.call(_a); },
37
+ onPreviousTrack: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPreviousTrack) === null || _b === void 0 ? void 0 : _b.call(_a); },
38
+ onNextTrack: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onNextTrack) === null || _b === void 0 ? void 0 : _b.call(_a); },
39
+ });
40
+ audioRef.current = audio;
41
+ setIsLoaded(true);
42
+ }, []);
43
+ /** Local recordings have no url of their own, so they still go through a blob */
44
+ const createAudioFromBlob = useCallback((blob) => {
45
+ if (objectUrlRef.current) {
46
+ revokeObjectURL(objectUrlRef.current);
47
+ }
48
+ objectUrlRef.current = createObjectURL(blob);
49
+ createAudioFromUrl(objectUrlRef.current);
50
+ }, [createAudioFromUrl]);
51
+ /**
52
+ * Waveform is cosmetic: some files (e.g. mp3 with junk before the first
53
+ * frame) fail to decode in Safari while playing fine, so a decode error
54
+ * must never block playback. Token keeps only the latest decode.
55
+ */
56
+ const applyWaveformLevels = useCallback(async (blob, seed) => {
57
+ /** Read through the ref: levels may arrive while the bytes are still loading */
58
+ if (waveformDataRef.current) {
59
+ return;
60
+ }
61
+ waveformTokenRef.current += 1;
62
+ const token = waveformTokenRef.current;
63
+ try {
64
+ const levels = await decodeAudioLevels(blob);
65
+ if (token === waveformTokenRef.current) {
66
+ setWaveformLevels(levels);
67
+ }
68
+ }
69
+ catch (error) {
70
+ console.warn('[voice-player] Failed to decode waveform', error);
71
+ /** Some runtimes decode nothing at all — draw a placeholder instead of a flat line */
72
+ if (token === waveformTokenRef.current) {
73
+ setWaveformLevels(createPseudoLevels(seed));
74
+ }
75
+ }
76
+ }, []);
77
+ const mountFromBlob = useCallback(async (blob) => {
78
+ if (audioRef.current) {
79
+ return;
80
+ }
81
+ setIsLoading(true);
82
+ try {
83
+ createAudioFromBlob(blob);
84
+ }
85
+ catch (error) {
86
+ console.warn('[voice-player] Failed to mount blob', error);
87
+ }
88
+ finally {
89
+ setIsLoading(false);
90
+ }
91
+ void applyWaveformLevels(blob, `${blob.size}`);
92
+ }, [applyWaveformLevels, createAudioFromBlob]);
93
+ /** Waveform bytes are cosmetic, so nothing here may delay playback */
94
+ const loadWaveformSource = useCallback(async (url, cached) => {
95
+ var _a;
96
+ if (waveformDataRef.current) {
97
+ return;
98
+ }
99
+ if (cached) {
100
+ return applyWaveformLevels(cached, url);
101
+ }
102
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
103
+ const controller = new AbortController();
104
+ abortControllerRef.current = controller;
105
+ try {
106
+ const blob = await fetchAudioBlob(url, controller.signal);
107
+ if (!controller.signal.aborted) {
108
+ await applyWaveformLevels(blob, url);
109
+ }
110
+ }
111
+ catch (error) {
112
+ if (error instanceof DOMException && error.name === 'AbortError') {
113
+ return;
114
+ }
115
+ console.warn('[voice-player] Failed to fetch waveform source', error);
116
+ }
117
+ }, [applyWaveformLevels]);
118
+ const ensureAudioLoaded = useCallback(async () => {
119
+ if (audioRef.current) {
120
+ return;
121
+ }
122
+ /** Only a recording that has not been uploaded yet has no url of its own */
123
+ if (!src) {
124
+ return preloadedBlob ? mountFromBlob(preloadedBlob) : undefined;
125
+ }
126
+ /**
127
+ * Remote files play straight from their url: it streams with range
128
+ * requests, keeps the user gesture (no await before play) and avoids
129
+ * blob sources, which some WebKit runtimes refuse to render.
130
+ */
131
+ createAudioFromUrl(src);
132
+ void loadWaveformSource(src, preloadedBlob);
133
+ }, [src, preloadedBlob, mountFromBlob, loadWaveformSource, createAudioFromUrl]);
134
+ /** Cleanup on src change or unmount */
135
+ useEffect(() => {
136
+ /** Drop the previous waveform so a failed decode cannot leave a foreign one */
137
+ setWaveformLevels(waveformDataRef.current || createInitialLevels());
138
+ return () => {
139
+ var _a;
140
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
141
+ waveformTokenRef.current += 1;
142
+ if (audioRef.current) {
143
+ cleanupAudio(audioRef.current);
144
+ audioRef.current = null;
145
+ }
146
+ if (objectUrlRef.current) {
147
+ revokeObjectURL(objectUrlRef.current);
148
+ objectUrlRef.current = null;
149
+ }
150
+ setIsLoaded(false);
151
+ };
152
+ }, [src]);
153
+ /** A cached recording mounts on its own only while it has no url yet */
154
+ useEffect(() => {
155
+ if (preloadedBlob && !src && !audioRef.current) {
156
+ void mountFromBlob(preloadedBlob);
157
+ }
158
+ }, [src, preloadedBlob, mountFromBlob]);
159
+ useEffect(() => {
160
+ if (waveformData) {
161
+ waveformTokenRef.current += 1;
162
+ setWaveformLevels(waveformData);
163
+ }
164
+ }, [waveformData]);
165
+ return {
166
+ audioRef,
167
+ isLoading,
168
+ isLoaded,
169
+ waveformLevels,
170
+ ensureAudioLoaded,
171
+ };
172
+ };
173
+
174
+ export { useAudioBlobLoader };
175
+ //# sourceMappingURL=useAudioBlobLoader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAudioBlobLoader.js","sources":["../../src/hooks/useAudioBlobLoader.ts"],"sourcesContent":["/**\n * useAudioBlobLoader — loads audio blob from URL or preloaded data,\n * creates object URL and decodes waveform levels.\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { createInitialLevels } from '../constants';\nimport { createPseudoLevels } from '../utils';\n\nimport type { PlaybackCallbacks } from '../platform';\n\nimport {\n cleanupAudio,\n createAudioPlayback,\n createObjectURL,\n decodeAudioLevels,\n fetchAudioBlob,\n revokeObjectURL,\n} from '../platform';\n\n\ninterface UseAudioBlobLoaderOptions {\n src: string;\n waveformData?: number[];\n preloadedBlob?: Blob | null;\n playbackCallbacks: PlaybackCallbacks;\n}\n\ninterface UseAudioBlobLoaderResult {\n audioRef: React.MutableRefObject<HTMLAudioElement | null>;\n isLoading: boolean;\n isLoaded: boolean;\n waveformLevels: number[];\n ensureAudioLoaded: () => Promise<void>;\n}\n\nconst useAudioBlobLoader = (options: UseAudioBlobLoaderOptions): UseAudioBlobLoaderResult => {\n\n const { src, waveformData, preloadedBlob, playbackCallbacks } = options;\n\n const [ isLoading, setIsLoading ] = useState(false);\n const [ isLoaded, setIsLoaded ] = useState(false);\n const [ waveformLevels, setWaveformLevels ] = useState<number[]>(waveformData || createInitialLevels);\n\n const audioRef = useRef<HTMLAudioElement | null>(null);\n const objectUrlRef = useRef<string | null>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n const waveformTokenRef = useRef(0);\n const waveformDataRef = useRef(waveformData);\n const callbacksRef = useRef(playbackCallbacks);\n waveformDataRef.current = waveformData;\n callbacksRef.current = playbackCallbacks;\n\n const createAudioFromUrl = useCallback((url: string) => {\n const { audio } = createAudioPlayback(url, {\n onLoadedMetadata: (dur) => callbacksRef.current.onLoadedMetadata(dur),\n onTimeUpdate: (time) => callbacksRef.current.onTimeUpdate(time),\n onEnded: () => callbacksRef.current.onEnded(),\n onPlay: () => callbacksRef.current.onPlay?.(),\n onPause: () => callbacksRef.current.onPause?.(),\n onStop: () => callbacksRef.current.onStop?.(),\n onSeekBackward: () => callbacksRef.current.onSeekBackward?.(),\n onSeekForward: () => callbacksRef.current.onSeekForward?.(),\n onPreviousTrack: () => callbacksRef.current.onPreviousTrack?.(),\n onNextTrack: () => callbacksRef.current.onNextTrack?.(),\n });\n\n audioRef.current = audio;\n setIsLoaded(true);\n }, []);\n\n /** Local recordings have no url of their own, so they still go through a blob */\n const createAudioFromBlob = useCallback((blob: Blob) => {\n if (objectUrlRef.current) {\n revokeObjectURL(objectUrlRef.current);\n }\n\n objectUrlRef.current = createObjectURL(blob);\n\n createAudioFromUrl(objectUrlRef.current);\n }, [ createAudioFromUrl ]);\n\n /**\n * Waveform is cosmetic: some files (e.g. mp3 with junk before the first\n * frame) fail to decode in Safari while playing fine, so a decode error\n * must never block playback. Token keeps only the latest decode.\n */\n const applyWaveformLevels = useCallback(async (blob: Blob, seed: string) => {\n /** Read through the ref: levels may arrive while the bytes are still loading */\n if (waveformDataRef.current) {\n return;\n }\n\n waveformTokenRef.current += 1;\n\n const token = waveformTokenRef.current;\n\n try {\n const levels = await decodeAudioLevels(blob);\n\n if (token === waveformTokenRef.current) {\n setWaveformLevels(levels);\n }\n } catch (error) {\n console.warn('[voice-player] Failed to decode waveform', error);\n\n /** Some runtimes decode nothing at all — draw a placeholder instead of a flat line */\n if (token === waveformTokenRef.current) {\n setWaveformLevels(createPseudoLevels(seed));\n }\n }\n }, []);\n\n const mountFromBlob = useCallback(async (blob: Blob) => {\n if (audioRef.current) {\n return;\n }\n\n setIsLoading(true);\n try {\n createAudioFromBlob(blob);\n } catch (error) {\n console.warn('[voice-player] Failed to mount blob', error);\n } finally {\n setIsLoading(false);\n }\n\n void applyWaveformLevels(blob, `${blob.size}`);\n }, [ applyWaveformLevels, createAudioFromBlob ]);\n\n /** Waveform bytes are cosmetic, so nothing here may delay playback */\n const loadWaveformSource = useCallback(async (url: string, cached?: Blob | null) => {\n if (waveformDataRef.current) {\n return;\n }\n\n if (cached) {\n return applyWaveformLevels(cached, url);\n }\n\n abortControllerRef.current?.abort();\n\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n try {\n const blob = await fetchAudioBlob(url, controller.signal);\n\n if (!controller.signal.aborted) {\n await applyWaveformLevels(blob, url);\n }\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n return;\n }\n\n console.warn('[voice-player] Failed to fetch waveform source', error);\n }\n }, [ applyWaveformLevels ]);\n\n const ensureAudioLoaded = useCallback(async () => {\n if (audioRef.current) {\n return;\n }\n\n /** Only a recording that has not been uploaded yet has no url of its own */\n if (!src) {\n return preloadedBlob ? mountFromBlob(preloadedBlob) : undefined;\n }\n\n /**\n * Remote files play straight from their url: it streams with range\n * requests, keeps the user gesture (no await before play) and avoids\n * blob sources, which some WebKit runtimes refuse to render.\n */\n createAudioFromUrl(src);\n\n void loadWaveformSource(src, preloadedBlob);\n }, [ src, preloadedBlob, mountFromBlob, loadWaveformSource, createAudioFromUrl ]);\n\n /** Cleanup on src change or unmount */\n useEffect(() => {\n /** Drop the previous waveform so a failed decode cannot leave a foreign one */\n setWaveformLevels(waveformDataRef.current || createInitialLevels());\n\n return () => {\n abortControllerRef.current?.abort();\n waveformTokenRef.current += 1;\n if (audioRef.current) {\n cleanupAudio(audioRef.current);\n audioRef.current = null;\n }\n if (objectUrlRef.current) {\n revokeObjectURL(objectUrlRef.current);\n objectUrlRef.current = null;\n }\n setIsLoaded(false);\n };\n }, [ src ]);\n\n /** A cached recording mounts on its own only while it has no url yet */\n useEffect(() => {\n if (preloadedBlob && !src && !audioRef.current) {\n void mountFromBlob(preloadedBlob);\n }\n }, [ src, preloadedBlob, mountFromBlob ]);\n\n useEffect(() => {\n if (waveformData) {\n waveformTokenRef.current += 1;\n setWaveformLevels(waveformData);\n }\n }, [ waveformData ]);\n\n return {\n audioRef,\n isLoading,\n isLoaded,\n waveformLevels,\n ensureAudioLoaded,\n };\n};\n\n\nexport { useAudioBlobLoader };\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ import { useState, useRef, useCallback } from 'react';
2
+ import { cleanupAudio, revokeObjectURL, createObjectURL, createAudioPlayback } from '../platform/audio-playback-adapter.js';
3
+
4
+ /**
5
+ * useAudioPlayback
6
+ *
7
+ * @author: exode <hello@exode.ru>
8
+ */
9
+ const useAudioPlayback = () => {
10
+ const [audioURL, setAudioURL] = useState(null);
11
+ const [isPlaying, setIsPlaying] = useState(false);
12
+ const [playbackTime, setPlaybackTime] = useState(0);
13
+ const [playbackDuration, setPlaybackDuration] = useState(0);
14
+ const audioRef = useRef(null);
15
+ const cleanupAudioElement = useCallback(() => {
16
+ const audio = audioRef.current;
17
+ if (!audio) {
18
+ return;
19
+ }
20
+ cleanupAudio(audio);
21
+ audioRef.current = null;
22
+ }, []);
23
+ const setAudioUrlSafely = useCallback((value) => {
24
+ setAudioURL((prev) => {
25
+ if (prev) {
26
+ revokeObjectURL(prev);
27
+ }
28
+ return value;
29
+ });
30
+ }, []);
31
+ const resetPlayback = useCallback(() => {
32
+ setPlaybackTime(0);
33
+ setPlaybackDuration(0);
34
+ setIsPlaying(false);
35
+ setAudioUrlSafely(null);
36
+ cleanupAudioElement();
37
+ }, [cleanupAudioElement, setAudioUrlSafely]);
38
+ const mountBlob = useCallback((blob) => {
39
+ const url = createObjectURL(blob);
40
+ setAudioUrlSafely(url);
41
+ cleanupAudioElement();
42
+ const { audio } = createAudioPlayback(url, {
43
+ onLoadedMetadata: (duration) => setPlaybackDuration(duration),
44
+ onTimeUpdate: (currentTime) => setPlaybackTime(currentTime),
45
+ onEnded: () => {
46
+ setIsPlaying(false);
47
+ setPlaybackTime(0);
48
+ },
49
+ onPlay: () => setIsPlaying(true),
50
+ onPause: () => setIsPlaying(false),
51
+ });
52
+ audioRef.current = audio;
53
+ setPlaybackTime(0);
54
+ setPlaybackDuration(0);
55
+ setIsPlaying(false);
56
+ return url;
57
+ }, [cleanupAudioElement, setAudioUrlSafely]);
58
+ const togglePlayback = useCallback((onError) => {
59
+ if (!audioURL) {
60
+ return;
61
+ }
62
+ if (!audioRef.current) {
63
+ const { audio } = createAudioPlayback(audioURL, {
64
+ onLoadedMetadata: (duration) => setPlaybackDuration(duration),
65
+ onTimeUpdate: (currentTime) => setPlaybackTime(currentTime),
66
+ onEnded: () => {
67
+ setIsPlaying(false);
68
+ setPlaybackTime(0);
69
+ },
70
+ onPlay: () => setIsPlaying(true),
71
+ onPause: () => setIsPlaying(false),
72
+ });
73
+ audioRef.current = audio;
74
+ }
75
+ const audio = audioRef.current;
76
+ if (!audio) {
77
+ return;
78
+ }
79
+ if (audio.paused) {
80
+ if (audio.duration > 0 && playbackDuration > 0) {
81
+ audio.currentTime = Math.min(playbackTime, audio.duration);
82
+ }
83
+ audio.play()
84
+ .then(() => setIsPlaying(true))
85
+ .catch((error) => {
86
+ setIsPlaying(false);
87
+ onError === null || onError === void 0 ? void 0 : onError(error);
88
+ });
89
+ }
90
+ else {
91
+ audio.pause();
92
+ setIsPlaying(false);
93
+ }
94
+ }, [audioURL, playbackDuration, playbackTime]);
95
+ const stopPlayback = useCallback(() => {
96
+ const audio = audioRef.current;
97
+ if (!audio) {
98
+ return;
99
+ }
100
+ audio.pause();
101
+ audio.currentTime = 0;
102
+ setIsPlaying(false);
103
+ setPlaybackTime(0);
104
+ }, []);
105
+ return {
106
+ audioURL,
107
+ isPlaying,
108
+ mountBlob,
109
+ playbackTime,
110
+ stopPlayback,
111
+ resetPlayback,
112
+ togglePlayback,
113
+ playbackDuration,
114
+ cleanupAudioElement,
115
+ };
116
+ };
117
+
118
+ export { useAudioPlayback };
119
+ //# sourceMappingURL=useAudioPlayback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAudioPlayback.js","sources":["../../src/hooks/useAudioPlayback.ts"],"sourcesContent":["/**\n * useAudioPlayback\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback, useRef, useState } from 'react';\n\nimport { cleanupAudio, createAudioPlayback, createObjectURL, revokeObjectURL } from '../platform';\n\n\ninterface UseAudioPlaybackResult {\n audioURL: string | null;\n isPlaying: boolean;\n playbackTime: number;\n playbackDuration: number;\n mountBlob: (blob: Blob) => string;\n togglePlayback: (onError?: (error: unknown) => void) => void;\n stopPlayback: () => void;\n resetPlayback: () => void;\n cleanupAudioElement: () => void;\n}\n\nconst useAudioPlayback = (): UseAudioPlaybackResult => {\n\n const [ audioURL, setAudioURL ] = useState<string | null>(null);\n const [ isPlaying, setIsPlaying ] = useState(false);\n const [ playbackTime, setPlaybackTime ] = useState(0);\n const [ playbackDuration, setPlaybackDuration ] = useState(0);\n\n const audioRef = useRef<HTMLAudioElement | null>(null);\n\n const cleanupAudioElement = useCallback(() => {\n const audio = audioRef.current;\n if (!audio) {\n return;\n }\n\n cleanupAudio(audio);\n audioRef.current = null;\n }, []);\n\n const setAudioUrlSafely = useCallback((value: string | null) => {\n setAudioURL((prev) => {\n if (prev) {\n revokeObjectURL(prev);\n }\n\n return value;\n });\n }, []);\n\n const resetPlayback = useCallback(() => {\n setPlaybackTime(0);\n setPlaybackDuration(0);\n setIsPlaying(false);\n setAudioUrlSafely(null);\n cleanupAudioElement();\n }, [ cleanupAudioElement, setAudioUrlSafely ]);\n\n const mountBlob = useCallback((blob: Blob): string => {\n const url = createObjectURL(blob);\n\n setAudioUrlSafely(url);\n cleanupAudioElement();\n\n const { audio } = createAudioPlayback(url, {\n onLoadedMetadata: (duration) => setPlaybackDuration(duration),\n onTimeUpdate: (currentTime) => setPlaybackTime(currentTime),\n onEnded: () => {\n setIsPlaying(false);\n setPlaybackTime(0);\n },\n onPlay: () => setIsPlaying(true),\n onPause: () => setIsPlaying(false),\n });\n\n audioRef.current = audio;\n setPlaybackTime(0);\n setPlaybackDuration(0);\n setIsPlaying(false);\n\n return url;\n }, [ cleanupAudioElement, setAudioUrlSafely ]);\n\n const togglePlayback = useCallback((onError?: (error: unknown) => void) => {\n if (!audioURL) {\n return;\n }\n\n if (!audioRef.current) {\n const { audio } = createAudioPlayback(audioURL, {\n onLoadedMetadata: (duration) => setPlaybackDuration(duration),\n onTimeUpdate: (currentTime) => setPlaybackTime(currentTime),\n onEnded: () => {\n setIsPlaying(false);\n setPlaybackTime(0);\n },\n onPlay: () => setIsPlaying(true),\n onPause: () => setIsPlaying(false),\n });\n\n audioRef.current = audio;\n }\n\n const audio = audioRef.current;\n if (!audio) {\n return;\n }\n\n if (audio.paused) {\n if (audio.duration > 0 && playbackDuration > 0) {\n audio.currentTime = Math.min(playbackTime, audio.duration);\n }\n\n audio.play()\n .then(() => setIsPlaying(true))\n .catch((error) => {\n setIsPlaying(false);\n onError?.(error);\n });\n } else {\n audio.pause();\n setIsPlaying(false);\n }\n }, [ audioURL, playbackDuration, playbackTime ]);\n\n const stopPlayback = useCallback(() => {\n const audio = audioRef.current;\n if (!audio) {\n return;\n }\n\n audio.pause();\n audio.currentTime = 0;\n setIsPlaying(false);\n setPlaybackTime(0);\n }, []);\n\n return {\n audioURL,\n isPlaying,\n mountBlob,\n playbackTime,\n stopPlayback,\n resetPlayback,\n togglePlayback,\n playbackDuration,\n cleanupAudioElement,\n };\n};\n\n\nexport { useAudioPlayback };\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ import { useRef, useState, useCallback, useEffect } from 'react';
2
+
3
+ /**
4
+ * useAudioRecorderHandlers — recording/send/cancel handlers for AudioRecorder
5
+ *
6
+ * @author: exode <hello@exode.ru>
7
+ */
8
+ const useAudioRecorderHandlers = (options) => {
9
+ const { vibrate, recorder, messages, onUploadError, } = options;
10
+ const { audioURL, isPaused, isRecording, hasRecording, stopPlayback, stopRecording, sendRecording, togglePlayback, startRecording, cancelRecording, deleteRecording, toggleRecordingPause, } = recorder;
11
+ const shouldSendAfterStopRef = useRef(false);
12
+ const [isSending, setIsSending] = useState(false);
13
+ const startNewRecording = useCallback(async () => {
14
+ if (isRecording) {
15
+ return;
16
+ }
17
+ try {
18
+ await startRecording();
19
+ vibrate(50);
20
+ }
21
+ catch (error) {
22
+ onUploadError === null || onUploadError === void 0 ? void 0 : onUploadError(error);
23
+ }
24
+ }, [
25
+ vibrate,
26
+ isRecording,
27
+ onUploadError,
28
+ startRecording,
29
+ ]);
30
+ const handleRecordToggle = useCallback(() => {
31
+ if (!isRecording) {
32
+ if (audioURL) {
33
+ stopPlayback();
34
+ deleteRecording();
35
+ }
36
+ void startNewRecording();
37
+ return;
38
+ }
39
+ if (isPaused) {
40
+ stopPlayback();
41
+ }
42
+ toggleRecordingPause();
43
+ }, [
44
+ audioURL,
45
+ isPaused,
46
+ isRecording,
47
+ stopPlayback,
48
+ deleteRecording,
49
+ startNewRecording,
50
+ toggleRecordingPause,
51
+ ]);
52
+ const handleSend = useCallback(async () => {
53
+ shouldSendAfterStopRef.current = false;
54
+ setIsSending(true);
55
+ try {
56
+ stopPlayback();
57
+ await sendRecording();
58
+ }
59
+ catch (error) {
60
+ const uploadError = error instanceof Error
61
+ ? error
62
+ : new Error(messages.errors.loadFileFailed);
63
+ onUploadError === null || onUploadError === void 0 ? void 0 : onUploadError(uploadError);
64
+ }
65
+ finally {
66
+ setIsSending(false);
67
+ }
68
+ }, [
69
+ stopPlayback,
70
+ onUploadError,
71
+ sendRecording,
72
+ messages.errors.loadFileFailed,
73
+ ]);
74
+ const handleSendRequest = useCallback(() => {
75
+ if (isSending) {
76
+ return;
77
+ }
78
+ if (isRecording) {
79
+ setIsSending(true);
80
+ shouldSendAfterStopRef.current = true;
81
+ stopRecording();
82
+ return;
83
+ }
84
+ void handleSend();
85
+ }, [
86
+ isSending,
87
+ handleSend,
88
+ isRecording,
89
+ stopRecording,
90
+ ]);
91
+ const handleCancel = useCallback(() => {
92
+ shouldSendAfterStopRef.current = false;
93
+ if (isRecording) {
94
+ cancelRecording();
95
+ return;
96
+ }
97
+ if (audioURL) {
98
+ stopPlayback();
99
+ deleteRecording();
100
+ }
101
+ }, [
102
+ audioURL,
103
+ isRecording,
104
+ stopPlayback,
105
+ deleteRecording,
106
+ cancelRecording,
107
+ ]);
108
+ const handlePlaybackToggle = useCallback(() => {
109
+ if (!hasRecording) {
110
+ return;
111
+ }
112
+ togglePlayback();
113
+ }, [hasRecording, togglePlayback]);
114
+ /** Auto-send after stop */
115
+ useEffect(() => {
116
+ if (!isRecording
117
+ && hasRecording
118
+ && shouldSendAfterStopRef.current) {
119
+ shouldSendAfterStopRef.current = false;
120
+ void handleSend();
121
+ }
122
+ }, [
123
+ handleSend,
124
+ isRecording,
125
+ hasRecording,
126
+ ]);
127
+ return {
128
+ isSending,
129
+ handleCancel,
130
+ handleSendRequest,
131
+ handleRecordToggle,
132
+ handlePlaybackToggle,
133
+ };
134
+ };
135
+
136
+ export { useAudioRecorderHandlers };
137
+ //# sourceMappingURL=useAudioRecorderHandlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAudioRecorderHandlers.js","sources":["../../src/hooks/useAudioRecorderHandlers.ts"],"sourcesContent":["/**\n * useAudioRecorderHandlers — recording/send/cancel handlers for AudioRecorder\n *\n * @author: exode <hello@exode.ru>\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport type { UseVoiceRecorderResult } from '../types';\nimport type { RecorderMessages } from '../components/AudioRecorder.messages';\n\n\ninterface UseAudioRecorderHandlersOptions {\n vibrate: (pattern: number | number[]) => void;\n messages: RecorderMessages;\n recorder: UseVoiceRecorderResult;\n onUploadError?: (error: unknown) => void;\n}\n\n\nconst useAudioRecorderHandlers = (options: UseAudioRecorderHandlersOptions) => {\n\n const {\n vibrate,\n recorder,\n messages,\n onUploadError,\n } = options;\n\n const {\n audioURL,\n isPaused,\n isRecording,\n hasRecording,\n stopPlayback,\n stopRecording,\n sendRecording,\n togglePlayback,\n startRecording,\n cancelRecording,\n deleteRecording,\n toggleRecordingPause,\n } = recorder;\n\n const shouldSendAfterStopRef = useRef(false);\n\n const [ isSending, setIsSending ] = useState(false);\n\n const startNewRecording = useCallback(async () => {\n if (isRecording) {\n return;\n }\n\n try {\n await startRecording();\n vibrate(50);\n } catch (error) {\n onUploadError?.(error);\n }\n }, [\n vibrate,\n isRecording,\n onUploadError,\n startRecording,\n ]);\n\n const handleRecordToggle = useCallback(() => {\n if (!isRecording) {\n if (audioURL) {\n stopPlayback();\n deleteRecording();\n }\n\n void startNewRecording();\n return;\n }\n\n if (isPaused) {\n stopPlayback();\n }\n\n toggleRecordingPause();\n }, [\n audioURL,\n isPaused,\n isRecording,\n stopPlayback,\n deleteRecording,\n startNewRecording,\n toggleRecordingPause,\n ]);\n\n const handleSend = useCallback(async () => {\n shouldSendAfterStopRef.current = false;\n setIsSending(true);\n try {\n stopPlayback();\n\n await sendRecording();\n } catch (error) {\n const uploadError = error instanceof Error\n ? error\n : new Error(messages.errors.loadFileFailed);\n\n onUploadError?.(uploadError);\n } finally {\n setIsSending(false);\n }\n }, [\n stopPlayback,\n onUploadError,\n sendRecording,\n messages.errors.loadFileFailed,\n ]);\n\n const handleSendRequest = useCallback(() => {\n if (isSending) {\n return;\n }\n\n if (isRecording) {\n setIsSending(true);\n shouldSendAfterStopRef.current = true;\n stopRecording();\n\n return;\n }\n\n void handleSend();\n }, [\n isSending,\n handleSend,\n isRecording,\n stopRecording,\n ]);\n\n const handleCancel = useCallback(() => {\n shouldSendAfterStopRef.current = false;\n\n if (isRecording) {\n cancelRecording();\n return;\n }\n\n if (audioURL) {\n stopPlayback();\n deleteRecording();\n }\n }, [\n audioURL,\n isRecording,\n stopPlayback,\n deleteRecording,\n cancelRecording,\n ]);\n\n const handlePlaybackToggle = useCallback(() => {\n if (!hasRecording) {\n return;\n }\n\n togglePlayback();\n }, [ hasRecording, togglePlayback ]);\n\n /** Auto-send after stop */\n useEffect(() => {\n if (!isRecording\n && hasRecording\n && shouldSendAfterStopRef.current\n ) {\n shouldSendAfterStopRef.current = false;\n\n void handleSend();\n }\n }, [\n handleSend,\n isRecording,\n hasRecording,\n ]);\n\n return {\n isSending,\n handleCancel,\n handleSendRequest,\n handleRecordToggle,\n handlePlaybackToggle,\n };\n};\n\n\nexport { useAudioRecorderHandlers };\n"],"names":[],"mappings":"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"}