stream-chat-react 13.11.0 → 13.13.0

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 (51) hide show
  1. package/dist/components/Attachment/Audio.js +38 -15
  2. package/dist/components/Attachment/Card.js +33 -11
  3. package/dist/components/Attachment/Geolocation.js +1 -1
  4. package/dist/components/Attachment/VoiceRecording.js +45 -20
  5. package/dist/components/Attachment/components/PlayButton.js +1 -1
  6. package/dist/components/Attachment/components/PlaybackRateButton.js +1 -1
  7. package/dist/components/Attachment/hooks/useAudioController.d.ts +1 -0
  8. package/dist/components/Attachment/hooks/useAudioController.js +1 -0
  9. package/dist/components/Attachment/index.d.ts +1 -0
  10. package/dist/components/Attachment/index.js +1 -0
  11. package/dist/components/AudioPlayback/AudioPlayer.d.ts +116 -0
  12. package/dist/components/AudioPlayback/AudioPlayer.js +456 -0
  13. package/dist/components/AudioPlayback/AudioPlayerPool.d.ts +49 -0
  14. package/dist/components/AudioPlayback/AudioPlayerPool.js +156 -0
  15. package/dist/components/AudioPlayback/WithAudioPlayback.d.ts +24 -0
  16. package/dist/components/AudioPlayback/WithAudioPlayback.js +57 -0
  17. package/dist/components/AudioPlayback/index.d.ts +3 -0
  18. package/dist/components/AudioPlayback/index.js +3 -0
  19. package/dist/components/AudioPlayback/plugins/AudioPlayerNotificationsPlugin.d.ts +7 -0
  20. package/dist/components/AudioPlayback/plugins/AudioPlayerNotificationsPlugin.js +25 -0
  21. package/dist/components/AudioPlayback/plugins/AudioPlayerPlugin.d.ts +10 -0
  22. package/dist/components/AudioPlayback/plugins/AudioPlayerPlugin.js +1 -0
  23. package/dist/components/AudioPlayback/plugins/index.d.ts +1 -0
  24. package/dist/components/AudioPlayback/plugins/index.js +1 -0
  25. package/dist/components/Channel/Channel.d.ts +2 -0
  26. package/dist/components/Channel/Channel.js +4 -2
  27. package/dist/components/Chat/hooks/useChat.js +1 -1
  28. package/dist/components/MediaRecorder/AudioRecorder/AudioRecordingPreview.d.ts +3 -2
  29. package/dist/components/MediaRecorder/AudioRecorder/AudioRecordingPreview.js +23 -8
  30. package/dist/components/MessageInput/AttachmentPreviewList/FileAttachmentPreview.js +1 -1
  31. package/dist/components/MessageInput/AttachmentPreviewList/GeolocationPreview.js +1 -1
  32. package/dist/components/MessageInput/AttachmentPreviewList/ImageAttachmentPreview.js +1 -1
  33. package/dist/components/MessageInput/AttachmentPreviewList/UnsupportedAttachmentPreview.js +1 -1
  34. package/dist/components/MessageInput/AttachmentPreviewList/VoiceRecordingPreview.d.ts +1 -1
  35. package/dist/components/MessageInput/AttachmentPreviewList/VoiceRecordingPreview.js +20 -7
  36. package/dist/components/MessageList/MessageList.js +8 -5
  37. package/dist/components/MessageList/renderMessages.js +6 -6
  38. package/dist/components/index.d.ts +2 -1
  39. package/dist/components/index.js +2 -1
  40. package/dist/context/ComponentContext.d.ts +4 -0
  41. package/dist/context/MessageListContext.d.ts +3 -0
  42. package/dist/experimental/index.browser.cjs.map +2 -2
  43. package/dist/experimental/index.node.cjs.map +2 -2
  44. package/dist/i18n/TranslationBuilder/notifications/NotificationTranslationTopic.js +2 -0
  45. package/dist/i18n/TranslationBuilder/notifications/browserAudioPlaybackError.d.ts +3 -0
  46. package/dist/i18n/TranslationBuilder/notifications/browserAudioPlaybackError.js +1 -0
  47. package/dist/index.browser.cjs +2833 -2089
  48. package/dist/index.browser.cjs.map +4 -4
  49. package/dist/index.node.cjs +2842 -2089
  50. package/dist/index.node.cjs.map +4 -4
  51. package/package.json +1 -1
@@ -0,0 +1,456 @@
1
+ import { StateStore } from 'stream-chat';
2
+ import throttle from 'lodash.throttle';
3
+ const DEFAULT_PLAYBACK_RATES = [1.0, 1.5, 2.0];
4
+ const isSeekable = (audioElement) => !(audioElement.duration === Infinity || isNaN(audioElement.duration));
5
+ export const defaultRegisterAudioPlayerError = ({ error, } = {}) => {
6
+ if (!error)
7
+ return;
8
+ console.error('[AUDIO PLAYER]', error);
9
+ };
10
+ export const elementIsPlaying = (audioElement) => audioElement && !(audioElement.paused || audioElement.ended);
11
+ export class AudioPlayer {
12
+ constructor({ durationSeconds, fileSize, id, mimeType, playbackRates: customPlaybackRates, plugins, pool, src, title, waveformData, }) {
13
+ this._plugins = new Map();
14
+ this.playTimeout = undefined;
15
+ this.unsubscribeEventListeners = null;
16
+ this._disposed = false;
17
+ this._restoringPosition = false;
18
+ this._removalTimeout = undefined;
19
+ this.setPlaybackStartSafetyTimeout = () => {
20
+ clearTimeout(this.playTimeout);
21
+ this.playTimeout = setTimeout(() => {
22
+ if (!this.elementRef)
23
+ return;
24
+ try {
25
+ this.elementRef.pause();
26
+ this.state.partialNext({ isPlaying: false });
27
+ }
28
+ catch (e) {
29
+ this.registerError({ errCode: 'failed-to-start' });
30
+ }
31
+ }, 2000);
32
+ };
33
+ this.clearPlaybackStartSafetyTimeout = () => {
34
+ if (!this.elementRef)
35
+ return;
36
+ clearTimeout(this.playTimeout);
37
+ this.playTimeout = undefined;
38
+ };
39
+ this.clearPendingLoadedMeta = () => {
40
+ const pending = this._pendingLoadedMeta;
41
+ if (pending?.element && pending.onLoaded) {
42
+ pending.element.removeEventListener('loadedmetadata', pending.onLoaded);
43
+ }
44
+ this._pendingLoadedMeta = undefined;
45
+ };
46
+ this.restoreSavedPosition = (elementRef) => {
47
+ const saved = this.secondsElapsed;
48
+ if (!saved || saved <= 0)
49
+ return;
50
+ const apply = () => {
51
+ const duration = elementRef.duration;
52
+ const clamped = typeof duration === 'number' && !isNaN(duration) && isFinite(duration)
53
+ ? Math.min(saved, duration)
54
+ : saved;
55
+ try {
56
+ if (elementRef.currentTime === clamped)
57
+ return;
58
+ elementRef.currentTime = clamped;
59
+ // Preempt UI with restored position to avoid flicker
60
+ this.setSecondsElapsed(clamped);
61
+ }
62
+ catch {
63
+ // ignore
64
+ }
65
+ };
66
+ // No information is available about the media resource.
67
+ if (elementRef.readyState < 1) {
68
+ this.clearPendingLoadedMeta();
69
+ this._restoringPosition = true;
70
+ const onLoaded = () => {
71
+ // Ensure this callback still belongs to the same pending registration and same element
72
+ if (this._pendingLoadedMeta?.onLoaded !== onLoaded)
73
+ return;
74
+ this._pendingLoadedMeta = undefined;
75
+ if (this.elementRef !== elementRef) {
76
+ this._restoringPosition = false;
77
+ return;
78
+ }
79
+ apply();
80
+ this._restoringPosition = false;
81
+ };
82
+ elementRef.addEventListener('loadedmetadata', onLoaded, { once: true });
83
+ this._pendingLoadedMeta = { element: elementRef, onLoaded };
84
+ }
85
+ else {
86
+ this._restoringPosition = true;
87
+ apply();
88
+ this._restoringPosition = false;
89
+ }
90
+ };
91
+ this.elementIsReady = () => {
92
+ if (this._elementIsReadyPromise)
93
+ return this._elementIsReadyPromise;
94
+ this._elementIsReadyPromise = new Promise((resolve) => {
95
+ if (!this.elementRef)
96
+ return resolve(false);
97
+ const element = this.elementRef;
98
+ const handleLoaded = () => {
99
+ element.removeEventListener('loadedmetadata', handleLoaded);
100
+ resolve(element.readyState > 0);
101
+ };
102
+ element.addEventListener('loadedmetadata', handleLoaded);
103
+ });
104
+ return this._elementIsReadyPromise;
105
+ };
106
+ this.setRef = (elementRef) => {
107
+ if (elementIsPlaying(this.elementRef)) {
108
+ // preserve state during swap
109
+ this.releaseElement({ resetState: false });
110
+ }
111
+ this.clearPendingLoadedMeta();
112
+ this._restoringPosition = false;
113
+ this._elementIsReadyPromise = undefined;
114
+ this.state.partialNext({ elementRef });
115
+ // When a new element is attached, make sure listeners are wired to it
116
+ if (elementRef) {
117
+ this.registerSubscriptions();
118
+ }
119
+ };
120
+ this.setSecondsElapsed = (secondsElapsed) => {
121
+ this.state.partialNext({
122
+ progressPercent: this.elementRef && secondsElapsed
123
+ ? (secondsElapsed / this.elementRef.duration) * 100
124
+ : 0,
125
+ secondsElapsed,
126
+ });
127
+ };
128
+ this.canPlayMimeType = (mimeType) => {
129
+ if (!mimeType)
130
+ return false;
131
+ if (this.elementRef)
132
+ return !!this.elementRef.canPlayType(mimeType);
133
+ return !!new Audio().canPlayType(mimeType);
134
+ };
135
+ this.play = async (params) => {
136
+ if (this._disposed)
137
+ return;
138
+ const elementRef = this.ensureElementRef();
139
+ if (elementIsPlaying(this.elementRef)) {
140
+ if (this.isPlaying)
141
+ return;
142
+ this.state.partialNext({ isPlaying: true });
143
+ return;
144
+ }
145
+ const { currentPlaybackRate, playbackRates } = {
146
+ currentPlaybackRate: this.currentPlaybackRate,
147
+ playbackRates: this.playbackRates,
148
+ ...params,
149
+ };
150
+ if (!this.canPlayRecord) {
151
+ this.registerError({ errCode: 'not-playable' });
152
+ return;
153
+ }
154
+ // Restore last known position for this player before attempting to play
155
+ this.restoreSavedPosition(elementRef);
156
+ elementRef.playbackRate = currentPlaybackRate ?? this.currentPlaybackRate;
157
+ this.setPlaybackStartSafetyTimeout();
158
+ try {
159
+ await elementRef.play();
160
+ this.state.partialNext({
161
+ currentPlaybackRate,
162
+ isPlaying: true,
163
+ playbackRates,
164
+ });
165
+ this._pool.setActiveAudioPlayer(this);
166
+ }
167
+ catch (e) {
168
+ this.registerError({ error: e });
169
+ this.state.partialNext({ isPlaying: false });
170
+ }
171
+ finally {
172
+ this.clearPlaybackStartSafetyTimeout();
173
+ }
174
+ };
175
+ this.pause = () => {
176
+ if (!elementIsPlaying(this.elementRef))
177
+ return;
178
+ this.clearPlaybackStartSafetyTimeout();
179
+ // existence of the element already checked by elementIsPlaying
180
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
181
+ this.elementRef.pause();
182
+ this.state.partialNext({ isPlaying: false });
183
+ };
184
+ this.stop = () => {
185
+ this.pause();
186
+ this.setSecondsElapsed(0);
187
+ if (this.elementRef)
188
+ this.elementRef.currentTime = 0;
189
+ };
190
+ this.togglePlay = async () => (this.isPlaying ? this.pause() : await this.play());
191
+ this.increasePlaybackRate = () => {
192
+ if (!this.elementRef)
193
+ return;
194
+ let currentPlaybackRateIndex = this.state
195
+ .getLatestValue()
196
+ .playbackRates.findIndex((rate) => rate === this.currentPlaybackRate);
197
+ if (currentPlaybackRateIndex === -1) {
198
+ currentPlaybackRateIndex = 0;
199
+ }
200
+ const nextIndex = currentPlaybackRateIndex === this.playbackRates.length - 1
201
+ ? 0
202
+ : currentPlaybackRateIndex + 1;
203
+ const currentPlaybackRate = this.playbackRates[nextIndex];
204
+ this.state.partialNext({ currentPlaybackRate });
205
+ this.elementRef.playbackRate = currentPlaybackRate;
206
+ };
207
+ this.seek = throttle(async ({ clientX, currentTarget }) => {
208
+ let element = this.elementRef;
209
+ if (!this.elementRef) {
210
+ element = this.ensureElementRef();
211
+ const isReady = await this.elementIsReady();
212
+ if (!isReady)
213
+ return;
214
+ }
215
+ if (!currentTarget || !element)
216
+ return;
217
+ if (!isSeekable(element)) {
218
+ this.registerError({ errCode: 'seek-not-supported' });
219
+ return;
220
+ }
221
+ const { width, x } = currentTarget.getBoundingClientRect();
222
+ const ratio = (clientX - x) / width;
223
+ if (ratio > 1 || ratio < 0)
224
+ return;
225
+ const currentTime = ratio * element.duration;
226
+ this.setSecondsElapsed(currentTime);
227
+ element.currentTime = currentTime;
228
+ }, 16);
229
+ this.registerError = (params) => {
230
+ defaultRegisterAudioPlayerError(params);
231
+ this.plugins.forEach(({ onError }) => onError?.({ player: this, ...params }));
232
+ };
233
+ /**
234
+ * Removes the audio element reference, event listeners and audio player from the player pool.
235
+ * Helpful when only a single AudioPlayer instance is to be removed from the AudioPlayerPool.
236
+ */
237
+ this.requestRemoval = () => {
238
+ this._disposed = true;
239
+ this.cancelScheduledRemoval();
240
+ this.clearPendingLoadedMeta();
241
+ this._restoringPosition = false;
242
+ this.releaseElement({ resetState: true });
243
+ this.unsubscribeEventListeners?.();
244
+ this.unsubscribeEventListeners = null;
245
+ this.plugins.forEach(({ onRemove }) => onRemove?.({ player: this }));
246
+ this._pool.deregister(this.id);
247
+ };
248
+ this.cancelScheduledRemoval = () => {
249
+ clearTimeout(this._removalTimeout);
250
+ this._removalTimeout = undefined;
251
+ };
252
+ this.scheduleRemoval = (ms = 0) => {
253
+ this.cancelScheduledRemoval();
254
+ this._removalTimeout = setTimeout(() => {
255
+ if (this.disposed)
256
+ return;
257
+ this.requestRemoval();
258
+ }, ms);
259
+ };
260
+ /**
261
+ * Releases only the underlying element back to the pool without disposing the player instance.
262
+ * Used by the pool to hand off the shared element in single-playback mode.
263
+ */
264
+ this.releaseElementForHandoff = () => {
265
+ if (!this.elementRef)
266
+ return;
267
+ this.releaseElement({ resetState: false });
268
+ this.unsubscribeEventListeners?.();
269
+ this.unsubscribeEventListeners = null;
270
+ };
271
+ this.registerSubscriptions = () => {
272
+ this.unsubscribeEventListeners?.();
273
+ const audioElement = this.elementRef;
274
+ if (!audioElement)
275
+ return;
276
+ const handleEnded = () => {
277
+ this.state.partialNext({
278
+ isPlaying: false,
279
+ secondsElapsed: audioElement?.duration ?? this.durationSeconds ?? 0,
280
+ });
281
+ };
282
+ const handleError = (e) => {
283
+ // if fired probably is one of these (e.srcElement.error.code)
284
+ // 1 = MEDIA_ERR_ABORTED (fetch aborted by user/JS)
285
+ // 2 = MEDIA_ERR_NETWORK (network failed while fetching)
286
+ // 3 = MEDIA_ERR_DECODE (data fetched but couldn’t decode)
287
+ // 4 = MEDIA_ERR_SRC_NOT_SUPPORTED (no resource supported / bad type)
288
+ // reported during the mount so only logging to the console
289
+ const audio = e.currentTarget;
290
+ const state = { isPlaying: false };
291
+ if (!audio?.error?.code) {
292
+ this.state.partialNext(state);
293
+ return;
294
+ }
295
+ if (audio.error.code === 4) {
296
+ state.canPlayRecord = false;
297
+ this.state.partialNext(state);
298
+ }
299
+ const errorMsg = [
300
+ undefined,
301
+ 'MEDIA_ERR_ABORTED: fetch aborted by user',
302
+ 'MEDIA_ERR_NETWORK: network failed while fetching',
303
+ 'MEDIA_ERR_DECODE: audio fetched but couldn’t decode',
304
+ 'MEDIA_ERR_SRC_NOT_SUPPORTED: source not supported',
305
+ ][audio?.error?.code];
306
+ if (!errorMsg)
307
+ return;
308
+ defaultRegisterAudioPlayerError({ error: new Error(errorMsg + ` (${audio.src})`) });
309
+ };
310
+ const handleTimeupdate = () => {
311
+ const t = audioElement?.currentTime ?? 0;
312
+ // Ignore spurious zero during restore/handoff to avoid UI flicker
313
+ if (this._restoringPosition && t === 0)
314
+ return;
315
+ // Also avoid regressing UI to zero if we already have non-zero progress and we're not playing
316
+ if (!this.isPlaying && t === 0 && this.secondsElapsed > 0)
317
+ return;
318
+ this.setSecondsElapsed(t);
319
+ };
320
+ audioElement.addEventListener('ended', handleEnded);
321
+ audioElement.addEventListener('error', handleError);
322
+ audioElement.addEventListener('timeupdate', handleTimeupdate);
323
+ this.unsubscribeEventListeners = () => {
324
+ audioElement.pause();
325
+ audioElement.removeEventListener('ended', handleEnded);
326
+ audioElement.removeEventListener('error', handleError);
327
+ audioElement.removeEventListener('timeupdate', handleTimeupdate);
328
+ };
329
+ };
330
+ this._data = {
331
+ durationSeconds,
332
+ fileSize,
333
+ id,
334
+ mimeType,
335
+ src,
336
+ title,
337
+ waveformData,
338
+ };
339
+ this._pool = pool;
340
+ this.setPlugins(() => plugins ?? []);
341
+ const playbackRates = customPlaybackRates?.length
342
+ ? customPlaybackRates
343
+ : DEFAULT_PLAYBACK_RATES;
344
+ // do not create element here; only evaluate canPlayRecord cheaply
345
+ const canPlayRecord = mimeType ? !!new Audio().canPlayType(mimeType) : true;
346
+ this.state = new StateStore({
347
+ canPlayRecord,
348
+ currentPlaybackRate: playbackRates[0],
349
+ elementRef: null,
350
+ isPlaying: false,
351
+ playbackError: null,
352
+ playbackRates,
353
+ progressPercent: 0,
354
+ secondsElapsed: 0,
355
+ });
356
+ this.plugins.forEach((p) => p.onInit?.({ player: this }));
357
+ }
358
+ get plugins() {
359
+ return Array.from(this._plugins.values());
360
+ }
361
+ get canPlayRecord() {
362
+ return this.state.getLatestValue().canPlayRecord;
363
+ }
364
+ get elementRef() {
365
+ return this.state.getLatestValue().elementRef;
366
+ }
367
+ get isPlaying() {
368
+ return this.state.getLatestValue().isPlaying;
369
+ }
370
+ get currentPlaybackRate() {
371
+ return this.state.getLatestValue().currentPlaybackRate;
372
+ }
373
+ get playbackRates() {
374
+ return this.state.getLatestValue().playbackRates;
375
+ }
376
+ get durationSeconds() {
377
+ return this._data.durationSeconds;
378
+ }
379
+ get fileSize() {
380
+ return this._data.fileSize;
381
+ }
382
+ get id() {
383
+ return this._data.id;
384
+ }
385
+ get src() {
386
+ return this._data.src;
387
+ }
388
+ get mimeType() {
389
+ return this._data.mimeType;
390
+ }
391
+ get title() {
392
+ return this._data.title;
393
+ }
394
+ get waveformData() {
395
+ return this._data.waveformData;
396
+ }
397
+ get secondsElapsed() {
398
+ return this.state.getLatestValue().secondsElapsed;
399
+ }
400
+ get progressPercent() {
401
+ return this.state.getLatestValue().progressPercent;
402
+ }
403
+ get disposed() {
404
+ return this._disposed;
405
+ }
406
+ ensureElementRef() {
407
+ if (this._disposed) {
408
+ throw new Error('AudioPlayer is disposed');
409
+ }
410
+ if (!this.elementRef) {
411
+ const el = this._pool.acquireElement({
412
+ ownerId: this.id,
413
+ src: this.src,
414
+ });
415
+ this.setRef(el);
416
+ }
417
+ return this.elementRef;
418
+ }
419
+ setDescriptor(descriptor) {
420
+ this._data = { ...this._data, ...descriptor };
421
+ if (descriptor.src !== this.src && this.elementRef) {
422
+ this.elementRef.src = descriptor.src;
423
+ }
424
+ }
425
+ releaseElement({ resetState }) {
426
+ this.clearPendingLoadedMeta();
427
+ this._restoringPosition = false;
428
+ if (resetState) {
429
+ this.stop();
430
+ }
431
+ else {
432
+ // Ensure isPlaying reflects reality, but keep progress/seconds
433
+ this.state.partialNext({ isPlaying: false });
434
+ if (this.elementRef) {
435
+ try {
436
+ this.elementRef.pause();
437
+ }
438
+ catch {
439
+ // ignore
440
+ }
441
+ }
442
+ }
443
+ if (this.elementRef) {
444
+ this._pool.releaseElement(this.id);
445
+ this.setRef(null);
446
+ }
447
+ }
448
+ setPlugins(setter) {
449
+ this._plugins = setter(this.plugins).reduce((acc, plugin) => {
450
+ if (plugin.id) {
451
+ acc.set(plugin.id, plugin);
452
+ }
453
+ return acc;
454
+ }, new Map());
455
+ }
456
+ }
@@ -0,0 +1,49 @@
1
+ import { AudioPlayer, type AudioPlayerOptions } from './AudioPlayer';
2
+ import { StateStore } from 'stream-chat';
3
+ export type AudioPlayerPoolState = {
4
+ activeAudioPlayer: AudioPlayer | null;
5
+ };
6
+ export declare class AudioPlayerPool {
7
+ state: StateStore<AudioPlayerPoolState>;
8
+ private pool;
9
+ private audios;
10
+ private sharedAudio;
11
+ private sharedOwnerId;
12
+ private readonly allowConcurrentPlayback;
13
+ constructor(config?: {
14
+ allowConcurrentPlayback?: boolean;
15
+ });
16
+ get players(): AudioPlayer[];
17
+ get activeAudioPlayer(): AudioPlayer | null;
18
+ getOrAdd: (params: Omit<AudioPlayerOptions, 'pool'>) => AudioPlayer;
19
+ /**
20
+ * In case of allowConcurrentPlayback enabled, a new Audio is created and assigned to the given audioPlayer owner.
21
+ * In case of disabled concurrency, the shared audio ownership is transferred to the new owner loading the owner's
22
+ * source.
23
+ *
24
+ * @param ownerId
25
+ * @param src
26
+ */
27
+ acquireElement: ({ ownerId, src }: {
28
+ ownerId: string;
29
+ src: string;
30
+ }) => HTMLAudioElement;
31
+ /**
32
+ * Removes the given audio players ownership of the shared audio element (in case of concurrent playback is disabled)
33
+ * and pauses the reproduction of the audio.
34
+ * In case of concurrent playback mode (allowConcurrentPlayback enabled), the audio is paused,
35
+ * its source cleared and removed from the audios pool readied for garbage collection.
36
+ *
37
+ * @param ownerId
38
+ */
39
+ releaseElement: (ownerId: string) => void;
40
+ /** Sets active audio player when allowConcurrentPlayback is disabled */
41
+ setActiveAudioPlayer: (activeAudioPlayer: AudioPlayer | null) => void;
42
+ /** Removes the AudioPlayer instance from the pool of players */
43
+ deregister(id: string): void;
44
+ /** Performs all the necessary cleanup actions and removes the player from the pool */
45
+ remove: (id: string) => void;
46
+ /** Removes and cleans up all the players from the pool */
47
+ clear: () => void;
48
+ registerSubscriptions: () => void;
49
+ }
@@ -0,0 +1,156 @@
1
+ import { AudioPlayer } from './AudioPlayer';
2
+ import { StateStore } from 'stream-chat';
3
+ export class AudioPlayerPool {
4
+ constructor(config) {
5
+ this.state = new StateStore({
6
+ activeAudioPlayer: null,
7
+ });
8
+ this.pool = new Map();
9
+ this.audios = new Map();
10
+ this.sharedAudio = null;
11
+ this.sharedOwnerId = null;
12
+ this.getOrAdd = (params) => {
13
+ const { playbackRates, plugins, ...descriptor } = params;
14
+ let player = this.pool.get(params.id);
15
+ if (player) {
16
+ if (!player.disposed) {
17
+ player.setDescriptor(descriptor);
18
+ return player;
19
+ }
20
+ this.deregister(params.id);
21
+ }
22
+ player = new AudioPlayer({
23
+ playbackRates,
24
+ plugins,
25
+ ...descriptor,
26
+ pool: this,
27
+ });
28
+ this.pool.set(params.id, player);
29
+ return player;
30
+ };
31
+ /**
32
+ * In case of allowConcurrentPlayback enabled, a new Audio is created and assigned to the given audioPlayer owner.
33
+ * In case of disabled concurrency, the shared audio ownership is transferred to the new owner loading the owner's
34
+ * source.
35
+ *
36
+ * @param ownerId
37
+ * @param src
38
+ */
39
+ this.acquireElement = ({ ownerId, src }) => {
40
+ if (!this.allowConcurrentPlayback) {
41
+ // Single shared element mode
42
+ if (!this.sharedAudio) {
43
+ this.sharedAudio = new Audio();
44
+ }
45
+ // Handoff from previous owner if different
46
+ if (this.sharedOwnerId && this.sharedOwnerId !== ownerId) {
47
+ const previous = this.pool.get(this.sharedOwnerId);
48
+ // Ask previous to pause and drop ref, but keep player in pool
49
+ previous?.pause();
50
+ previous?.releaseElementForHandoff();
51
+ }
52
+ this.sharedOwnerId = ownerId;
53
+ if (this.sharedAudio.src !== src) {
54
+ // setting src starts loading; avoid explicit load() to prevent currentTime reset flicker
55
+ this.sharedAudio.src = src;
56
+ }
57
+ return this.sharedAudio;
58
+ }
59
+ // Concurrent-per-owner mode
60
+ let audio = this.audios.get(ownerId);
61
+ if (!audio) {
62
+ audio = new Audio();
63
+ this.audios.set(ownerId, audio);
64
+ }
65
+ if (audio.src !== src) {
66
+ // setting src starts loading; avoid explicit load() here as well
67
+ audio.src = src;
68
+ }
69
+ return audio;
70
+ };
71
+ /**
72
+ * Removes the given audio players ownership of the shared audio element (in case of concurrent playback is disabled)
73
+ * and pauses the reproduction of the audio.
74
+ * In case of concurrent playback mode (allowConcurrentPlayback enabled), the audio is paused,
75
+ * its source cleared and removed from the audios pool readied for garbage collection.
76
+ *
77
+ * @param ownerId
78
+ */
79
+ this.releaseElement = (ownerId) => {
80
+ if (!this.allowConcurrentPlayback) {
81
+ if (this.sharedOwnerId !== ownerId)
82
+ return;
83
+ const el = this.sharedAudio;
84
+ if (el) {
85
+ try {
86
+ el.pause();
87
+ }
88
+ catch {
89
+ // ignore
90
+ }
91
+ el.removeAttribute('src');
92
+ el.load();
93
+ }
94
+ // Keep shared element instance for reuse
95
+ this.sharedOwnerId = null;
96
+ return;
97
+ }
98
+ const el = this.audios.get(ownerId);
99
+ if (!el)
100
+ return;
101
+ try {
102
+ el.pause();
103
+ }
104
+ catch {
105
+ // ignore
106
+ }
107
+ el.removeAttribute('src');
108
+ el.load();
109
+ this.audios.delete(ownerId);
110
+ };
111
+ /** Sets active audio player when allowConcurrentPlayback is disabled */
112
+ this.setActiveAudioPlayer = (activeAudioPlayer) => {
113
+ if (this.allowConcurrentPlayback)
114
+ return;
115
+ this.state.partialNext({ activeAudioPlayer });
116
+ };
117
+ /** Performs all the necessary cleanup actions and removes the player from the pool */
118
+ this.remove = (id) => {
119
+ const player = this.pool.get(id);
120
+ if (!player)
121
+ return;
122
+ player.requestRemoval();
123
+ };
124
+ /** Removes and cleans up all the players from the pool */
125
+ this.clear = () => {
126
+ this.players.forEach((player) => {
127
+ this.remove(player.id);
128
+ });
129
+ };
130
+ this.registerSubscriptions = () => {
131
+ // Only register subscriptions for players that have an attached element.
132
+ // Avoid creating elements or cross-wiring listeners on the shared element in single-playback mode.
133
+ this.players.forEach((p) => {
134
+ if (p.elementRef) {
135
+ p.registerSubscriptions();
136
+ }
137
+ });
138
+ };
139
+ this.allowConcurrentPlayback = !!config?.allowConcurrentPlayback;
140
+ }
141
+ get players() {
142
+ return Array.from(this.pool.values());
143
+ }
144
+ get activeAudioPlayer() {
145
+ return this.state.getLatestValue().activeAudioPlayer;
146
+ }
147
+ /** Removes the AudioPlayer instance from the pool of players */
148
+ deregister(id) {
149
+ if (this.pool.has(id)) {
150
+ this.pool.delete(id);
151
+ }
152
+ if (this.activeAudioPlayer?.id === id) {
153
+ this.setActiveAudioPlayer(null);
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import type { AudioPlayerOptions } from './AudioPlayer';
3
+ export type WithAudioPlaybackProps = {
4
+ children?: React.ReactNode;
5
+ allowConcurrentPlayback?: boolean;
6
+ };
7
+ export declare const WithAudioPlayback: ({ allowConcurrentPlayback, children, }: WithAudioPlaybackProps) => React.JSX.Element;
8
+ export type UseAudioPlayerProps = {
9
+ /**
10
+ * Identifier of the entity that requested the audio playback, e.g. message ID.
11
+ * Asset to specific audio player is a many-to-many relationship
12
+ * - one URL can be associated with multiple UI elements,
13
+ * - one UI element can display multiple audio sources.
14
+ * Therefore, the AudioPlayer ID is a combination of request:src.
15
+ *
16
+ * The requester string can take into consideration whether there are multiple instances of
17
+ * the same URL requested by the same requester (message has multiple attachments with the same asset URL).
18
+ * In reality the fact that one message has multiple attachments with the same asset URL
19
+ * could be considered a bad practice or a bug.
20
+ */
21
+ requester?: string;
22
+ } & Partial<Omit<AudioPlayerOptions, 'id' | 'pool'>>;
23
+ export declare const useAudioPlayer: ({ durationSeconds, fileSize, mimeType, playbackRates, plugins, requester, src, title, waveformData, }: UseAudioPlayerProps) => import("./AudioPlayer").AudioPlayer | undefined;
24
+ export declare const useActiveAudioPlayer: () => import("./AudioPlayer").AudioPlayer | null | undefined;