@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.
@@ -6,6 +6,7 @@
6
6
  export interface VoiceMessagePlayerRef {
7
7
  play: () => void;
8
8
  prefetch: () => void;
9
+ seekToTime: (seconds: number) => void;
9
10
  }
10
11
  declare const VoiceMessagePlayer: any;
11
12
  export { VoiceMessagePlayer };
@@ -1 +1 @@
1
- {"version":3,"file":"VoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB;AAeD,QAAA,MAAM,kBAAkB,KA8MtB,CAAC;AAGH,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"VoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../../src/components/VoiceMessagePlayer/VoiceMessagePlayer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAeD,QAAA,MAAM,kBAAkB,KAqOtB,CAAC;AAGH,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -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,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,wBAoIhE,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
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,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,wBA2IhE,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useAudioPlayback.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioPlayback.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,sBAAsB;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IAClC,cAAc,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;IAC7D,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACnC;AAED,QAAA,MAAM,gBAAgB,QAAO,sBA2H5B,CAAC;AAGF,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"useAudioPlayback.d.ts","sourceRoot":"","sources":["../../src/hooks/useAudioPlayback.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,UAAU,sBAAsB;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IAClC,cAAc,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;IAC7D,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACnC;AAED,QAAA,MAAM,gBAAgB,QAAO,sBA+H5B,CAAC;AAGF,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useVoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceMessagePlayer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAG1F,QAAA,MAAM,qBAAqB,GAAI,SAAS,4BAA4B,KAAG,2BA6JtE,CAAC;AAGF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
1
+ {"version":3,"file":"useVoiceMessagePlayer.d.ts","sourceRoot":"","sources":["../../src/hooks/useVoiceMessagePlayer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAG1F,QAAA,MAAM,qBAAqB,GAAI,SAAS,4BAA4B,KAAG,2BAmNtE,CAAC;AAGF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
package/dist/index.cjs CHANGED
@@ -78,6 +78,61 @@ const stopStream = (stream) => {
78
78
  *
79
79
  * @author: exode <hello@exode.ru>
80
80
  */
81
+ const SEEK_OFFSET = 5;
82
+ const setupMediaSession = (audio, callbacks) => {
83
+ if (!('mediaSession' in navigator)) {
84
+ return;
85
+ }
86
+ navigator.mediaSession.setActionHandler('play', () => {
87
+ audio.play().catch(() => { });
88
+ });
89
+ navigator.mediaSession.setActionHandler('pause', () => {
90
+ audio.pause();
91
+ });
92
+ navigator.mediaSession.setActionHandler('stop', () => {
93
+ var _a;
94
+ audio.pause();
95
+ audio.currentTime = 0;
96
+ (_a = callbacks.onStop) === null || _a === void 0 ? void 0 : _a.call(callbacks);
97
+ });
98
+ navigator.mediaSession.setActionHandler('seekbackward', () => {
99
+ var _a;
100
+ audio.currentTime = Math.max(0, audio.currentTime - SEEK_OFFSET);
101
+ (_a = callbacks.onSeekBackward) === null || _a === void 0 ? void 0 : _a.call(callbacks);
102
+ });
103
+ navigator.mediaSession.setActionHandler('seekforward', () => {
104
+ var _a;
105
+ audio.currentTime = Math.min(audio.duration, audio.currentTime + SEEK_OFFSET);
106
+ (_a = callbacks.onSeekForward) === null || _a === void 0 ? void 0 : _a.call(callbacks);
107
+ });
108
+ navigator.mediaSession.setActionHandler('seekto', (details) => {
109
+ if (details.seekTime != null && isFinite(audio.duration)) {
110
+ audio.currentTime = details.seekTime;
111
+ }
112
+ });
113
+ navigator.mediaSession.setActionHandler('previoustrack', () => {
114
+ var _a;
115
+ (_a = callbacks.onPreviousTrack) === null || _a === void 0 ? void 0 : _a.call(callbacks);
116
+ });
117
+ navigator.mediaSession.setActionHandler('nexttrack', () => {
118
+ var _a;
119
+ (_a = callbacks.onNextTrack) === null || _a === void 0 ? void 0 : _a.call(callbacks);
120
+ });
121
+ };
122
+ const clearMediaSession = () => {
123
+ if (!('mediaSession' in navigator)) {
124
+ return;
125
+ }
126
+ const actions = [
127
+ 'play', 'pause', 'stop',
128
+ 'seekbackward', 'seekforward', 'seekto',
129
+ 'previoustrack', 'nexttrack',
130
+ ];
131
+ actions.forEach((action) => {
132
+ navigator.mediaSession.setActionHandler(action, null);
133
+ });
134
+ navigator.mediaSession.playbackState = 'none';
135
+ };
81
136
  const createAudioPlayback = (url, callbacks) => {
82
137
  const audio = new Audio(url);
83
138
  audio.onloadedmetadata = () => {
@@ -89,6 +144,21 @@ const createAudioPlayback = (url, callbacks) => {
89
144
  audio.onended = () => {
90
145
  callbacks.onEnded();
91
146
  };
147
+ audio.onplay = () => {
148
+ var _a;
149
+ setupMediaSession(audio, callbacks);
150
+ if ('mediaSession' in navigator) {
151
+ navigator.mediaSession.playbackState = 'playing';
152
+ }
153
+ (_a = callbacks.onPlay) === null || _a === void 0 ? void 0 : _a.call(callbacks);
154
+ };
155
+ audio.onpause = () => {
156
+ var _a;
157
+ if ('mediaSession' in navigator) {
158
+ navigator.mediaSession.playbackState = 'paused';
159
+ }
160
+ (_a = callbacks.onPause) === null || _a === void 0 ? void 0 : _a.call(callbacks);
161
+ };
92
162
  return { audio, url };
93
163
  };
94
164
  const cleanupAudio = (audio) => {
@@ -97,6 +167,9 @@ const cleanupAudio = (audio) => {
97
167
  audio.onended = null;
98
168
  audio.ontimeupdate = null;
99
169
  audio.onloadedmetadata = null;
170
+ audio.onplay = null;
171
+ audio.onpause = null;
172
+ clearMediaSession();
100
173
  };
101
174
  const createObjectURL = (blob) => {
102
175
  return URL.createObjectURL(blob);
@@ -366,6 +439,8 @@ const useAudioPlayback = () => {
366
439
  setIsPlaying(false);
367
440
  setPlaybackTime(0);
368
441
  },
442
+ onPlay: () => setIsPlaying(true),
443
+ onPause: () => setIsPlaying(false),
369
444
  });
370
445
  audioRef.current = audio;
371
446
  setPlaybackTime(0);
@@ -385,6 +460,8 @@ const useAudioPlayback = () => {
385
460
  setIsPlaying(false);
386
461
  setPlaybackTime(0);
387
462
  },
463
+ onPlay: () => setIsPlaying(true),
464
+ onPause: () => setIsPlaying(false),
388
465
  });
389
466
  audioRef.current = audio;
390
467
  }
@@ -825,6 +902,13 @@ const useAudioBlobLoader = (options) => {
825
902
  onLoadedMetadata: (dur) => callbacksRef.current.onLoadedMetadata(dur),
826
903
  onTimeUpdate: (time) => callbacksRef.current.onTimeUpdate(time),
827
904
  onEnded: () => callbacksRef.current.onEnded(),
905
+ onPlay: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPlay) === null || _b === void 0 ? void 0 : _b.call(_a); },
906
+ onPause: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPause) === null || _b === void 0 ? void 0 : _b.call(_a); },
907
+ onStop: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onStop) === null || _b === void 0 ? void 0 : _b.call(_a); },
908
+ onSeekBackward: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onSeekBackward) === null || _b === void 0 ? void 0 : _b.call(_a); },
909
+ onSeekForward: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onSeekForward) === null || _b === void 0 ? void 0 : _b.call(_a); },
910
+ onPreviousTrack: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onPreviousTrack) === null || _b === void 0 ? void 0 : _b.call(_a); },
911
+ onNextTrack: () => { var _a, _b; return (_b = (_a = callbacksRef.current).onNextTrack) === null || _b === void 0 ? void 0 : _b.call(_a); },
828
912
  });
829
913
  audioRef.current = audio;
830
914
  setIsLoaded(true);
@@ -955,9 +1039,13 @@ const audioPlaybackManager = {
955
1039
  * @author: exode <hello@exode.ru>
956
1040
  */
957
1041
  const useVoiceMessagePlayer = (options) => {
958
- const { src, onEnded, waveformData, preloadedBlob, duration: externalDuration, } = options;
1042
+ const { src, onEnded, onPreviousTrack, onNextTrack, waveformData, preloadedBlob, duration: externalDuration, } = options;
959
1043
  const onEndedRef = React.useRef(onEnded);
1044
+ const onPreviousTrackRef = React.useRef(onPreviousTrack);
1045
+ const onNextTrackRef = React.useRef(onNextTrack);
960
1046
  onEndedRef.current = onEnded;
1047
+ onPreviousTrackRef.current = onPreviousTrack;
1048
+ onNextTrackRef.current = onNextTrack;
961
1049
  const [isPlaying, setIsPlaying] = React.useState(false);
962
1050
  const [currentTime, setCurrentTime] = React.useState(0);
963
1051
  const [duration, setDuration] = React.useState(externalDuration || 0);
@@ -991,6 +1079,14 @@ const useVoiceMessagePlayer = (options) => {
991
1079
  setCurrentTime(0);
992
1080
  (_a = onEndedRef.current) === null || _a === void 0 ? void 0 : _a.call(onEndedRef);
993
1081
  },
1082
+ onPlay: () => setIsPlaying(true),
1083
+ onPause: () => setIsPlaying(false),
1084
+ onStop: () => {
1085
+ setIsPlaying(false);
1086
+ setCurrentTime(0);
1087
+ },
1088
+ onPreviousTrack: () => { var _a; return (_a = onPreviousTrackRef.current) === null || _a === void 0 ? void 0 : _a.call(onPreviousTrackRef); },
1089
+ onNextTrack: () => { var _a; return (_a = onNextTrackRef.current) === null || _a === void 0 ? void 0 : _a.call(onNextTrackRef); },
994
1090
  },
995
1091
  });
996
1092
  /** Pre-fetch audio without playing (for viewport preloading) */
@@ -1045,15 +1141,44 @@ const useVoiceMessagePlayer = (options) => {
1045
1141
  setIsPlaying(false);
1046
1142
  }
1047
1143
  }, [isLoading, stopThis, audioRef]);
1048
- const seekTo = React.useCallback((seekProgress) => {
1144
+ const seekTo = React.useCallback((seekPercent) => {
1049
1145
  const audio = audioRef.current;
1050
1146
  if (!audio || !isFinite(audio.duration)) {
1051
1147
  return;
1052
1148
  }
1053
- const time = seekProgress * audio.duration;
1149
+ const time = seekPercent * audio.duration;
1054
1150
  audio.currentTime = time;
1055
1151
  setCurrentTime(time);
1056
1152
  }, [audioRef]);
1153
+ /** Seek to a specific time in seconds and start playback */
1154
+ const seekToTime = React.useCallback((seconds) => {
1155
+ const audio = audioRef.current;
1156
+ if (audio && isFinite(audio.duration)) {
1157
+ audioPlaybackManager.stopOthers(stopThis);
1158
+ audio.currentTime = Math.max(0, Math.min(seconds, audio.duration));
1159
+ setCurrentTime(audio.currentTime);
1160
+ audio.play()
1161
+ .then(() => setIsPlaying(true))
1162
+ .catch(() => setIsPlaying(false));
1163
+ }
1164
+ else {
1165
+ void ensureAudioLoaded().then(() => {
1166
+ const loadedAudio = audioRef.current;
1167
+ if (loadedAudio) {
1168
+ audioPlaybackManager.stopOthers(stopThis);
1169
+ loadedAudio.currentTime = Math.max(0, Math.min(seconds, loadedAudio.duration || seconds));
1170
+ setCurrentTime(loadedAudio.currentTime);
1171
+ loadedAudio.play()
1172
+ .then(() => setIsPlaying(true))
1173
+ .catch(() => setIsPlaying(false));
1174
+ }
1175
+ });
1176
+ }
1177
+ }, [
1178
+ audioRef,
1179
+ stopThis,
1180
+ ensureAudioLoaded,
1181
+ ]);
1057
1182
  /** Sync external duration */
1058
1183
  React.useEffect(() => {
1059
1184
  if (externalDuration) {
@@ -1070,6 +1195,7 @@ const useVoiceMessagePlayer = (options) => {
1070
1195
  currentTime,
1071
1196
  waveformLevels,
1072
1197
  seekTo,
1198
+ seekToTime,
1073
1199
  prefetch,
1074
1200
  preload,
1075
1201
  formatTime,
@@ -2108,6 +2234,13 @@ class VoicePlaybackChainClass {
2108
2234
  this.entries[idx + 1].play();
2109
2235
  }
2110
2236
  };
2237
+ /** Play the previous voice message before the given media ID */
2238
+ this.playPrevious = (currentId) => {
2239
+ const idx = this.entries.findIndex((e) => e.id === currentId);
2240
+ if (idx > 0) {
2241
+ this.entries[idx - 1].play();
2242
+ }
2243
+ };
2111
2244
  }
2112
2245
  }
2113
2246
  const VoicePlaybackChain = new VoicePlaybackChainClass();
@@ -2215,14 +2348,30 @@ const VoiceMessagePlayer = React.forwardRef((props, ref) => {
2215
2348
  VoicePlaybackChain.playNext(chainId);
2216
2349
  }
2217
2350
  }, [onEnded, chainId]);
2218
- const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2351
+ const handleNextTrack = React.useCallback(() => {
2352
+ if (chainId != null) {
2353
+ VoicePlaybackChain.playNext(chainId);
2354
+ }
2355
+ }, [chainId]);
2356
+ const handlePreviousTrack = React.useCallback(() => {
2357
+ if (chainId != null) {
2358
+ VoicePlaybackChain.playPrevious(chainId);
2359
+ }
2360
+ }, [chainId]);
2361
+ const { play, progress, isLoaded, isPlaying, isLoading, currentTime, waveformLevels, seekTo, seekToTime, prefetch, preload, formatTime, togglePlayback, duration: resolvedDuration, } = useVoiceMessagePlayer({
2219
2362
  src,
2220
2363
  duration,
2221
2364
  waveformData,
2222
2365
  preloadedBlob: props.preloadedBlob,
2223
2366
  onEnded: handleEnded,
2367
+ onNextTrack: handleNextTrack,
2368
+ onPreviousTrack: handlePreviousTrack,
2224
2369
  });
2225
- React.useImperativeHandle(ref, () => ({ play, prefetch }), [play, prefetch]);
2370
+ React.useImperativeHandle(ref, () => ({ play, prefetch, seekToTime }), [
2371
+ play,
2372
+ prefetch,
2373
+ seekToTime,
2374
+ ]);
2226
2375
  /** Auto-preload audio when player enters viewport */
2227
2376
  React.useEffect(() => {
2228
2377
  if (!autoPreload || isLoaded || isLoading) {