Skip to content

Commit bc598a2

Browse files
committed
refactor: move event listener registration to AudioPlayer
1 parent e889046 commit bc598a2

File tree

2 files changed

+99
-123
lines changed

2 files changed

+99
-123
lines changed

src/components/AudioPlayer/AudioPlayer.ts

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class AudioPlayer {
7070
private _durationSeconds?: number;
7171
private _plugins = new Map<string, AudioPlayerPlugin>();
7272
private playTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
73+
private unsubscribeEventListeners: (() => void) | null = null;
7374

7475
constructor({
7576
durationSeconds,
@@ -153,6 +154,25 @@ export class AudioPlayer {
153154
return this._mimeType;
154155
}
155156

157+
private setPlaybackStartSafetyTimeout = () => {
158+
clearTimeout(this.playTimeout);
159+
this.playTimeout = setTimeout(() => {
160+
if (!this.elementRef) return;
161+
try {
162+
this.elementRef.pause();
163+
this.state.partialNext({ isPlaying: false });
164+
} catch (e) {
165+
this.registerError({ errCode: 'failed-to-start' });
166+
}
167+
}, 2000);
168+
};
169+
170+
private clearPlaybackStartSafetyTimeout = () => {
171+
if (!this.elementRef) return;
172+
clearTimeout(this.playTimeout);
173+
this.playTimeout = undefined;
174+
};
175+
156176
private setDescriptor({ durationSeconds, mimeType, src }: AudioDescriptor) {
157177
if (mimeType !== this._mimeType) {
158178
this._mimeType = mimeType;
@@ -193,25 +213,6 @@ export class AudioPlayer {
193213
}, new Map<string, AudioPlayerPlugin>());
194214
}
195215

196-
private setupSafetyTimeout = () => {
197-
clearTimeout(this.playTimeout);
198-
this.playTimeout = setTimeout(() => {
199-
if (!this.elementRef) return;
200-
try {
201-
this.elementRef.pause();
202-
this.state.partialNext({ isPlaying: false });
203-
} catch (e) {
204-
this.registerError({ errCode: 'failed-to-start' });
205-
}
206-
}, 2000);
207-
};
208-
209-
private clearSafetyTimeout = () => {
210-
if (!this.elementRef) return;
211-
clearTimeout(this.playTimeout);
212-
this.playTimeout = undefined;
213-
};
214-
215216
canPlayMimeType = (mimeType: string) =>
216217
!!(mimeType && this.elementRef?.canPlayType(mimeType));
217218

@@ -235,7 +236,7 @@ export class AudioPlayer {
235236

236237
this.elementRef.playbackRate = currentPlaybackRate ?? this.currentPlaybackRate;
237238

238-
this.setupSafetyTimeout();
239+
this.setPlaybackStartSafetyTimeout();
239240

240241
try {
241242
await this.elementRef.play();
@@ -248,13 +249,13 @@ export class AudioPlayer {
248249
this.registerError({ error: e as Error });
249250
this.state.partialNext({ isPlaying: false });
250251
} finally {
251-
this.clearSafetyTimeout();
252+
this.clearPlaybackStartSafetyTimeout();
252253
}
253254
};
254255

255256
pause = () => {
256257
if (!elementIsPlaying(this.elementRef)) return;
257-
this.clearSafetyTimeout();
258+
this.clearPlaybackStartSafetyTimeout();
258259

259260
// existence of the element already checked by elementIsPlaying
260261
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -309,6 +310,68 @@ export class AudioPlayer {
309310
};
310311

311312
onRemove = () => {
313+
this.unsubscribeEventListeners?.();
314+
this.unsubscribeEventListeners = null;
312315
this.plugins.forEach(({ onRemove }) => onRemove?.({ player: this }));
313316
};
317+
318+
registerSubscriptions = () => {
319+
this.unsubscribeEventListeners?.();
320+
321+
const audioElement = this.elementRef;
322+
323+
const handleEnded = () => {
324+
this.state.partialNext({
325+
isPlaying: false,
326+
secondsElapsed: audioElement?.duration ?? this.durationSeconds ?? 0,
327+
});
328+
};
329+
330+
const handleError = (e: HTMLMediaElementEventMap['error']) => {
331+
// if fired probably is one of these (e.srcElement.error.code)
332+
// 1 = MEDIA_ERR_ABORTED (fetch aborted by user/JS)
333+
// 2 = MEDIA_ERR_NETWORK (network failed while fetching)
334+
// 3 = MEDIA_ERR_DECODE (data fetched but couldn’t decode)
335+
// 4 = MEDIA_ERR_SRC_NOT_SUPPORTED (no resource supported / bad type)
336+
// reported during the mount so only logging to the console
337+
const audio = e.currentTarget as HTMLAudioElement | null;
338+
const state: Partial<AudioPlayerState> = { isPlaying: false };
339+
340+
if (!audio?.error?.code) {
341+
this.state.partialNext(state);
342+
return;
343+
}
344+
345+
if (audio.error.code === 4) {
346+
state.canPlayRecord = false;
347+
this.state.partialNext(state);
348+
}
349+
350+
const errorMsg = [
351+
undefined,
352+
'MEDIA_ERR_ABORTED: fetch aborted by user',
353+
'MEDIA_ERR_NETWORK: network failed while fetching',
354+
'MEDIA_ERR_DECODE: audio fetched but couldn’t decode',
355+
'MEDIA_ERR_SRC_NOT_SUPPORTED: source not supported',
356+
][audio?.error?.code];
357+
if (!errorMsg) return;
358+
359+
defaultRegisterAudioPlayerError({ error: new Error(errorMsg + ` (${audio.src})`) });
360+
};
361+
362+
const handleTimeupdate = () => {
363+
this.setSecondsElapsed(audioElement?.currentTime);
364+
};
365+
366+
audioElement.addEventListener('ended', handleEnded);
367+
audioElement.addEventListener('error', handleError);
368+
audioElement.addEventListener('timeupdate', handleTimeupdate);
369+
370+
this.unsubscribeEventListeners = () => {
371+
audioElement.pause();
372+
audioElement.removeEventListener('ended', handleEnded);
373+
audioElement.removeEventListener('error', handleError);
374+
audioElement.removeEventListener('timeupdate', handleTimeupdate);
375+
};
376+
};
314377
}
Lines changed: 14 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,36 @@
1-
import {
2-
AudioPlayer,
3-
type AudioPlayerOptions,
4-
type AudioPlayerState,
5-
defaultRegisterAudioPlayerError,
6-
} from './AudioPlayer';
1+
import { AudioPlayer, type AudioPlayerOptions } from './AudioPlayer';
72

83
export class AudioPlayerPool {
9-
pool = new Map<string, { player: AudioPlayer; unsubscribe?: () => void }>();
4+
pool = new Map<string, AudioPlayer>();
105

116
getOrAdd = (params: AudioPlayerOptions) => {
12-
let player = this.pool.get(params.id)?.player;
7+
let player = this.pool.get(params.id);
138
if (player) return player;
149
player = new AudioPlayer(params);
15-
16-
this.pool.set(params.id, {
17-
player,
18-
unsubscribe: this.registerSubscriptions(player),
19-
});
10+
player.registerSubscriptions();
11+
this.pool.set(params.id, player);
2012
return player;
2113
};
2214

2315
remove = (id: string) => {
2416
const player = this.pool.get(id);
2517
if (!player) return;
26-
player.unsubscribe?.();
27-
player.player.stop();
28-
player.player.elementRef.src = '';
29-
player.player.elementRef.load();
30-
player.player.onRemove();
18+
player.stop();
19+
player.elementRef.src = '';
20+
player.elementRef.load();
21+
player.onRemove();
3122
this.pool.delete(id);
3223
};
3324

3425
clear = () => {
35-
Array.from(this.pool.values()).forEach(({ player }) => {
26+
Array.from(this.pool.values()).forEach((player) => {
3627
this.remove(player.id);
3728
});
3829
};
3930

40-
registerSubscriptions = (player?: AudioPlayer) => {
41-
if (!player) {
42-
Array.from(this.pool.values()).forEach((p) => {
43-
this.registerSubscriptions(p.player);
44-
});
45-
return;
46-
}
47-
48-
const poolPlayer = this.pool.get(player.id);
49-
50-
poolPlayer?.unsubscribe?.();
51-
52-
const audioElement = player.elementRef;
53-
54-
const handleEnded = () => {
55-
player.state.partialNext({
56-
isPlaying: false,
57-
secondsElapsed: audioElement?.duration ?? player.durationSeconds ?? 0,
58-
});
59-
};
60-
61-
const handleError = (e: HTMLMediaElementEventMap['error']) => {
62-
// if fired probably is one of these (e.srcElement.error.code)
63-
// 1 = MEDIA_ERR_ABORTED (fetch aborted by user/JS)
64-
// 2 = MEDIA_ERR_NETWORK (network failed while fetching)
65-
// 3 = MEDIA_ERR_DECODE (data fetched but couldn’t decode)
66-
// 4 = MEDIA_ERR_SRC_NOT_SUPPORTED (no resource supported / bad type)
67-
// reported during the mount so only logging to the console
68-
const audio = e.currentTarget as HTMLAudioElement | null;
69-
const state: Partial<AudioPlayerState> = { isPlaying: false };
70-
71-
if (!audio?.error?.code) {
72-
player.state.partialNext(state);
73-
return;
74-
}
75-
76-
if (audio.error.code === 4) {
77-
state.canPlayRecord = false;
78-
player.state.partialNext(state);
79-
}
80-
81-
const errorMsg = [
82-
undefined,
83-
'MEDIA_ERR_ABORTED: fetch aborted by user',
84-
'MEDIA_ERR_NETWORK: network failed while fetching',
85-
'MEDIA_ERR_DECODE: audio fetched but couldn’t decode',
86-
'MEDIA_ERR_SRC_NOT_SUPPORTED: source not supported',
87-
][audio?.error?.code];
88-
if (!errorMsg) return;
89-
90-
defaultRegisterAudioPlayerError({ error: new Error(errorMsg + ` (${audio.src})`) });
91-
};
92-
93-
const handleTimeupdate = () => {
94-
player.setSecondsElapsed(audioElement?.currentTime);
95-
};
96-
97-
audioElement.addEventListener('ended', handleEnded);
98-
audioElement.addEventListener('error', handleError);
99-
audioElement.addEventListener('timeupdate', handleTimeupdate);
100-
101-
return () => {
102-
audioElement.pause();
103-
audioElement.removeEventListener('ended', handleEnded);
104-
audioElement.removeEventListener('error', handleError);
105-
audioElement.removeEventListener('timeupdate', handleTimeupdate);
106-
};
107-
};
108-
109-
unregisterSubscriptions = (id?: string) => {
110-
if (!id) {
111-
Array.from(this.pool.values()).forEach(({ unsubscribe }) => unsubscribe?.());
112-
for (const { player } of this.pool.values()) {
113-
this.pool.set(player.id, { player });
114-
}
115-
return;
116-
}
117-
const player = this.pool.get(id);
118-
if (!player) return;
119-
120-
player.unsubscribe?.();
121-
delete player.unsubscribe;
31+
registerSubscriptions = () => {
32+
Array.from(this.pool.values()).forEach((p) => {
33+
p.registerSubscriptions();
34+
});
12235
};
12336
}

0 commit comments

Comments
 (0)