@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/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts +1 -0
- package/dist/components/VoiceMessagePlayer/VoiceMessagePlayer.d.ts.map +1 -1
- package/dist/hooks/useAudioBlobLoader.d.ts.map +1 -1
- package/dist/hooks/useAudioPlayback.d.ts.map +1 -1
- package/dist/hooks/useVoiceMessagePlayer.d.ts.map +1 -1
- package/dist/index.cjs +154 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +154 -5
- package/dist/index.js.map +1 -1
- package/dist/platform/audio-playback-adapter.d.ts +7 -0
- package/dist/platform/audio-playback-adapter.d.ts.map +1 -1
- package/dist/types/recorder.d.ts +4 -1
- package/dist/types/recorder.d.ts.map +1 -1
- package/dist/utils/voice-playback-chain.d.ts +2 -0
- package/dist/utils/voice-playback-chain.d.ts.map +1 -1
- package/package.json +1 -1
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((
|
|
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 =
|
|
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
|
|
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 }), [
|
|
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) {
|