@@ -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}
0 commit comments