1+ import type { IAudioBackend } from "../index" ;
2+
3+ export class HTMLAudioBackend implements IAudioBackend {
4+ private audio : HTMLAudioElement ;
5+ private timeUpdateCallback : ( ( time : number ) => void ) | null = null ;
6+ private endedCallback : ( ( ) => void ) | null = null ;
7+ private errorCallback : ( ( error : Error ) => void ) | null = null ;
8+ private boundOnTimeUpdate : ( ) => void ;
9+ private boundOnEnded : ( ) => void ;
10+ private boundOnError : ( ) => void ;
11+ private boundOnLoadedMetadata : ( ) => void ;
12+ private duration = 0 ;
13+
14+ constructor ( ) {
15+ this . audio = new Audio ( ) ;
16+ this . audio . preload = "auto" ;
17+
18+ this . boundOnTimeUpdate = this . handleTimeUpdate . bind ( this ) ;
19+ this . boundOnEnded = this . handleEnded . bind ( this ) ;
20+ this . boundOnError = this . handleError . bind ( this ) ;
21+ this . boundOnLoadedMetadata = this . handleLoadedMetadata . bind ( this ) ;
22+
23+ this . audio . addEventListener ( "timeupdate" , this . boundOnTimeUpdate ) ;
24+ this . audio . addEventListener ( "ended" , this . boundOnEnded ) ;
25+ this . audio . addEventListener ( "error" , this . boundOnError ) ;
26+ this . audio . addEventListener ( "loadedmetadata" , this . boundOnLoadedMetadata ) ;
27+ }
28+
29+ async load ( url : string ) : Promise < void > {
30+ return new Promise ( ( resolve , reject ) => {
31+ const onCanPlayThrough = ( ) => {
32+ this . audio . removeEventListener ( "canplaythrough" , onCanPlayThrough ) ;
33+ this . audio . removeEventListener ( "error" , onError ) ;
34+ resolve ( ) ;
35+ } ;
36+
37+ const onError = ( ) => {
38+ this . audio . removeEventListener ( "canplaythrough" , onCanPlayThrough ) ;
39+ this . audio . removeEventListener ( "error" , onError ) ;
40+ reject ( new Error ( `Failed to load audio: ${ url } ` ) ) ;
41+ } ;
42+
43+ this . audio . addEventListener ( "canplaythrough" , onCanPlayThrough ) ;
44+ this . audio . addEventListener ( "error" , onError ) ;
45+
46+ this . audio . src = url ;
47+ this . audio . load ( ) ;
48+ } ) ;
49+ }
50+
51+ async play ( ) : Promise < void > {
52+ if ( this . audio . paused ) {
53+ try {
54+ await this . audio . play ( ) ;
55+ } catch ( error ) {
56+ throw new Error ( `Play failed: ${ ( error as Error ) . message } ` ) ;
57+ }
58+ }
59+ }
60+
61+ pause ( ) : void {
62+ if ( ! this . audio . paused ) {
63+ this . audio . pause ( ) ;
64+ }
65+ }
66+
67+ stop ( ) : void {
68+ this . audio . pause ( ) ;
69+ this . audio . currentTime = 0 ;
70+ }
71+
72+ seek ( time : number ) : void {
73+ const wasPlaying = ! this . audio . paused ;
74+ this . audio . currentTime = time ;
75+ if ( wasPlaying ) {
76+ this . audio . play ( ) . catch ( ( ) => { } ) ;
77+ }
78+ }
79+
80+ setVolume ( volume : number ) : void {
81+ this . audio . volume = Math . max ( 0 , Math . min ( 1 , volume ) ) ;
82+ }
83+
84+ setPlaybackRate ( rate : number ) : void {
85+ this . audio . playbackRate = Math . max ( 0.25 , Math . min ( 4 , rate ) ) ;
86+ }
87+
88+ getCurrentTime ( ) : number {
89+ return this . audio . currentTime ;
90+ }
91+
92+ getDuration ( ) : number {
93+ return this . duration || this . audio . duration || 0 ;
94+ }
95+
96+ onTimeUpdate ( callback : ( time : number ) => void ) : void {
97+ this . timeUpdateCallback = callback ;
98+ }
99+
100+ onEnded ( callback : ( ) => void ) : void {
101+ this . endedCallback = callback ;
102+ }
103+
104+ onError ( callback : ( error : Error ) => void ) : void {
105+ this . errorCallback = callback ;
106+ }
107+
108+ dispose ( ) : void {
109+ this . audio . removeEventListener ( "timeupdate" , this . boundOnTimeUpdate ) ;
110+ this . audio . removeEventListener ( "ended" , this . boundOnEnded ) ;
111+ this . audio . removeEventListener ( "error" , this . boundOnError ) ;
112+ this . audio . removeEventListener ( "loadedmetadata" , this . boundOnLoadedMetadata ) ;
113+
114+ this . audio . pause ( ) ;
115+ this . audio . src = "" ;
116+ this . audio . load ( ) ;
117+
118+ this . timeUpdateCallback = null ;
119+ this . endedCallback = null ;
120+ this . errorCallback = null ;
121+ }
122+
123+ private handleTimeUpdate ( ) : void {
124+ if ( this . timeUpdateCallback ) {
125+ this . timeUpdateCallback ( this . audio . currentTime ) ;
126+ }
127+ }
128+
129+ private handleEnded ( ) : void {
130+ if ( this . endedCallback ) {
131+ this . endedCallback ( ) ;
132+ }
133+ }
134+
135+ private handleError ( ) : void {
136+ const error = this . audio . error ;
137+ if ( this . errorCallback ) {
138+ this . errorCallback (
139+ new Error ( error ?. message ?? "Unknown audio error" )
140+ ) ;
141+ }
142+ }
143+
144+ private handleLoadedMetadata ( ) : void {
145+ this . duration = this . audio . duration ;
146+ }
147+ }
148+
149+ export function createHTMLBackend ( ) : IAudioBackend {
150+ return new HTMLAudioBackend ( ) ;
151+ }
0 commit comments