11<!DOCTYPE html>
2- < html >
3-
2+ < html lang ="en ">
43< head >
5- < title > </ title >
6- < style type ="text/css ">
7- html ,
8- body {
9- width : 600px ;
4+ < meta charset ="UTF-8 ">
5+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
6+ < title > Gapless.js Demo - Seamless Audio Playback</ title >
7+ < script src ="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4 "> </ script >
8+ < link rel ="stylesheet " href ="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css ">
9+ < style >
10+ @keyframes pulse-ring {
11+ 0% { transform : scale (0.95 ); box-shadow : 0 0 0 0 rgba (59 , 130 , 246 , 0.7 ); }
12+ 70% { transform : scale (1 ); box-shadow : 0 0 0 10px rgba (59 , 130 , 246 , 0 ); }
13+ 100% { transform : scale (0.95 ); box-shadow : 0 0 0 0 rgba (59 , 130 , 246 , 0 ); }
1014 }
15+ .playing-indicator { animation : pulse-ring 2s ease-in-out infinite; }
1116
12- .loaded {
13- color : green;
17+ @keyframes soundwave {
18+ 0% , 100% { height : 5px ; }
19+ 50% { height : 20px ; }
20+ }
21+ .soundwave-bar {
22+ width : 3px ;
23+ background-color : # 3b82f6 ;
24+ margin : 0 1px ;
25+ transition : height 0.1s ease;
1426 }
27+ .soundwave-bar : nth-child (1 ) { animation : soundwave 0.5s ease-in-out infinite; }
28+ .soundwave-bar : nth-child (2 ) { animation : soundwave 0.5s ease-in-out 0.1s infinite; }
29+ .soundwave-bar : nth-child (3 ) { animation : soundwave 0.5s ease-in-out 0.2s infinite; }
30+ .soundwave-bar : nth-child (4 ) { animation : soundwave 0.5s ease-in-out 0.3s infinite; }
31+ .soundwave-bar : nth-child (5 ) { animation : soundwave 0.5s ease-in-out 0.4s infinite; }
32+
33+ .track-item { transition : all 0.3s ease; }
34+ .track-item : hover { transform : translateX (5px ); }
35+
36+ .progress-bar { transition : width 0.1s linear; }
1537 </ style >
1638</ head >
39+ < body class ="bg-gray-900 text-white min-h-screen ">
40+ < div class ="max-w-4xl mx-auto p-6 ">
41+ <!-- Header -->
42+ < header class ="text-center mb-10 ">
43+ < h1 class ="text-4xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-purple-600 bg-clip-text text-transparent ">
44+ Gapless.js Demo
45+ </ h1 >
46+ < p class ="text-gray-400 "> Experience seamless audio playback with zero gaps between tracks</ p >
47+ </ header >
48+
49+ <!-- Main Player -->
50+ < div class ="bg-gray-800 rounded-2xl shadow-2xl p-8 mb-6 ">
51+ <!-- Current Track Info -->
52+ < div class ="text-center mb-6 ">
53+ < h2 id ="currentTrackTitle " class ="text-2xl font-semibold mb-2 "> No track loaded</ h2 >
54+ < div id ="trackStatus " class ="text-sm text-gray-400 mb-4 ">
55+ < span id ="currentTime "> 0:00</ span > / < span id ="duration "> 0:00</ span >
56+ </ div >
57+ </ div >
58+
59+ <!-- Visualizer -->
60+ < div class ="flex justify-center items-center h-12 mb-6 " id ="visualizer ">
61+ < div class ="flex items-end h-full " id ="soundwaveBars " style ="display: none; ">
62+ < div class ="soundwave-bar "> </ div >
63+ < div class ="soundwave-bar "> </ div >
64+ < div class ="soundwave-bar "> </ div >
65+ < div class ="soundwave-bar "> </ div >
66+ < div class ="soundwave-bar "> </ div >
67+ </ div >
68+ </ div >
69+
70+ <!-- Progress Bar -->
71+ < div class ="mb-6 ">
72+ < div class ="bg-gray-700 rounded-full h-3 overflow-hidden cursor-pointer " id ="progressContainer ">
73+ < div class ="progress-bar bg-gradient-to-r from-blue-500 to-purple-500 h-full rounded-full " id ="progressBar " style ="width: 0% "> </ div >
74+ </ div >
75+ </ div >
1776
18- < body >
19- < h4 >
20- You'll want to wait until WebAudio is loaded for both the current track and the next track before jumping to the
21- end. Otherwise you may notice a slight gap. If both WebAudios are fully loaded you should hear no discernable gap
22- most of the time.
23- </ h4 >
24-
25- < div onClick ="player.currentTrack.seekToEnd() "> Click to jump to end of track</ div >
26- < div onClick ="player.currentTrack.togglePlayPause() "> Toggle Play/Pause</ div >
27- < pre id ="ready "> Both tracks are not loaded</ pre >
28- < pre id ="status "> </ pre >
29- < script type ="text/javascript " src ="./dist/cjs/index.cjs " type ="module "> </ script >
77+ <!-- Controls -->
78+ < div class ="flex justify-center items-center space-x-6 mb-6 ">
79+ < button id ="prevBtn " class ="p-3 rounded-full bg-gray-700 hover:bg-gray-600 transition-colors ">
80+ < i class ="fas fa-backward text-xl "> </ i >
81+ </ button >
82+ < button id ="playPauseBtn " class ="p-4 rounded-full bg-blue-600 hover:bg-blue-700 transition-colors playing-indicator ">
83+ < i class ="fas fa-play text-2xl w-8 text-center " id ="playPauseIcon "> </ i >
84+ </ button >
85+ < button id ="nextBtn " class ="p-3 rounded-full bg-gray-700 hover:bg-gray-600 transition-colors ">
86+ < i class ="fas fa-forward text-xl "> </ i >
87+ </ button >
88+ </ div >
89+
90+ <!-- Volume Control -->
91+ < div class ="flex items-center justify-center space-x-3 ">
92+ < i class ="fas fa-volume-down text-gray-400 "> </ i >
93+ < input type ="range " id ="volumeSlider " min ="0 " max ="100 " value ="70 "
94+ class ="w-32 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer ">
95+ < i class ="fas fa-volume-up text-gray-400 "> </ i >
96+ </ div >
97+ </ div >
98+
99+ <!-- Playlist -->
100+ < div class ="bg-gray-800 rounded-2xl shadow-2xl p-6 ">
101+ < h3 class ="text-xl font-semibold mb-4 flex items-center ">
102+ < i class ="fas fa-list-music mr-2 "> </ i > Playlist
103+ </ h3 >
104+ < div id ="playlist " class ="space-y-2 ">
105+ <!-- Tracks will be inserted here -->
106+ </ div >
107+ </ div >
108+
109+ <!-- Status Information -->
110+ < div class ="mt-6 bg-gray-800 rounded-2xl shadow-2xl p-6 ">
111+ < h3 class ="text-xl font-semibold mb-4 flex items-center ">
112+ < i class ="fas fa-info-circle mr-2 "> </ i > Technical Status
113+ </ h3 >
114+ < div class ="grid grid-cols-2 gap-4 text-sm ">
115+ < div >
116+ < span class ="text-gray-400 "> WebAudio Status:</ span >
117+ < span id ="webAudioStatus " class ="ml-2 font-mono "> Checking...</ span >
118+ </ div >
119+ < div >
120+ < span class ="text-gray-400 "> Buffer Status:</ span >
121+ < span id ="bufferStatus " class ="ml-2 font-mono "> N/A</ span >
122+ </ div >
123+ < div >
124+ < span class ="text-gray-400 "> Current Track:</ span >
125+ < span id ="currentTrackIdx " class ="ml-2 font-mono "> N/A</ span >
126+ </ div >
127+ < div >
128+ < span class ="text-gray-400 "> Next Track Loaded:</ span >
129+ < span id ="nextTrackLoaded " class ="ml-2 font-mono "> N/A</ span >
130+ </ div >
131+ </ div >
132+ </ div >
133+ </ div >
134+
135+ < script type ="text/javascript " src ="./dist/cjs/index.cjs "> </ script >
30136 < script type ="text/javascript ">
137+ // Track metadata
138+ const trackMetadata = [
139+ { title : "Electric Dreams" , artist : "JRAD" , album : "Live 2017-03-17" } ,
140+ { title : "Cosmic Highway" , artist : "JRAD" , album : "Live 2017-03-17" } ,
141+ { title : "Starlight Serenade" , artist : "JRAD" , album : "Live 2017-03-17" } ,
142+ { title : "Neon Nights" , artist : "JRAD" , album : "Live 2017-03-17" }
143+ ] ;
144+
145+ // Initialize player
31146 window . player = new Gapless . Queue ( {
32147 tracks : [
33148 "https://archive.org/download/jrad2017-03-17.cmc621.cmc64.sbd.matrix.flac16/jrad2017-03-17.cmc621.cmc64.sbd.matrix-s2t03.mp3" ,
34149 "https://archive.org/download/jrad2017-03-17.cmc621.cmc64.sbd.matrix.flac16/jrad2017-03-17.cmc621.cmc64.sbd.matrix-s2t04.mp3" ,
35150 "https://archive.org/download/jrad2017-03-17.cmc621.cmc64.sbd.matrix.flac16/jrad2017-03-17.cmc621.cmc64.sbd.matrix-s2t05.mp3" ,
36151 "https://archive.org/download/jrad2017-03-17.cmc621.cmc64.sbd.matrix.flac16/jrad2017-03-17.cmc621.cmc64.sbd.matrix-s2t06.mp3"
37152 ] ,
38- onProgress : function ( track ) {
39- if ( player . nextTrack && player . nextTrack . webAudioLoadingState === 'LOADED' ) {
40- document . querySelector ( '#ready' ) . classList . add ( 'loaded' ) ;
41- document . querySelector ( '#ready' ) . innerHTML = 'Both tracks are LOADED' ;
42- }
43- document . querySelector ( '#status' ) . innerHTML = JSON . stringify ( {
44- currentTrack : track ? track . completeState : { } ,
45- nextTrack : player . nextTrack ? player . nextTrack . completeState : { }
46- } , null , 2 ) ;
153+ onProgress : updateProgress ,
154+ onPlayNextTrack : updateTrackDisplay ,
155+ onPlayPreviousTrack : updateTrackDisplay ,
156+ onStartNewTrack : updateTrackDisplay ,
157+ onEnded : ( ) => {
158+ console . log ( 'Playlist ended' ) ;
159+ updatePlayPauseButton ( false ) ;
47160 }
48161 } ) ;
49162
50- player . play ( ) ;
163+ // Add metadata to tracks
164+ player . tracks . forEach ( ( track , idx ) => {
165+ track . metadata = trackMetadata [ idx ] ;
166+ } ) ;
167+
168+ // DOM elements
169+ const playPauseBtn = document . getElementById ( 'playPauseBtn' ) ;
170+ const playPauseIcon = document . getElementById ( 'playPauseIcon' ) ;
171+ const prevBtn = document . getElementById ( 'prevBtn' ) ;
172+ const nextBtn = document . getElementById ( 'nextBtn' ) ;
173+ const progressBar = document . getElementById ( 'progressBar' ) ;
174+ const progressContainer = document . getElementById ( 'progressContainer' ) ;
175+ const currentTimeEl = document . getElementById ( 'currentTime' ) ;
176+ const durationEl = document . getElementById ( 'duration' ) ;
177+ const volumeSlider = document . getElementById ( 'volumeSlider' ) ;
178+ const soundwaveBars = document . getElementById ( 'soundwaveBars' ) ;
179+ const currentTrackTitle = document . getElementById ( 'currentTrackTitle' ) ;
180+ const playlist = document . getElementById ( 'playlist' ) ;
181+
182+ // Event listeners
183+ playPauseBtn . addEventListener ( 'click' , ( ) => {
184+ player . togglePlayPause ( ) ;
185+ updatePlayPauseButton ( ! player . currentTrack ?. isPaused ) ;
186+ } ) ;
187+
188+ prevBtn . addEventListener ( 'click' , ( ) => player . playPrevious ( ) ) ;
189+ nextBtn . addEventListener ( 'click' , ( ) => player . playNext ( ) ) ;
190+
191+ volumeSlider . addEventListener ( 'input' , ( e ) => {
192+ player . setVolume ( e . target . value / 100 ) ;
193+ } ) ;
51194
195+ progressContainer . addEventListener ( 'click' , ( e ) => {
196+ if ( player . currentTrack && player . currentTrack . duration ) {
197+ const rect = progressContainer . getBoundingClientRect ( ) ;
198+ const x = e . clientX - rect . left ;
199+ const percentage = x / rect . width ;
200+ const newTime = percentage * player . currentTrack . duration ;
201+ player . currentTrack . seek ( newTime ) ;
202+ }
203+ } ) ;
204+
205+ // Helper functions
206+ function formatTime ( seconds ) {
207+ if ( ! seconds || isNaN ( seconds ) ) return '0:00' ;
208+ const mins = Math . floor ( seconds / 60 ) ;
209+ const secs = Math . floor ( seconds % 60 ) ;
210+ return `${ mins } :${ secs . toString ( ) . padStart ( 2 , '0' ) } ` ;
211+ }
212+
213+ function updatePlayPauseButton ( isPlaying ) {
214+ playPauseIcon . className = isPlaying ? 'fas fa-pause text-2xl w-8 text-center' : 'fas fa-play text-2xl w-8 text-center' ;
215+ if ( isPlaying ) {
216+ playPauseBtn . classList . add ( 'playing-indicator' ) ;
217+ soundwaveBars . style . display = 'flex' ;
218+ } else {
219+ playPauseBtn . classList . remove ( 'playing-indicator' ) ;
220+ soundwaveBars . style . display = 'none' ;
221+ }
222+ }
223+
224+ function updateProgress ( track ) {
225+ if ( ! track ) return ;
226+
227+ // Update time display
228+ currentTimeEl . textContent = formatTime ( track . currentTime ) ;
229+ durationEl . textContent = formatTime ( track . duration ) ;
230+
231+ // Update progress bar
232+ if ( track . duration ) {
233+ const percentage = ( track . currentTime / track . duration ) * 100 ;
234+ progressBar . style . width = `${ percentage } %` ;
235+ }
236+
237+ // Update technical status
238+ document . getElementById ( 'webAudioStatus' ) . textContent = track . isUsingWebAudio ? 'Enabled' : 'Disabled' ;
239+ document . getElementById ( 'webAudioStatus' ) . className = track . isUsingWebAudio ? 'ml-2 font-mono text-green-400' : 'ml-2 font-mono text-yellow-400' ;
240+
241+ document . getElementById ( 'bufferStatus' ) . textContent = track . isLoaded ? 'Ready' : 'Loading...' ;
242+ document . getElementById ( 'bufferStatus' ) . className = track . isLoaded ? 'ml-2 font-mono text-green-400' : 'ml-2 font-mono text-yellow-400' ;
243+
244+ document . getElementById ( 'currentTrackIdx' ) . textContent = `${ track . idx + 1 } / ${ player . tracks . length } ` ;
245+
246+ const nextTrackStatus = player . nextTrack ?. isLoaded ? 'Yes' : 'No' ;
247+ document . getElementById ( 'nextTrackLoaded' ) . textContent = nextTrackStatus ;
248+ document . getElementById ( 'nextTrackLoaded' ) . className = player . nextTrack ?. isLoaded ? 'ml-2 font-mono text-green-400' : 'ml-2 font-mono text-yellow-400' ;
249+ }
250+
251+ function updateTrackDisplay ( track ) {
252+ if ( ! track ) return ;
253+
254+ // Update current track title
255+ const metadata = track . metadata || { } ;
256+ currentTrackTitle . textContent = metadata . title || `Track ${ track . idx + 1 } ` ;
257+
258+ // Update playlist highlighting
259+ renderPlaylist ( ) ;
260+
261+ // Update play/pause button
262+ updatePlayPauseButton ( ! track . isPaused ) ;
263+ }
264+
265+ function renderPlaylist ( ) {
266+ playlist . innerHTML = '' ;
267+ player . tracks . forEach ( ( track , idx ) => {
268+ const metadata = track . metadata || { } ;
269+ const isActive = player . currentTrack ?. idx === idx ;
270+
271+ const trackEl = document . createElement ( 'div' ) ;
272+ trackEl . className = `track-item p-3 rounded-lg cursor-pointer flex items-center space-x-3 ${
273+ isActive ? 'bg-blue-600 bg-opacity-20 border border-blue-500' : 'bg-gray-700 hover:bg-gray-600'
274+ } `;
275+
276+ trackEl . innerHTML = `
277+ <div class="text-2xl w-8 text-center">
278+ ${ isActive && ! player . currentTrack . isPaused ? '<i class="fas fa-volume-up text-blue-400"></i>' : `<span class="text-gray-500">${ idx + 1 } </span>` }
279+ </div>
280+ <div class="flex-1">
281+ <div class="font-semibold">${ metadata . title || `Track ${ idx + 1 } ` } </div>
282+ <div class="text-sm text-gray-400">${ metadata . artist || 'Unknown Artist' } </div>
283+ </div>
284+ <div class="text-sm text-gray-400">
285+ ${ track . duration ? formatTime ( track . duration ) : '--:--' }
286+ </div>
287+ ` ;
288+
289+ trackEl . addEventListener ( 'click' , ( ) => {
290+ player . gotoTrack ( idx , true ) ;
291+ } ) ;
292+
293+ playlist . appendChild ( trackEl ) ;
294+ } ) ;
295+ }
296+
297+ // Initialize display
298+ renderPlaylist ( ) ;
299+ player . setVolume ( 0.7 ) ;
300+
301+ // Auto-play on load
302+ player . play ( ) ;
52303 </ script >
53304</ body >
54-
55305</ html >
0 commit comments