@exode-team/react-recorder 1.1.7 → 1.1.9

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 (29) hide show
  1. package/dist/components/AudioRecorder.d.ts +1 -1
  2. package/dist/components/AudioRecorder.d.ts.map +1 -1
  3. package/dist/components/RecordingShell/RecordingShell.d.ts +1 -0
  4. package/dist/components/RecordingShell/RecordingShell.d.ts.map +1 -1
  5. package/dist/components/RecordingShell/RecordingShell.styled.d.ts +0 -2
  6. package/dist/components/RecordingShell/RecordingShell.styled.d.ts.map +1 -1
  7. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts +1 -0
  8. package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts.map +1 -1
  9. package/dist/components/WaveformBars/WaveformBars.d.ts +4 -0
  10. package/dist/components/WaveformBars/WaveformBars.d.ts.map +1 -1
  11. package/dist/components/WaveformBars/WaveformBars.styled.d.ts +3 -0
  12. package/dist/components/WaveformBars/WaveformBars.styled.d.ts.map +1 -0
  13. package/dist/hooks/useAudioRecorderHandlers.d.ts.map +1 -1
  14. package/dist/hooks/useMediaRecorder.d.ts.map +1 -1
  15. package/dist/hooks/useRecordingSender.d.ts +2 -1
  16. package/dist/hooks/useRecordingSender.d.ts.map +1 -1
  17. package/dist/hooks/useRecordingTiming.d.ts +2 -0
  18. package/dist/hooks/useRecordingTiming.d.ts.map +1 -1
  19. package/dist/hooks/useVoiceMessagePlayer.d.ts.map +1 -1
  20. package/dist/hooks/useVoiceRecorder.d.ts.map +1 -1
  21. package/dist/hooks/useVoiceRecorderInput.d.ts +1 -1
  22. package/dist/hooks/useVoiceRecorderInput.d.ts.map +1 -1
  23. package/dist/index.cjs +149 -54
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.js +149 -54
  26. package/dist/index.js.map +1 -1
  27. package/dist/types/recorder.d.ts +10 -2
  28. package/dist/types/recorder.d.ts.map +1 -1
  29. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -274,6 +274,9 @@ const useMediaRecorder = (callbacks) => {
274
274
  if (!mediaRecorderRef.current || !isRecording) {
275
275
  return;
276
276
  }
277
+ if (mediaRecorderRef.current.state === 'paused') {
278
+ mediaRecorderRef.current.resume();
279
+ }
277
280
  mediaRecorderRef.current.stop();
278
281
  }, [isRecording]);
279
282
  const cancelRecording = useCallback(() => {
@@ -432,6 +435,7 @@ const useAudioPlayback = () => {
432
435
  * @author: exode <hello@exode.ru>
433
436
  */
434
437
  const useRecordingTiming = () => {
438
+ const [lastDuration, setLastDuration] = useState(0);
435
439
  const [recordingTime, setRecordingTime] = useState(0);
436
440
  const timerRef = useRef(null);
437
441
  const recordingStartRef = useRef(null);
@@ -459,6 +463,7 @@ const useRecordingTiming = () => {
459
463
  const startNewSession = useCallback(() => {
460
464
  recordingAccumulatedRef.current = 0;
461
465
  setRecordingTime(0);
466
+ setLastDuration(0);
462
467
  recordingStartRef.current = performance.now();
463
468
  startTicker();
464
469
  }, [startTicker]);
@@ -476,7 +481,21 @@ const useRecordingTiming = () => {
476
481
  setRecordingTime(roundTo(recordingAccumulatedRef.current));
477
482
  stopTicker();
478
483
  }, [stopTicker]);
484
+ const getElapsedMs = useCallback(() => {
485
+ const accumulated = recordingAccumulatedRef.current;
486
+ const elapsed = recordingStartRef.current !== null
487
+ ? (performance.now() - recordingStartRef.current) / 1000 + accumulated
488
+ : accumulated;
489
+ return Math.round(elapsed * 1000);
490
+ }, []);
479
491
  const resetSession = useCallback(() => {
492
+ const accumulated = recordingAccumulatedRef.current;
493
+ const elapsed = recordingStartRef.current !== null
494
+ ? (performance.now() - recordingStartRef.current) / 1000 + accumulated
495
+ : accumulated;
496
+ if (elapsed > 0) {
497
+ setLastDuration(roundTo(elapsed));
498
+ }
480
499
  stopTicker();
481
500
  recordingStartRef.current = null;
482
501
  recordingAccumulatedRef.current = 0;
@@ -488,6 +507,8 @@ const useRecordingTiming = () => {
488
507
  };
489
508
  }, [stopTicker]);
490
509
  return {
510
+ lastDuration,
511
+ getElapsedMs,
491
512
  pauseSession,
492
513
  resetSession,
493
514
  recordingTime,
@@ -589,7 +610,7 @@ const useRecorderVisualization = () => {
589
610
  * @author: exode <hello@exode.ru>
590
611
  */
591
612
  const useRecordingSender = (options) => {
592
- const { uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
613
+ const { uploader, fileName, mimeType, uploadId, audioBlob, onUploaded, getDurationMs, onRecordingError, deleteRecording, autoResetAfterUpload, } = options;
593
614
  const sendRecording = useCallback(async () => {
594
615
  if (!audioBlob) {
595
616
  const error = new Error('Voice recording is not available');
@@ -602,11 +623,13 @@ const useRecordingSender = (options) => {
602
623
  throw error;
603
624
  }
604
625
  try {
626
+ const durationMs = getDurationMs();
627
+ const durationSec = durationMs / 1000;
605
628
  const file = new File([audioBlob], fileName, {
606
629
  type: audioBlob.type || mimeType,
607
630
  });
608
631
  const uploaded = await uploader(file, { uploadId });
609
- await onUploaded(uploaded);
632
+ await onUploaded(uploaded, durationSec);
610
633
  if (autoResetAfterUpload) {
611
634
  deleteRecording();
612
635
  }
@@ -622,8 +645,9 @@ const useRecordingSender = (options) => {
622
645
  audioBlob,
623
646
  mimeType,
624
647
  onUploaded,
625
- onRecordingError,
648
+ getDurationMs,
626
649
  deleteRecording,
650
+ onRecordingError,
627
651
  autoResetAfterUpload,
628
652
  ]);
629
653
  return { sendRecording };
@@ -639,7 +663,7 @@ function useVoiceRecorder(props) {
639
663
  const [audioBlob, setAudioBlob] = useState(null);
640
664
  const { audioURL, isPlaying, mountBlob, playbackTime, stopPlayback, resetPlayback, togglePlayback: _togglePlayback, playbackDuration, } = useAudioPlayback();
641
665
  const { audioLevels, previewLevels, resetAudioLevels, stopVisualization, resetPreviewLevels, resumeVisualization, cleanupVisualization, generatePreviewLevels, initializeVisualization, } = useRecorderVisualization();
642
- const { recordingTime, pauseSession, resetSession, resumeSession, startNewSession, } = useRecordingTiming();
666
+ const { lastDuration, getElapsedMs, recordingTime, pauseSession, resetSession, resumeSession, startNewSession, } = useRecordingTiming();
643
667
  const resetCurrentRecording = useCallback((opts) => {
644
668
  var _a;
645
669
  setAudioBlob(null);
@@ -667,10 +691,11 @@ function useVoiceRecorder(props) {
667
691
  mimeType,
668
692
  uploadId,
669
693
  uploader,
670
- autoResetAfterUpload,
671
694
  onUploaded,
672
- onRecordingError,
673
695
  deleteRecording,
696
+ onRecordingError,
697
+ autoResetAfterUpload,
698
+ getDurationMs: getElapsedMs,
674
699
  });
675
700
  const mountRecordingBlob = useCallback((blob) => {
676
701
  setAudioBlob(blob);
@@ -753,6 +778,7 @@ function useVoiceRecorder(props) {
753
778
  isPlaying,
754
779
  isRecording,
755
780
  audioLevels,
781
+ lastDuration,
756
782
  playbackTime,
757
783
  recordingTime,
758
784
  previewLevels,
@@ -1017,15 +1043,44 @@ const useVoiceMessagePlayer = (options) => {
1017
1043
  setIsPlaying(false);
1018
1044
  }
1019
1045
  }, [isLoading, stopThis, audioRef]);
1020
- const seekTo = useCallback((seekProgress) => {
1046
+ const seekTo = useCallback((seekPercent) => {
1021
1047
  const audio = audioRef.current;
1022
1048
  if (!audio || !isFinite(audio.duration)) {
1023
1049
  return;
1024
1050
  }
1025
- const time = seekProgress * audio.duration;
1051
+ const time = seekPercent * audio.duration;
1026
1052
  audio.currentTime = time;
1027
1053
  setCurrentTime(time);
1028
1054
  }, [audioRef]);
1055
+ /** Seek to a specific time in seconds and start playback */
1056
+ const seekToTime = useCallback((seconds) => {
1057
+ const audio = audioRef.current;
1058
+ if (audio && isFinite(audio.duration)) {
1059
+ audioPlaybackManager.stopOthers(stopThis);
1060
+ audio.currentTime = Math.max(0, Math.min(seconds, audio.duration));
1061
+ setCurrentTime(audio.currentTime);
1062
+ audio.play()
1063
+ .then(() => setIsPlaying(true))
1064
+ .catch(() => setIsPlaying(false));
1065
+ }
1066
+ else {
1067
+ void ensureAudioLoaded().then(() => {
1068
+ const loadedAudio = audioRef.current;
1069
+ if (loadedAudio) {
1070
+ audioPlaybackManager.stopOthers(stopThis);
1071
+ loadedAudio.currentTime = Math.max(0, Math.min(seconds, loadedAudio.duration || seconds));
1072
+ setCurrentTime(loadedAudio.currentTime);
1073
+ loadedAudio.play()
1074
+ .then(() => setIsPlaying(true))
1075
+ .catch(() => setIsPlaying(false));
1076
+ }
1077
+ });
1078
+ }
1079
+ }, [
1080
+ audioRef,
1081
+ stopThis,
1082
+ ensureAudioLoaded,
1083
+ ]);
1029
1084
  /** Sync external duration */
1030
1085
  useEffect(() => {
1031
1086
  if (externalDuration) {
@@ -1042,6 +1097,7 @@ const useVoiceMessagePlayer = (options) => {
1042
1097
  currentTime,
1043
1098
  waveformLevels,
1044
1099
  seekTo,
1100
+ seekToTime,
1045
1101
  prefetch,
1046
1102
  preload,
1047
1103
  formatTime,
@@ -1099,9 +1155,6 @@ const useAudioRecorderHandlers = (options) => {
1099
1155
  toggleRecordingPause,
1100
1156
  ]);
1101
1157
  const handleSend = useCallback(async () => {
1102
- if (isSending) {
1103
- return;
1104
- }
1105
1158
  shouldSendAfterStopRef.current = false;
1106
1159
  setIsSending(true);
1107
1160
  try {
@@ -1118,7 +1171,6 @@ const useAudioRecorderHandlers = (options) => {
1118
1171
  setIsSending(false);
1119
1172
  }
1120
1173
  }, [
1121
- isSending,
1122
1174
  stopPlayback,
1123
1175
  onUploadError,
1124
1176
  sendRecording,
@@ -1129,6 +1181,7 @@ const useAudioRecorderHandlers = (options) => {
1129
1181
  return;
1130
1182
  }
1131
1183
  if (isRecording) {
1184
+ setIsSending(true);
1132
1185
  shouldSendAfterStopRef.current = true;
1133
1186
  stopRecording();
1134
1187
  return;
@@ -1266,6 +1319,44 @@ const IdleControl = (props) => {
1266
1319
  return (jsxs(IdleWrapper, { children: [jsxs(IdleButton, { type: "button", onClick: onClick, "aria-label": startAriaLabel, children: [jsx(IdleButtonOutline, { "aria-hidden": true }), jsx(IdleButtonGlow, { "aria-hidden": true }), jsx(Icon32Microphone, { height: 48, width: 48 })] }), jsx(IdleInstruction, { children: instruction })] }));
1267
1320
  };
1268
1321
 
1322
+ const Bars = styled.div `
1323
+ position: absolute;
1324
+ inset: 0;
1325
+ display: flex;
1326
+ align-items: flex-end;
1327
+ gap: 3px;
1328
+ `;
1329
+ const Bar = styled.div `
1330
+ flex: 1;
1331
+ min-width: 2px;
1332
+ border-radius: 9999px;
1333
+ background: var(--white, #ffffff);
1334
+ opacity: 0.6;
1335
+ transition: height 120ms ease, opacity 120ms ease;
1336
+ `;
1337
+
1338
+ const WaveformBars = (props) => {
1339
+ const { values, progress, styleInterpolator, scale = 40, minHeight = 6, divider = 100, } = props;
1340
+ return (jsx(Bars, { children: values.map((value, index) => {
1341
+ const normalized = clampValue(value);
1342
+ const height = Math.max(minHeight, (normalized / divider) * scale);
1343
+ const barProgress = values.length > 1 ? index / (values.length - 1) : 0;
1344
+ const isPlayed = (progress !== null && progress !== void 0 ? progress : 0) > 0 && barProgress <= (progress !== null && progress !== void 0 ? progress : 0);
1345
+ const interpolated = styleInterpolator === null || styleInterpolator === void 0 ? void 0 : styleInterpolator({
1346
+ normalized,
1347
+ height,
1348
+ index,
1349
+ progress: progress !== null && progress !== void 0 ? progress : 0,
1350
+ barProgress,
1351
+ isPlayed,
1352
+ });
1353
+ return (jsx(Bar, { style: {
1354
+ height: `${height}px`,
1355
+ ...interpolated,
1356
+ } }, index));
1357
+ }) }));
1358
+ };
1359
+
1269
1360
  /**
1270
1361
  * RecordingShell styles
1271
1362
  *
@@ -1360,21 +1451,6 @@ const Waveform = styled.div `
1360
1451
  align-items: center;
1361
1452
  overflow: hidden;
1362
1453
  `;
1363
- const Bars = styled.div `
1364
- position: absolute;
1365
- inset: 0;
1366
- display: flex;
1367
- align-items: flex-end;
1368
- gap: 3px;
1369
- `;
1370
- const Bar = styled.div `
1371
- flex: 1;
1372
- min-width: 2px;
1373
- border-radius: 9999px;
1374
- background: var(--white, #ffffff);
1375
- opacity: 0.6;
1376
- transition: height 120ms ease, opacity 120ms ease;
1377
- `;
1378
1454
  const Timer$1 = styled.span `
1379
1455
  text-align: right;
1380
1456
  font-size: 13px;
@@ -1436,31 +1512,33 @@ const Spinner = styled.span `
1436
1512
  }
1437
1513
  `;
1438
1514
 
1439
- const WaveformBars = (props) => {
1440
- const { values, styleInterpolator, scale = 40, minHeight = 6, divider = 100, } = props;
1441
- return (jsx(Bars, { children: values.map((value, index) => {
1442
- const normalized = clampValue(value);
1443
- const height = Math.max(minHeight, (normalized / divider) * scale);
1444
- const interpolated = styleInterpolator === null || styleInterpolator === void 0 ? void 0 : styleInterpolator({ normalized, height, index });
1445
- return (jsx(Bar, { style: {
1446
- height: `${height}px`,
1447
- ...interpolated,
1448
- } }, index));
1449
- }) }));
1450
- };
1451
-
1452
1515
  const RecordingShell = (props) => {
1453
1516
  const { isPaused, isPlaying, isSending, isRecording, audioLevels, hasRecording, playbackTime, previewLevels, recordingTime, stopAriaLabel, playAriaLabel, sendAriaLabel, startAriaLabel, cancelAriaLabel, playbackDuration, pausePlaybackAriaLabel, pauseRecordingAriaLabel, resumeRecordingAriaLabel, onSend, onCancel, formatTime, onRecordToggle, onTogglePlayback, } = props;
1517
+ const { lastDuration } = props;
1518
+ const isPreview = !isRecording || isPaused;
1454
1519
  const waveformValues = isRecording ? audioLevels : (hasRecording ? previewLevels : audioLevels);
1520
+ const effectiveDuration = recordingTime
1521
+ || playbackDuration
1522
+ || lastDuration
1523
+ || 1;
1524
+ const progress = (isPreview && isPlaying)
1525
+ ? Math.min(1, playbackTime / effectiveDuration)
1526
+ : 0;
1455
1527
  const displayTime = useMemo(() => {
1456
- if (isRecording) {
1528
+ if (isPlaying) {
1529
+ return formatTime(playbackTime);
1530
+ }
1531
+ if (isRecording && !isPaused) {
1532
+ return formatTime(recordingTime);
1533
+ }
1534
+ if (isRecording && isPaused) {
1457
1535
  return formatTime(recordingTime);
1458
1536
  }
1537
+ if (lastDuration > 0) {
1538
+ return formatTime(lastDuration);
1539
+ }
1459
1540
  if (hasRecording) {
1460
- if (isPlaying) {
1461
- return formatTime(playbackTime);
1462
- }
1463
- if (playbackDuration > 0) {
1541
+ if (playbackDuration > 0.5) {
1464
1542
  return formatTime(playbackDuration);
1465
1543
  }
1466
1544
  if (recordingTime > 0) {
@@ -1468,7 +1546,17 @@ const RecordingShell = (props) => {
1468
1546
  }
1469
1547
  }
1470
1548
  return formatTime(0);
1471
- }, [formatTime, hasRecording, isPlaying, playbackDuration, playbackTime, recordingTime]);
1549
+ }, [
1550
+ isPaused,
1551
+ isPlaying,
1552
+ formatTime,
1553
+ isRecording,
1554
+ lastDuration,
1555
+ hasRecording,
1556
+ playbackTime,
1557
+ recordingTime,
1558
+ playbackDuration,
1559
+ ]);
1472
1560
  const recordButtonDisabled = isSending;
1473
1561
  const recordButtonIcon = !isRecording || isPaused
1474
1562
  ? jsx(Icon16Microphone, { width: 24, height: 24 })
@@ -1486,10 +1574,13 @@ const RecordingShell = (props) => {
1486
1574
  : jsx(Icon16Play, { width: 24, height: 24 });
1487
1575
  const playbackAria = isPlaying ? pausePlaybackAriaLabel : playAriaLabel;
1488
1576
  const sendDisabled = isSending || (!hasRecording && !isRecording);
1489
- return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxs(Track, { children: [jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsx(Waveform, { "$expanded": canShowPlayback, children: jsx(WaveformBars, { values: waveformValues, divider: 100, scale: 42, minHeight: 6, styleInterpolator: ({ normalized }) => ({
1577
+ return (jsxs(Wrapper$1, { children: [jsx(CancelButton$1, { type: "button", onClick: onCancel, "aria-label": cancelAriaLabel, children: jsx(Icon28DeleteOutlineAndroid, { fill: "var(--exode_red)", width: 24, height: 24, style: { minWidth: 24 } }) }), jsxs(Track, { children: [jsx(CircleButton, { type: "button", "$variant": recordButtonVariant, onClick: onRecordToggle, "aria-label": recordButtonAria, disabled: recordButtonDisabled, children: recordButtonIcon }), !canShowPlayback ? null : (jsx(CircleButton, { type: "button", "$variant": "play", onClick: onTogglePlayback, "aria-label": playbackAria, disabled: playbackDisabled, children: playbackIcon })), jsx(Waveform, { "$expanded": canShowPlayback, children: jsx(WaveformBars, { scale: 42, minHeight: 6, divider: 100, progress: progress, values: waveformValues, styleInterpolator: ({ normalized, isPlayed, progress: p }) => ({
1490
1578
  height: `${12 + normalized / 2}px`,
1491
1579
  maxHeight: '34px',
1492
- opacity: 0.38 + Math.min(0.5, normalized / 120),
1580
+ opacity: p > 0
1581
+ ? (isPlayed ? 0.80 : 0.22)
1582
+ : 0.38 + Math.min(0.5, normalized / 120),
1583
+ transition: 'opacity 80ms ease',
1493
1584
  }) }) }), jsx(Timer$1, { children: displayTime })] }), jsx(SendButton$1, { type: "button", onClick: onSend, disabled: sendDisabled, "aria-label": sendAriaLabel, children: isSending ? (jsx(SpinnerSlot, { children: jsx(Spinner, {}) })) : (jsx(Icon28SendOutline, { width: 24, height: 24 })) })] }));
1494
1585
  };
1495
1586
 
@@ -1572,9 +1663,9 @@ const AudioRecorder = (props) => {
1572
1663
  messages,
1573
1664
  onUploadError,
1574
1665
  });
1575
- const { isPaused, isPlaying, formatTime, audioLevels, isRecording, playbackTime, hasRecording, previewLevels, recordingTime, playbackDuration, } = recorder;
1576
- const showRecorderBar = isRecording || hasRecording;
1577
- return (jsx(Container, { className: className, children: showRecorderBar ? (jsx(RecordingShell, { isPaused: isPaused, isSending: isSending, isPlaying: isPlaying, formatTime: formatTime, onCancel: handleCancel, isRecording: isRecording, audioLevels: audioLevels, onSend: handleSendRequest, playbackTime: playbackTime, hasRecording: hasRecording, previewLevels: previewLevels, recordingTime: recordingTime, playbackDuration: playbackDuration, onRecordToggle: handleRecordToggle, sendAriaLabel: messages.buttons.send, onTogglePlayback: handlePlaybackToggle, cancelAriaLabel: messages.buttons.cancel, playAriaLabel: messages.aria.playRecording, stopAriaLabel: messages.aria.stopRecording, startAriaLabel: messages.aria.startRecording, pauseRecordingAriaLabel: messages.aria.pauseRecording, resumeRecordingAriaLabel: messages.aria.resumeRecording, pausePlaybackAriaLabel: messages.aria.pausePlayback })) : (jsx(IdleControl, { onClick: handleRecordToggle, startAriaLabel: messages.aria.startRecording, instruction: messages.instruction })) }));
1666
+ const { isPaused, isPlaying, formatTime, audioLevels, isRecording, lastDuration, playbackTime, hasRecording, previewLevels, recordingTime, playbackDuration, } = recorder;
1667
+ const showRecorderBar = isRecording || hasRecording || isSending;
1668
+ return (jsx(Container, { className: className, children: showRecorderBar ? (jsx(RecordingShell, { isPaused: isPaused, isSending: isSending, isPlaying: isPlaying, formatTime: formatTime, onCancel: handleCancel, isRecording: isRecording, audioLevels: audioLevels, onSend: handleSendRequest, playbackTime: playbackTime, lastDuration: lastDuration, hasRecording: hasRecording, previewLevels: previewLevels, recordingTime: recordingTime, playbackDuration: playbackDuration, onRecordToggle: handleRecordToggle, sendAriaLabel: messages.buttons.send, onTogglePlayback: handlePlaybackToggle, cancelAriaLabel: messages.buttons.cancel, playAriaLabel: messages.aria.playRecording, stopAriaLabel: messages.aria.stopRecording, startAriaLabel: messages.aria.startRecording, pauseRecordingAriaLabel: messages.aria.pauseRecording, resumeRecordingAriaLabel: messages.aria.resumeRecording, pausePlaybackAriaLabel: messages.aria.pausePlayback })) : (jsx(IdleControl, { onClick: handleRecordToggle, startAriaLabel: messages.aria.startRecording, instruction: messages.instruction })) }));
1578
1669
  };
1579
1670
 
1580
1671
  /**
@@ -2152,14 +2243,18 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2152
2243
  VoicePlaybackChain.playNext(chainId);
2153
2244
  }
2154
2245
  }, [onEnded, chainId]);
2155
- const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2246
+ const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, seekToTime, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2156
2247
  src,
2157
2248
  duration,
2158
2249
  waveformData,
2159
2250
  preloadedBlob: props.preloadedBlob,
2160
2251
  onEnded: handleEnded,
2161
2252
  });
2162
- useImperativeHandle(ref, () => ({ play, prefetch }), [play, prefetch]);
2253
+ useImperativeHandle(ref, () => ({ play, prefetch, seekToTime }), [
2254
+ play,
2255
+ prefetch,
2256
+ seekToTime,
2257
+ ]);
2163
2258
  /** Auto-preload audio when player enters viewport */
2164
2259
  useEffect(() => {
2165
2260
  if (!autoPreload || isLoaded || isLoading) {
@@ -2213,7 +2308,7 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2213
2308
  observer.disconnect();
2214
2309
  };
2215
2310
  }, []);
2216
- const displayTime = isPlaying
2311
+ const displayTime = (isPlaying || currentTime > 0)
2217
2312
  ? formatTime(currentTime)
2218
2313
  : formatTime(resolvedDuration);
2219
2314
  const handleTrackClick = useCallback((e) => {