@exode-team/react-recorder 1.1.8 → 1.1.10

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/dist/index.js CHANGED
@@ -76,6 +76,61 @@ const stopStream = (stream) => {
76
76
  *
77
77
  * @author: exode <hello@exode.ru>
78
78
  */
79
+ const SEEK_OFFSET = 5;
80
+ const setupMediaSession = (audio, callbacks) => {
81
+ if (!('mediaSession' in navigator)) {
82
+ return;
83
+ }
84
+ navigator.mediaSession.setActionHandler('play', () => {
85
+ audio.play().catch(() => { });
86
+ });
87
+ navigator.mediaSession.setActionHandler('pause', () => {
88
+ audio.pause();
89
+ });
90
+ navigator.mediaSession.setActionHandler('stop', () => {
91
+ var _a;
92
+ audio.pause();
93
+ audio.currentTime = 0;
94
+ (_a = callbacks.onStop) === null || _a === void 0 ? void 0 : _a.call(callbacks);
95
+ });
96
+ navigator.mediaSession.setActionHandler('seekbackward', () => {
97
+ var _a;
98
+ audio.currentTime = Math.max(0, audio.currentTime - SEEK_OFFSET);
99
+ (_a = callbacks.onSeekBackward) === null || _a === void 0 ? void 0 : _a.call(callbacks);
100
+ });
101
+ navigator.mediaSession.setActionHandler('seekforward', () => {
102
+ var _a;
103
+ audio.currentTime = Math.min(audio.duration, audio.currentTime + SEEK_OFFSET);
104
+ (_a = callbacks.onSeekForward) === null || _a === void 0 ? void 0 : _a.call(callbacks);
105
+ });
106
+ navigator.mediaSession.setActionHandler('seekto', (details) => {
107
+ if (details.seekTime != null && isFinite(audio.duration)) {
108
+ audio.currentTime = details.seekTime;
109
+ }
110
+ });
111
+ navigator.mediaSession.setActionHandler('previoustrack', () => {
112
+ var _a;
113
+ (_a = callbacks.onPreviousTrack) === null || _a === void 0 ? void 0 : _a.call(callbacks);
114
+ });
115
+ navigator.mediaSession.setActionHandler('nexttrack', () => {
116
+ var _a;
117
+ (_a = callbacks.onNextTrack) === null || _a === void 0 ? void 0 : _a.call(callbacks);
118
+ });
119
+ };
120
+ const clearMediaSession = () => {
121
+ if (!('mediaSession' in navigator)) {
122
+ return;
123
+ }
124
+ const actions = [
125
+ 'play', 'pause', 'stop',
126
+ 'seekbackward', 'seekforward', 'seekto',
127
+ 'previoustrack', 'nexttrack',
128
+ ];
129
+ actions.forEach((action) => {
130
+ navigator.mediaSession.setActionHandler(action, null);
131
+ });
132
+ navigator.mediaSession.playbackState = 'none';
133
+ };
79
134
  const createAudioPlayback = (url, callbacks) => {
80
135
  const audio = new Audio(url);
81
136
  audio.onloadedmetadata = () => {
@@ -87,6 +142,21 @@ const createAudioPlayback = (url, callbacks) => {
87
142
  audio.onended = () => {
88
143
  callbacks.onEnded();
89
144
  };
145
+ audio.onplay = () => {
146
+ var _a;
147
+ setupMediaSession(audio, callbacks);
148
+ if ('mediaSession' in navigator) {
149
+ navigator.mediaSession.playbackState = 'playing';
150
+ }
151
+ (_a = callbacks.onPlay) === null || _a === void 0 ? void 0 : _a.call(callbacks);
152
+ };
153
+ audio.onpause = () => {
154
+ var _a;
155
+ if ('mediaSession' in navigator) {
156
+ navigator.mediaSession.playbackState = 'paused';
157
+ }
158
+ (_a = callbacks.onPause) === null || _a === void 0 ? void 0 : _a.call(callbacks);
159
+ };
90
160
  return { audio, url };
91
161
  };
92
162
  const cleanupAudio = (audio) => {
@@ -95,6 +165,9 @@ const cleanupAudio = (audio) => {
95
165
  audio.onended = null;
96
166
  audio.ontimeupdate = null;
97
167
  audio.onloadedmetadata = null;
168
+ audio.onplay = null;
169
+ audio.onpause = null;
170
+ clearMediaSession();
98
171
  };
99
172
  const createObjectURL = (blob) => {
100
173
  return URL.createObjectURL(blob);
@@ -364,6 +437,8 @@ const useAudioPlayback = () => {
364
437
  setIsPlaying(false);
365
438
  setPlaybackTime(0);
366
439
  },
440
+ onPlay: () => setIsPlaying(true),
441
+ onPause: () => setIsPlaying(false),
367
442
  });
368
443
  audioRef.current = audio;
369
444
  setPlaybackTime(0);
@@ -383,6 +458,8 @@ const useAudioPlayback = () => {
383
458
  setIsPlaying(false);
384
459
  setPlaybackTime(0);
385
460
  },
461
+ onPlay: () => setIsPlaying(true),
462
+ onPause: () => setIsPlaying(false),
386
463
  });
387
464
  audioRef.current = audio;
388
465
  }
@@ -823,6 +900,13 @@ const useAudioBlobLoader = (options) => {
823
900
  onLoadedMetadata: (dur) => callbacksRef.current.onLoadedMetadata(dur),
824
901
  onTimeUpdate: (time) => callbacksRef.current.onTimeUpdate(time),
825
902
  onEnded: () => callbacksRef.current.onEnded(),
903
+ onPlay: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPlay) === null || _b === void 0 ? void 0 : _b.call(_a); },
904
+ onPause: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPause) === null || _b === void 0 ? void 0 : _b.call(_a); },
905
+ onStop: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onStop) === null || _b === void 0 ? void 0 : _b.call(_a); },
906
+ onSeekBackward: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onSeekBackward) === null || _b === void 0 ? void 0 : _b.call(_a); },
907
+ onSeekForward: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onSeekForward) === null || _b === void 0 ? void 0 : _b.call(_a); },
908
+ onPreviousTrack: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPreviousTrack) === null || _b === void 0 ? void 0 : _b.call(_a); },
909
+ onNextTrack: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onNextTrack) === null || _b === void 0 ? void 0 : _b.call(_a); },
826
910
  });
827
911
  audioRef.current = audio;
828
912
  setIsLoaded(true);
@@ -953,9 +1037,13 @@ const audioPlaybackManager = {
953
1037
  * @author: exode <hello@exode.ru>
954
1038
  */
955
1039
  const useVoiceMessagePlayer = (options) => {
956
- const { src, onEnded, waveformData, preloadedBlob, duration: externalDuration, } = options;
1040
+ const { src, onEnded, onPreviousTrack, onNextTrack, waveformData, preloadedBlob, duration: externalDuration, } = options;
957
1041
  const onEndedRef = useRef(onEnded);
1042
+ const onPreviousTrackRef = useRef(onPreviousTrack);
1043
+ const onNextTrackRef = useRef(onNextTrack);
958
1044
  onEndedRef.current = onEnded;
1045
+ onPreviousTrackRef.current = onPreviousTrack;
1046
+ onNextTrackRef.current = onNextTrack;
959
1047
  const [isPlaying, setIsPlaying] = useState(false);
960
1048
  const [currentTime, setCurrentTime] = useState(0);
961
1049
  const [duration, setDuration] = useState(externalDuration || 0);
@@ -989,6 +1077,14 @@ const useVoiceMessagePlayer = (options) => {
989
1077
  setCurrentTime(0);
990
1078
  (_a = onEndedRef.current) === null || _a === void 0 ? void 0 : _a.call(onEndedRef);
991
1079
  },
1080
+ onPlay: () => setIsPlaying(true),
1081
+ onPause: () => setIsPlaying(false),
1082
+ onStop: () => {
1083
+ setIsPlaying(false);
1084
+ setCurrentTime(0);
1085
+ },
1086
+ onPreviousTrack: () => { var _a; return (_a = onPreviousTrackRef.current) === null || _a === void 0 ? void 0 : _a.call(onPreviousTrackRef); },
1087
+ onNextTrack: () => { var _a; return (_a = onNextTrackRef.current) === null || _a === void 0 ? void 0 : _a.call(onNextTrackRef); },
992
1088
  },
993
1089
  });
994
1090
  /** Pre-fetch audio without playing (for viewport preloading) */
@@ -1043,15 +1139,44 @@ const useVoiceMessagePlayer = (options) => {
1043
1139
  setIsPlaying(false);
1044
1140
  }
1045
1141
  }, [isLoading, stopThis, audioRef]);
1046
- const seekTo = useCallback((seekProgress) => {
1142
+ const seekTo = useCallback((seekPercent) => {
1047
1143
  const audio = audioRef.current;
1048
1144
  if (!audio || !isFinite(audio.duration)) {
1049
1145
  return;
1050
1146
  }
1051
- const time = seekProgress * audio.duration;
1147
+ const time = seekPercent * audio.duration;
1052
1148
  audio.currentTime = time;
1053
1149
  setCurrentTime(time);
1054
1150
  }, [audioRef]);
1151
+ /** Seek to a specific time in seconds and start playback */
1152
+ const seekToTime = useCallback((seconds) => {
1153
+ const audio = audioRef.current;
1154
+ if (audio && isFinite(audio.duration)) {
1155
+ audioPlaybackManager.stopOthers(stopThis);
1156
+ audio.currentTime = Math.max(0, Math.min(seconds, audio.duration));
1157
+ setCurrentTime(audio.currentTime);
1158
+ audio.play()
1159
+ .then(() => setIsPlaying(true))
1160
+ .catch(() => setIsPlaying(false));
1161
+ }
1162
+ else {
1163
+ void ensureAudioLoaded().then(() => {
1164
+ const loadedAudio = audioRef.current;
1165
+ if (loadedAudio) {
1166
+ audioPlaybackManager.stopOthers(stopThis);
1167
+ loadedAudio.currentTime = Math.max(0, Math.min(seconds, loadedAudio.duration || seconds));
1168
+ setCurrentTime(loadedAudio.currentTime);
1169
+ loadedAudio.play()
1170
+ .then(() => setIsPlaying(true))
1171
+ .catch(() => setIsPlaying(false));
1172
+ }
1173
+ });
1174
+ }
1175
+ }, [
1176
+ audioRef,
1177
+ stopThis,
1178
+ ensureAudioLoaded,
1179
+ ]);
1055
1180
  /** Sync external duration */
1056
1181
  useEffect(() => {
1057
1182
  if (externalDuration) {
@@ -1068,6 +1193,7 @@ const useVoiceMessagePlayer = (options) => {
1068
1193
  currentTime,
1069
1194
  waveformLevels,
1070
1195
  seekTo,
1196
+ seekToTime,
1071
1197
  prefetch,
1072
1198
  preload,
1073
1199
  formatTime,
@@ -2106,6 +2232,13 @@ class VoicePlaybackChainClass {
2106
2232
  this.entries[idx + 1].play();
2107
2233
  }
2108
2234
  };
2235
+ /** Play the previous voice message before the given media ID */
2236
+ this.playPrevious = (currentId) => {
2237
+ const idx = this.entries.findIndex((e) => e.id === currentId);
2238
+ if (idx > 0) {
2239
+ this.entries[idx - 1].play();
2240
+ }
2241
+ };
2109
2242
  }
2110
2243
  }
2111
2244
  const VoicePlaybackChain = new VoicePlaybackChainClass();
@@ -2213,14 +2346,30 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2213
2346
  VoicePlaybackChain.playNext(chainId);
2214
2347
  }
2215
2348
  }, [onEnded, chainId]);
2216
- const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2349
+ const handleNextTrack = useCallback(() => {
2350
+ if (chainId != null) {
2351
+ VoicePlaybackChain.playNext(chainId);
2352
+ }
2353
+ }, [chainId]);
2354
+ const handlePreviousTrack = useCallback(() => {
2355
+ if (chainId != null) {
2356
+ VoicePlaybackChain.playPrevious(chainId);
2357
+ }
2358
+ }, [chainId]);
2359
+ const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, seekToTime, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2217
2360
  src,
2218
2361
  duration,
2219
2362
  waveformData,
2220
2363
  preloadedBlob: props.preloadedBlob,
2221
2364
  onEnded: handleEnded,
2365
+ onNextTrack: handleNextTrack,
2366
+ onPreviousTrack: handlePreviousTrack,
2222
2367
  });
2223
- useImperativeHandle(ref, () => ({ play, prefetch }), [play, prefetch]);
2368
+ useImperativeHandle(ref, () => ({ play, prefetch, seekToTime }), [
2369
+ play,
2370
+ prefetch,
2371
+ seekToTime,
2372
+ ]);
2224
2373
  /** Auto-preload audio when player enters viewport */
2225
2374
  useEffect(() => {
2226
2375
  if (!autoPreload || isLoaded || isLoading) {